breakaway 1.3.2 → 1.4.0-main.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ # {{header}}
2
+ #
3
+ # Promote: puts the staging build the owner chose live on production, the Worker {{productionName}}. It deploys the
4
+ # files Deploy built and kept for that commit, and builds nothing. The board's Promote button starts it, or Actions,
5
+ # Run workflow, with the commit staging runs. Agents never run this workflow.
6
+ name: Promote
7
+
8
+ on:
9
+ workflow_dispatch:
10
+ inputs:
11
+ sha:
12
+ description: The full commit of the staging build to put live
13
+ required: true
14
+ type: string
15
+ destructive_ok:
16
+ description: I've read the destructive migrations it carries, and they may run
17
+ required: false
18
+ default: false
19
+ type: boolean
20
+
21
+ permissions: {}
22
+
23
+ # Production deploys one at a time, and a rollback waits for a promote (and the other way round).
24
+ concurrency:
25
+ group: deploy-production
26
+ cancel-in-progress: false
27
+
28
+ env:
29
+ STAGING: {{staging}}
30
+ PRODUCTION: {{production}}
31
+ BRANCH: {{branch}}
32
+ HEALTH_URL: {{healthProduction}}
33
+
34
+ jobs:
35
+ production:
36
+ name: production
37
+ if: github.ref == 'refs/heads/{{branchName}}'
38
+ runs-on: ubuntu-latest
39
+ timeout-minutes: 30
40
+ # CLOUDFLARE_API_TOKEN is a secret of this environment, which only {{branchName}} may use: a token for the
41
+ # production Worker only.
42
+ environment: production
43
+ permissions:
44
+ contents: write
45
+ deployments: write
46
+ actions: read
47
+ pull-requests: read
48
+ env:
49
+ GITHUB_TOKEN: ${{ github.token }}
50
+ GH_TOKEN: ${{ github.token }}
51
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
52
+ CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
53
+ DEPLOYS_PAUSED: ${{ vars.DEPLOYS_PAUSED }}
54
+ SHA: ${{ inputs.sha }}
55
+ DESTRUCTIVE_OK: ${{ inputs.destructive_ok }}
56
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
57
+ BREAKAWAY_ENV: production
58
+ WORKER: {{production}}
59
+ steps:
60
+ # The helpers run from {{branchName}}; the build comes from the artifact Deploy kept.
61
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
62
+ with:
63
+ fetch-depth: 0
64
+ persist-credentials: false
65
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
66
+ with:
67
+ node-version: 22
68
+
69
+ - name: Fetch the staging build
70
+ id: artifact
71
+ run: |
72
+ set -euo pipefail
73
+ if ! [[ "$SHA" =~ ^[0-9a-f]{40}$ ]]; then
74
+ echo "::error title=Not a commit::Give the full commit of the staging build to promote (40 characters)."
75
+ exit 1
76
+ fi
77
+ id=$(gh api "repos/$GITHUB_REPOSITORY/actions/artifacts?name=release-$SHA&per_page=1" --jq '[.artifacts[] | select(.expired | not)][0].id // empty')
78
+ if [ -z "$id" ]; then
79
+ echo "::error title=No stored build::Deploy kept no build of ${SHA:0:7}, or it expired after 90 days, so there is nothing to promote. Merge to $BRANCH to make a new candidate."
80
+ exit 1
81
+ fi
82
+ mkdir -p "$RUNNER_TEMP/release"
83
+ gh api "repos/$GITHUB_REPOSITORY/actions/artifacts/$id/zip" > "$RUNNER_TEMP/release.zip"
84
+ unzip -q "$RUNNER_TEMP/release.zip" -d "$RUNNER_TEMP/release"
85
+ echo "digest=$(node scripts/release-artifact.mjs digest "$RUNNER_TEMP/release")" >> "$GITHUB_OUTPUT"
86
+
87
+ # The same refusals as the board's Promote button, checked again here: not paused, the latest successful staging
88
+ # build, on {{branchName}}, not live already, and the stored build is the one staging ran.
89
+ - name: Check the candidate
90
+ id: check
91
+ env:
92
+ DIGEST: ${{ steps.artifact.outputs.digest }}
93
+ run: node scripts/promote-check.mjs --staging "$STAGING" --production "$PRODUCTION" --sha "$SHA" --artifact-digest "$DIGEST" --branch "$BRANCH"
94
+
95
+ - name: Check its migrations
96
+ id: migrations
97
+ run: |
98
+ set -euo pipefail
99
+ live=$(node scripts/deploy-plan.mjs live --environment "$PRODUCTION" | sed 's/^sha=//')
100
+ echo "live=$live" >> "$GITHUB_OUTPUT"
101
+ if [ -n "$live" ]; then
102
+ added=$(git diff --name-only --diff-filter=A "$live" "$SHA" -- migrations | grep '\.sql$' || true)
103
+ else
104
+ added=$(git ls-tree -r --name-only "$SHA" -- migrations | grep '\.sql$' || true)
105
+ fi
106
+ echo "migrations=$(echo "$added" | xargs -r -n1 basename | paste -sd, -)" >> "$GITHUB_OUTPUT"
107
+ # A migration that destroys data carries an owner-approved line, and runs only when the owner ticked it.
108
+ destructive=""
109
+ for file in $added; do
110
+ if git show "$SHA:$file" | grep -qE '^[[:space:]]*--[[:space:]]*owner-approved:[[:space:]]*[^[:space:]]'; then destructive="$destructive $(basename "$file")"; fi
111
+ done
112
+ if [ -n "$destructive" ] && [ "$DESTRUCTIVE_OK" != true ]; then
113
+ echo "::error title=Destructive migration::${SHA:0:7} carries a migration that destroys data:$destructive. Read it, then promote again with destructive_ok."
114
+ exit 1
115
+ fi
116
+
117
+ - name: Record the start
118
+ id: record
119
+ run: |
120
+ node scripts/record-deployment.mjs --environment "$PRODUCTION" --sha "$SHA" --production --state in_progress --description "promoting: starting" --log-url "$RUN_URL"
121
+
122
+ - name: Deploy to production
123
+ id: deploy
124
+ env:
125
+ DEPLOYMENT: ${{ steps.record.outputs.deployment }}
126
+ run: |
127
+ set -euo pipefail
128
+ helpers="$GITHUB_WORKSPACE/scripts"
129
+ step() { node "$helpers/record-deployment.mjs" --deployment "$DEPLOYMENT" --state in_progress --description "promoting: $1" > /dev/null; }
130
+ mkdir -p "$RUNNER_TEMP/app"
131
+ tar -xzf "$RUNNER_TEMP/release/tree.tgz" -C "$RUNNER_TEMP/app"
132
+ cd "$RUNNER_TEMP/app"
133
+ wrangler() { npx --yes wrangler "$@" --name "$WORKER"{{productionEnvFlag}}; }
134
+ previous=""
135
+ if wrangler deployments list --json > "$RUNNER_TEMP/deployments.json" 2> "$RUNNER_TEMP/list-error.txt"; then
136
+ previous=$(node "$helpers/deploy-plan.mjs" current < "$RUNNER_TEMP/deployments.json" | sed 's/^version=//')
137
+ else
138
+ cat "$RUNNER_TEMP/deployments.json" "$RUNNER_TEMP/list-error.txt" | node "$helpers/deploy-plan.mjs" missing > /dev/null
139
+ fi
140
+ step migrating
141
+ {{@beforeDeploy}}
142
+ step deploying
143
+ export WRANGLER_OUTPUT_FILE_PATH="$RUNNER_TEMP/wrangler-output.ndjson"
144
+ if [ -z "$previous" ]; then
145
+ wrangler deploy --message "${SHA:0:7}"
146
+ version=$(node "$helpers/deploy-plan.mjs" uploaded < "$WRANGLER_OUTPUT_FILE_PATH" | sed 's/^version=//')
147
+ else
148
+ wrangler versions upload --message "${SHA:0:7}"
149
+ version=$(node "$helpers/deploy-plan.mjs" uploaded < "$WRANGLER_OUTPUT_FILE_PATH" | sed 's/^version=//')
150
+ wrangler versions deploy "$version@100%" --message "${SHA:0:7}" --yes
151
+ fi
152
+ echo "version=$version" >> "$GITHUB_OUTPUT"
153
+ step checking
154
+ if [ -z "$HEALTH_URL" ]; then
155
+ echo "::notice title=Not checked::Production runs ${SHA:0:7} as version $version. Set healthCheck.production in .github/breakaway-pipeline.json and the next promote checks it, and goes back if it doesn't answer."
156
+ exit 0
157
+ fi
158
+ for attempt in $(seq 1 18); do
159
+ if curl -fsS --max-time 10 -o /dev/null "$HEALTH_URL"; then
160
+ echo "Production runs ${SHA:0:7} as version $version, and $HEALTH_URL answers."
161
+ exit 0
162
+ fi
163
+ sleep 5
164
+ done
165
+ if [ -n "$previous" ] && wrangler rollback "$previous" --message "${SHA:0:7} failed its check" --yes; then
166
+ echo "rolled_back=true" >> "$GITHUB_OUTPUT"
167
+ echo "::error title=Rolled back::${SHA:0:7} didn't answer $HEALTH_URL within 90 seconds, so production went back to version $previous. Read the Worker's logs on Cloudflare."
168
+ else
169
+ echo "::error title=Failed its check::${SHA:0:7} didn't answer $HEALTH_URL within 90 seconds, and production had no version to go back to. Read the Worker's logs on Cloudflare."
170
+ fi
171
+ exit 1
172
+
173
+ - name: Record the end
174
+ if: always() && steps.record.outputs.deployment
175
+ env:
176
+ DEPLOYMENT: ${{ steps.record.outputs.deployment }}
177
+ OUTCOME: ${{ job.status }}
178
+ VERSION: ${{ steps.deploy.outputs.version }}
179
+ DIGEST: ${{ steps.artifact.outputs.digest }}
180
+ MIGRATIONS: ${{ steps.migrations.outputs.migrations }}
181
+ ROLLED_BACK: ${{ steps.deploy.outputs.rolled_back }}
182
+ run: |
183
+ set -euo pipefail
184
+ record() { node scripts/record-deployment.mjs --deployment "$DEPLOYMENT" --log-url "$RUN_URL" "$@" > /dev/null; }
185
+ if [ "$OUTCOME" = success ]; then
186
+ record --state success --version "$VERSION" --artifact "$DIGEST" --migrations "$MIGRATIONS" ${HEALTH_URL:+--environment-url "$HEALTH_URL"}
187
+ elif [ "$ROLLED_BACK" = true ]; then
188
+ record --state failure --description "rolled back: version $VERSION failed its check"
189
+ else
190
+ record --state failure --description "failed: the run says why"
191
+ fi
192
+
193
+ # Production only: a tag and a GitHub release with every pull request since what production ran before.
194
+ - name: Tag and release
195
+ env:
196
+ LIVE: ${{ steps.migrations.outputs.live }}
197
+ VERSION: ${{ steps.deploy.outputs.version }}
198
+ run: |
199
+ set -euo pipefail
200
+ tag="v$(date -u +%Y%m%d)-${SHA:0:7}"
201
+ from=${LIVE:-$(git rev-list --max-parents=0 "$SHA" | tail -1)}
202
+ node scripts/release-notes.mjs --title "${GITHUB_REPOSITORY#*/}" --version "$tag" --from "$from" --to "$SHA" > "$RUNNER_TEMP/notes.md"
203
+ gh release create "$tag" --repo "$GITHUB_REPOSITORY" --target "$SHA" --title "$tag" --notes-file "$RUNNER_TEMP/notes.md" --latest
@@ -0,0 +1,212 @@
1
+ # {{header}}
2
+ #
3
+ # Release: the npm package {{packageName}}. Every merge to {{branchName}} that passes its checks stages a pre-release,
4
+ # X.Y.Z-main.N, on next; the owner releases one as the stable X.Y.Z, on latest, from the same commit. Nothing is
5
+ # published by itself: each version waits, staged on npm, until the owner approves it with 2FA (npm stage approve, or
6
+ # Staged Packages on npmjs.com). The version comes from {{packageDirectory}}/package.json. Keep this file's name: npm's
7
+ # trusted publisher names it, with the npm environment. Agents never run this workflow.
8
+ name: Release
9
+
10
+ on:
11
+ workflow_run:
12
+ workflows: {{checks}}
13
+ types: [completed]
14
+ branches: [{{branch}}]
15
+ # The owner releases a pre-release as stable: the board's Release button, or Actions, Run workflow.
16
+ workflow_dispatch:
17
+ inputs:
18
+ prerelease:
19
+ description: The pre-release to release as stable, like {{prefixName}}1.4.0-main.5
20
+ required: true
21
+ type: string
22
+
23
+ permissions: {}
24
+
25
+ concurrency:
26
+ group: release
27
+ cancel-in-progress: false
28
+
29
+ env:
30
+ PACKAGE: {{package}}
31
+ DIRECTORY: {{directory}}
32
+ ACCESS: {{access}}
33
+ # The tags that count the versions: <prefix>X.Y.Z-main.N for each pre-release, <prefix>X.Y.Z for each stable.
34
+ PREFIX: {{prefix}}
35
+ CHECKS: {{checksEnv}}
36
+
37
+ jobs:
38
+ prerelease:
39
+ name: pre-release
40
+ if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_repository.full_name == github.repository
41
+ runs-on: ubuntu-latest
42
+ timeout-minutes: 20
43
+ # The npm environment, which only {{branchName}} may use, is where npm's trusted publisher points, and holds
44
+ # NPM_TOKEN until that applies.
45
+ environment: npm
46
+ permissions:
47
+ contents: write
48
+ id-token: write
49
+ actions: read
50
+ env:
51
+ GITHUB_TOKEN: ${{ github.token }}
52
+ GH_TOKEN: ${{ github.token }}
53
+ SHA: ${{ github.event.workflow_run.head_sha }}
54
+ DEPLOYS_PAUSED: ${{ vars.DEPLOYS_PAUSED }}
55
+ steps:
56
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
57
+ with:
58
+ ref: ${{ github.event.workflow_run.head_sha }}
59
+ fetch-depth: 0
60
+ persist-credentials: false
61
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
62
+ with:
63
+ node-version: 22
64
+ registry-url: https://registry.npmjs.org
65
+
66
+ - name: Check it should release
67
+ id: plan
68
+ run: |
69
+ set -euo pipefail
70
+ if [ "$DEPLOYS_PAUSED" = true ]; then
71
+ echo "::notice title=Paused::Releases are paused (DEPLOYS_PAUSED is true), so ${SHA:0:7} staged nothing."
72
+ echo "ready=false" >> "$GITHUB_OUTPUT"
73
+ exit 0
74
+ fi
75
+ node scripts/deploy-plan.mjs checks --sha "$SHA" --checks "$CHECKS" >> "$GITHUB_OUTPUT"
76
+
77
+ - name: Pick the version
78
+ id: version
79
+ if: steps.plan.outputs.ready == 'true'
80
+ run: node scripts/package-release.mjs prerelease --dir "$DIRECTORY" --prefix "$PREFIX" --sha "$SHA" >> "$GITHUB_OUTPUT"
81
+
82
+ - name: Install and build
83
+ if: steps.plan.outputs.ready == 'true' && steps.version.outputs.exists == 'false'
84
+ env:
85
+ VERSION: ${{ steps.version.outputs.version }}
86
+ run: |
87
+ set -euo pipefail
88
+ if grep -q '"packageManager"' package.json 2>/dev/null; then corepack enable; fi
89
+ {{@install}}
90
+ {{@build}}
91
+ node -e "const f=require('path').join(process.env.DIRECTORY,'package.json'),p=JSON.parse(require('fs').readFileSync(f));p.version=process.env.VERSION;require('fs').writeFileSync(f,JSON.stringify(p,null,2)+'\n')"
92
+
93
+ # Staged, never published: it goes live on next once the owner approves it with 2FA. npm tries trusted publishing
94
+ # first; NPM_TOKEN stands in until it applies, a token that can't bypass 2FA, so it never publishes by itself.
95
+ - name: Stage it on npm
96
+ if: steps.plan.outputs.ready == 'true' && steps.version.outputs.exists == 'false'
97
+ env:
98
+ VERSION: ${{ steps.version.outputs.version }}
99
+ DIST_TAG: next
100
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
101
+ # npm refuses provenance from a private repository.
102
+ PROVENANCE: ${{ github.event.repository.private && 'no' || 'yes' }}
103
+ run: |
104
+ set -euo pipefail
105
+ cd "$DIRECTORY"
106
+ npm install -g npm@^11.15.0
107
+ provenance=""
108
+ if [ "$PROVENANCE" = yes ]; then provenance=--provenance; fi
109
+ # npm says why trusted publishing didn't apply only in its verbose log. Keep that log to the runner and show
110
+ # npm's usual lines, and on a failure its oidc lines: never the whole verbose log.
111
+ if npm stage publish $provenance --access "$ACCESS" --tag "$DIST_TAG" --loglevel verbose > "$RUNNER_TEMP/npm.log" 2>&1; then
112
+ grep -vE '^npm (verbose|http|timing|silly)' "$RUNNER_TEMP/npm.log" || true
113
+ echo "::notice title=Staged on npm::$PACKAGE@$VERSION goes live on $DIST_TAG once the owner approves it with 2FA: npm stage approve <id>, or Staged Packages on npmjs.com."
114
+ else
115
+ grep -vE '^npm (verbose|http|timing|silly)' "$RUNNER_TEMP/npm.log" || true
116
+ if grep -qiE 'previously published|cannot publish over' "$RUNNER_TEMP/npm.log"; then
117
+ echo "::error title=Already on npm::$PACKAGE@$VERSION is already on npm, and npm takes a version once. Nothing was staged."
118
+ exit 1
119
+ fi
120
+ echo "npm's trusted publishing (OIDC):"
121
+ oidc=$(grep -i 'oidc' "$RUNNER_TEMP/npm.log" | grep -v 'eyJ' | cut -c1-300 || true)
122
+ echo "${oidc:-npm logged nothing about it.}"
123
+ echo "::error title=npm refused to stage it::npm's trusted publisher for $PACKAGE must name exactly: GitHub Actions, user ${GITHUB_REPOSITORY%%/*}, repository ${GITHUB_REPOSITORY#*/}, workflow release.yml, environment npm. Until it applies, the npm environment needs NPM_TOKEN: a granular token with read and write access to $PACKAGE that doesn't bypass 2FA."
124
+ exit 1
125
+ fi
126
+
127
+ - name: Tag it
128
+ if: steps.plan.outputs.ready == 'true' && steps.version.outputs.exists == 'false'
129
+ env:
130
+ TAG: ${{ steps.version.outputs.tag }}
131
+ run: gh api -X POST "repos/$GITHUB_REPOSITORY/git/refs" -f "ref=refs/tags/$TAG" -f "sha=$SHA" > /dev/null
132
+
133
+ stable:
134
+ name: stable
135
+ if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/{{branchName}}'
136
+ runs-on: ubuntu-latest
137
+ timeout-minutes: 20
138
+ environment: npm
139
+ permissions:
140
+ contents: write
141
+ id-token: write
142
+ env:
143
+ GITHUB_TOKEN: ${{ github.token }}
144
+ GH_TOKEN: ${{ github.token }}
145
+ PRERELEASE: ${{ inputs.prerelease }}
146
+ DEPLOYS_PAUSED: ${{ vars.DEPLOYS_PAUSED }}
147
+ steps:
148
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
149
+ with:
150
+ fetch-depth: 0
151
+ persist-credentials: false
152
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
153
+ with:
154
+ node-version: 22
155
+ registry-url: https://registry.npmjs.org
156
+
157
+ - name: Pick the stable
158
+ id: version
159
+ run: |
160
+ set -euo pipefail
161
+ if [ "$DEPLOYS_PAUSED" = true ]; then
162
+ echo "::error title=Paused::Releases are paused (DEPLOYS_PAUSED is true). Release again when the pause is over."
163
+ exit 1
164
+ fi
165
+ node scripts/package-release.mjs stable "$PRERELEASE" --prefix "$PREFIX" >> "$GITHUB_OUTPUT"
166
+
167
+ # The pre-release's own commit, under the stable version: nothing from a later commit.
168
+ - name: Install and build the pre-release's commit
169
+ env:
170
+ VERSION: ${{ steps.version.outputs.version }}
171
+ COMMIT: ${{ steps.version.outputs.commit }}
172
+ run: |
173
+ set -euo pipefail
174
+ git checkout --detach "$COMMIT"
175
+ if grep -q '"packageManager"' package.json 2>/dev/null; then corepack enable; fi
176
+ {{@install}}
177
+ {{@build}}
178
+ node -e "const f=require('path').join(process.env.DIRECTORY,'package.json'),p=JSON.parse(require('fs').readFileSync(f));p.version=process.env.VERSION;require('fs').writeFileSync(f,JSON.stringify(p,null,2)+'\n')"
179
+
180
+ - name: Stage it on npm
181
+ env:
182
+ VERSION: ${{ steps.version.outputs.version }}
183
+ DIST_TAG: latest
184
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
185
+ PROVENANCE: ${{ github.event.repository.private && 'no' || 'yes' }}
186
+ run: |
187
+ set -euo pipefail
188
+ cd "$DIRECTORY"
189
+ npm install -g npm@^11.15.0
190
+ provenance=""
191
+ if [ "$PROVENANCE" = yes ]; then provenance=--provenance; fi
192
+ if npm stage publish $provenance --access "$ACCESS" --tag "$DIST_TAG" --loglevel verbose > "$RUNNER_TEMP/npm.log" 2>&1; then
193
+ grep -vE '^npm (verbose|http|timing|silly)' "$RUNNER_TEMP/npm.log" || true
194
+ echo "::notice title=Staged on npm::$PACKAGE@$VERSION goes live on $DIST_TAG once the owner approves it with 2FA: npm stage approve <id>, or Staged Packages on npmjs.com."
195
+ else
196
+ grep -vE '^npm (verbose|http|timing|silly)' "$RUNNER_TEMP/npm.log" || true
197
+ if grep -qiE 'previously published|cannot publish over' "$RUNNER_TEMP/npm.log"; then
198
+ echo "::error title=Already on npm::$PACKAGE@$VERSION is already on npm, and npm takes a version once. Nothing was staged."
199
+ exit 1
200
+ fi
201
+ echo "npm's trusted publishing (OIDC):"
202
+ oidc=$(grep -i 'oidc' "$RUNNER_TEMP/npm.log" | grep -v 'eyJ' | cut -c1-300 || true)
203
+ echo "${oidc:-npm logged nothing about it.}"
204
+ echo "::error title=npm refused to stage it::npm's trusted publisher for $PACKAGE must name exactly: GitHub Actions, user ${GITHUB_REPOSITORY%%/*}, repository ${GITHUB_REPOSITORY#*/}, workflow release.yml, environment npm. Until it applies, the npm environment needs NPM_TOKEN: a granular token with read and write access to $PACKAGE that doesn't bypass 2FA."
205
+ exit 1
206
+ fi
207
+
208
+ - name: Tag it
209
+ env:
210
+ TAG: ${{ steps.version.outputs.tag }}
211
+ COMMIT: ${{ steps.version.outputs.commit }}
212
+ run: gh api -X POST "repos/$GITHUB_REPOSITORY/git/refs" -f "ref=refs/tags/$TAG" -f "sha=$COMMIT" > /dev/null
@@ -0,0 +1,98 @@
1
+ # {{header}}
2
+ #
3
+ # Roll back: puts production, the Worker {{productionName}}, back on a version it ran before. The board's Roll back
4
+ # button starts it, or Actions, Run workflow (the GitHub mobile app too). It is never paused. Staging goes back by
5
+ # deploying again. Agents never run this workflow.
6
+ name: Roll back
7
+
8
+ on:
9
+ workflow_dispatch:
10
+ inputs:
11
+ worker:
12
+ description: The Worker to roll back (production's)
13
+ required: true
14
+ default: {{production}}
15
+ type: string
16
+ version:
17
+ description: The version to go back to (blank for the one before)
18
+ required: false
19
+ type: string
20
+ reason:
21
+ description: What broke
22
+ required: true
23
+ type: string
24
+
25
+ permissions: {}
26
+
27
+ concurrency:
28
+ group: deploy-production
29
+ cancel-in-progress: false
30
+
31
+ env:
32
+ PRODUCTION: {{production}}
33
+ HEALTH_URL: {{healthProduction}}
34
+
35
+ jobs:
36
+ rollback:
37
+ name: roll back
38
+ if: github.ref == 'refs/heads/{{branchName}}'
39
+ runs-on: ubuntu-latest
40
+ timeout-minutes: 15
41
+ environment: production
42
+ permissions:
43
+ contents: read
44
+ deployments: write
45
+ env:
46
+ GITHUB_TOKEN: ${{ github.token }}
47
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
48
+ CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
49
+ TARGET: ${{ inputs.worker }}
50
+ VERSION: ${{ inputs.version }}
51
+ REASON: ${{ inputs.reason }}
52
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
53
+ steps:
54
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
55
+ with:
56
+ persist-credentials: false
57
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
58
+ with:
59
+ node-version: 22
60
+
61
+ - name: Roll back
62
+ run: |
63
+ set -euo pipefail
64
+ if [ "$TARGET" != "$PRODUCTION" ]; then
65
+ echo "::error title=Production only::Roll back runs on production, $PRODUCTION, not $TARGET. Staging goes back by deploying again."
66
+ exit 1
67
+ fi
68
+ if [ -n "$VERSION" ] && ! [[ "$VERSION" =~ ^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$ ]]; then
69
+ echo "::error title=Not a version::$VERSION isn't a Worker version ID. Leave it blank to go back to the one before."
70
+ exit 1
71
+ fi
72
+ helpers="$GITHUB_WORKSPACE/scripts"
73
+ # Outside the checkout, so wrangler reads no config: the Worker is named, and only its versions change.
74
+ cd "$RUNNER_TEMP"
75
+ wrangler() { npx --yes wrangler@4 "$@" --name "$PRODUCTION"; }
76
+ wrangler rollback ${VERSION:+"$VERSION"} --message "$REASON" --yes
77
+ wrangler deployments list --json > deployments.json
78
+ now=$(node "$helpers/deploy-plan.mjs" current < deployments.json | sed 's/^version=//')
79
+ sha=$(node "$helpers/deploy-plan.mjs" sha-of --environment "$PRODUCTION" --version "$now" | sed 's/^sha=//')
80
+ if [ -z "$sha" ]; then
81
+ echo "::warning title=Commit unknown::No Deployment recorded version $now, so the rollback is recorded on $GITHUB_SHA."
82
+ sha=$GITHUB_SHA
83
+ fi
84
+ deployment=$(node "$helpers/record-deployment.mjs" --environment "$PRODUCTION" --sha "$sha" --task rollback --production --state in_progress --description "rolling back" --log-url "$RUN_URL")
85
+ state=success
86
+ if [ -n "$HEALTH_URL" ]; then
87
+ state=failure
88
+ for attempt in $(seq 1 18); do
89
+ if curl -fsS --max-time 10 -o /dev/null "$HEALTH_URL"; then state=success; break; fi
90
+ sleep 5
91
+ done
92
+ fi
93
+ node "$helpers/record-deployment.mjs" --deployment "$deployment" --state "$state" --note "rolled back: $REASON" --version "$now" --log-url "$RUN_URL" > /dev/null
94
+ if [ "$state" != success ]; then
95
+ echo "::error title=Still failing::Production went back to version $now, but $HEALTH_URL doesn't answer. Read the Worker's logs on Cloudflare."
96
+ exit 1
97
+ fi
98
+ echo "Production runs version $now again (${sha:0:7})."