breakaway 1.4.0-main.2 → 1.4.0-main.21
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.
- package/package.json +4 -1
- package/scripts/board-files.mjs +18 -0
- package/scripts/deploy-plan.mjs +135 -0
- package/scripts/lib/deploy-plan.js +102 -0
- package/scripts/lib/package-release.js +71 -0
- package/scripts/package-release.mjs +52 -0
- package/scripts/tasks/cli.js +132 -9
- package/scripts/tasks/init.js +3 -562
- package/scripts/tasks/pipeline.js +747 -0
- package/scripts/tasks.mjs +73 -8
- package/src/cli-version.js +2 -2
- package/src/init.js +590 -0
- package/src/packages.js +56 -0
- package/src/prompt.js +1 -1
- package/src/repos.js +28 -9
- package/src/specs.js +96 -0
- package/template/pipeline/deploy.yml +178 -0
- package/template/pipeline/promote.yml +203 -0
- package/template/pipeline/release.yml +212 -0
- package/template/pipeline/rollback.yml +98 -0
|
@@ -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})."
|