-
Notifications
You must be signed in to change notification settings - Fork 56
192 lines (176 loc) · 8.28 KB
/
Copy pathpropagate_release_bundle.yml
File metadata and controls
192 lines (176 loc) · 8.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
name: Propagate release bundle
# On repository_dispatch (event type publish-bundle, sent by
# sync_code_repo.yml in code-development right after it publishes a release
# here), download the release asset simplerisk-<version>.tgz and propagate
# those exact bytes to the legacy distribution channels:
# 1. simplerisk/bundles (master) - via the Git Data API, never a clone
# (the repo is 3.4 GB; even a depth-1 clone fetches every tarball)
# 2. s3://<DOWNLOADS_BUCKET>/public/bundles/ - unconditional overwrite;
# a re-release means the old object is stale by definition
# Both converge on re-runs; heal anything by re-dispatching. The prod
# releases.xml bundle_md5/sha256 are NOT written here - sync_code_repo.yml
# (which builds the asset) writes the complete prod feed entry with the
# real hashes; the S3 object above derives from the same asset, so they
# agree by construction.
#
# Explicit dispatch (not on: release: published) is deliberate: it fires
# only after the asset exists, and it covers re-releases - a
# `gh release upload --clobber` fires no release event, which is exactly
# the case where stale channel artifacts must be replaced.
#
# Requires (one-time setup):
# - Secrets CORE_SYNC_APP_ID / CORE_SYNC_APP_PRIVATE_KEY (the
# SimpleRisk Core Sync App, installed on code, bundles, and
# updates.simplerisk.com)
# - Variables AWS_REGION, DOWNLOADS_BUCKET, DOWNLOADS_PUBLISHER_ROLE_ARN
# (OIDC role trusting this repo's master, PutObject on
# public/bundles/*)
#
# Design: docs/superpowers/specs/2026-06-06-release-bundle-propagation-design.md
# in the code-development repo.
on:
repository_dispatch:
types: [publish-bundle]
# Manual heals and ad-hoc propagation of backfilled releases.
workflow_dispatch:
inputs:
version:
description: 'Release version (YYYYMMDD-NNN)'
required: true
permissions:
contents: read
id-token: write # OIDC role assumption for the S3 upload
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
# Re-dispatches for the same or different versions queue rather than race.
concurrency:
group: propagate-release-bundle
cancel-in-progress: false
jobs:
propagate:
runs-on: ubuntu-latest
steps:
- name: Resolve and validate version
id: ver
env:
DISPATCH_VERSION: ${{ github.event.client_payload.version }}
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
VERSION="${DISPATCH_VERSION:-$INPUT_VERSION}"
if ! printf '%s' "$VERSION" | grep -qE '^[0-9]{8}-[0-9]{3}$'; then
echo "::error::invalid or missing version: '$VERSION'"; exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Download release asset and compute checksums
id: asset
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
gh release download "$VERSION" -R simplerisk/code \
--pattern "simplerisk-$VERSION.tgz" --output "simplerisk-$VERSION.tgz"
SIZE=$(wc -c < "simplerisk-$VERSION.tgz" | tr -d ' ')
# The Git Data blob API and git itself both cap files at 100 MB;
# refuse early with a clear error when we get close (95 MB).
if [ "$SIZE" -ge 99614720 ]; then
echo "::error::bundle is $SIZE bytes (>= 95 MiB) - too close to the 100 MB API/git limit"; exit 1
fi
MD5=$(md5sum "simplerisk-$VERSION.tgz" | cut -d' ' -f1)
SHA256=$(sha256sum "simplerisk-$VERSION.tgz" | cut -d' ' -f1)
{
echo "md5=$MD5"
echo "sha256=$SHA256"
echo "size=$SIZE"
} >> "$GITHUB_OUTPUT"
# SHA-pinned (same pin as sync_code_repo.yml in code-development) so a
# supply-chain compromise of the action repo can't run new code in a
# token-minting job.
- name: Mint bundles-repo token
id: bundles-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.CORE_SYNC_APP_ID }}
private-key: ${{ secrets.CORE_SYNC_APP_PRIVATE_KEY }}
owner: simplerisk
repositories: bundles
- name: Push bundle to the bundles repo
id: bundles
env:
GH_TOKEN: ${{ steps.bundles-token.outputs.token }}
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
FILE="simplerisk-$VERSION.tgz"
# Idempotency: compare git blob shas via the trees API (the
# contents API errors on >1 MB files; tree listings are cheap).
NEW_SHA=$(git hash-object "$FILE")
# An absent file yields empty jq output with exit 0; a non-zero
# exit is a REAL API failure (auth, rate limit, network) and must
# fail here, not several Git Data calls later with a cryptic error.
EXISTING_SHA=$(gh api "repos/simplerisk/bundles/git/trees/master" \
--jq ".tree[] | select(.path==\"$FILE\") | .sha") \
|| { echo "::error::failed to fetch bundles tree — cannot check idempotency"; exit 1; }
if [ "$NEW_SHA" = "$EXISTING_SHA" ]; then
echo "action=identical-skip" >> "$GITHUB_OUTPUT"
exit 0
fi
# Git Data API write path: blob -> tree -> commit -> ref. The blob
# endpoint is the documented route for files up to 100 MB; the
# base64 payload (~1.33x file size) goes via --input, never argv.
python3 -c '
import base64, json, sys
with open(sys.argv[1], "rb") as f:
content = base64.b64encode(f.read()).decode()
json.dump({"content": content, "encoding": "base64"}, open("/tmp/blob.json", "w"))
' "$FILE"
BLOB_SHA=$(gh api -X POST repos/simplerisk/bundles/git/blobs \
--input /tmp/blob.json --jq .sha)
rm -f /tmp/blob.json
HEAD_SHA=$(gh api repos/simplerisk/bundles/git/ref/heads/master --jq .object.sha)
BASE_TREE=$(gh api "repos/simplerisk/bundles/commits/$HEAD_SHA" --jq .commit.tree.sha)
# jq-built payloads: gh api's repeated-key array syntax has
# unreliable object grouping for arrays of objects.
jq -n --arg base "$BASE_TREE" --arg path "$FILE" --arg sha "$BLOB_SHA" \
'{base_tree: $base, tree: [{path: $path, mode: "100644", type: "blob", sha: $sha}]}' \
> /tmp/tree.json
TREE_SHA=$(gh api -X POST repos/simplerisk/bundles/git/trees --input /tmp/tree.json --jq .sha)
jq -n --arg msg "SimpleRisk $VERSION Release" --arg tree "$TREE_SHA" --arg parent "$HEAD_SHA" \
'{message: $msg, tree: $tree, parents: [$parent],
author: {name: "SimpleRisk Updater", email: "support@simplerisk.com"}}' \
> /tmp/commit.json
COMMIT_SHA=$(gh api -X POST repos/simplerisk/bundles/git/commits --input /tmp/commit.json --jq .sha)
gh api -X PATCH repos/simplerisk/bundles/git/refs/heads/master \
-f sha="$COMMIT_SHA" >/dev/null
if [ -n "$EXISTING_SHA" ]; then
echo "action=replaced" >> "$GITHUB_OUTPUT"
else
echo "action=committed" >> "$GITHUB_OUTPUT"
fi
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.DOWNLOADS_PUBLISHER_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- name: Upload to S3 (unconditional overwrite)
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
aws s3 cp "simplerisk-$VERSION.tgz" \
"s3://${{ vars.DOWNLOADS_BUCKET }}/public/bundles/simplerisk-$VERSION.tgz"
- name: Step summary
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
{
echo "## Propagated release bundle"
echo
echo "- Version: \`$VERSION\` (${{ steps.asset.outputs.size }} bytes)"
echo "- MD5: \`${{ steps.asset.outputs.md5 }}\`"
echo "- SHA256: \`${{ steps.asset.outputs.sha256 }}\`"
echo "- bundles repo: ${{ steps.bundles.outputs.action }}"
echo "- S3 public/bundles: uploaded (overwrite)"
} >> "$GITHUB_STEP_SUMMARY"