breakaway 1.4.0-main.55 → 1.4.0-main.56
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 +1 -1
- package/prompts/install.md +1 -1
- package/scripts/install/cli.js +17 -2
- package/scripts/install/lib.js +26 -1
- package/template/.github/workflows/deploy.yml +46 -14
- package/template/README.md +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "1.4.0-main.
|
|
3
|
+
"version": "1.4.0-main.56",
|
|
4
4
|
"description": "The task board for you and your coding agents: a Cloudflare Worker, its web app, Taskwarrior sync, and the CLI (npx breakaway).",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/prompts/install.md
CHANGED
|
@@ -82,7 +82,7 @@ gh run list --repo <owner>/<name> --workflow deploy.yml --limit 1 # it takes a
|
|
|
82
82
|
gh run watch <run ID> --repo <owner>/<name> --exit-status
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
When the dry run passes, run it again without `-f dry-run=true` and watch it the same way. It stops with a message when something needs their hands: read the run's log (`gh run view <run ID> --repo <owner>/<name> --log-failed`) and say what it asks for.
|
|
85
|
+
When the dry run passes, run it again without `-f dry-run=true` and watch it the same way. On an address on their own domain, this first deploy can wait up to five minutes for its certificate, and can end with a notice that the board is waiting for its secrets: that's expected, they go on next. It stops with a message when something needs their hands: read the run's log (`gh run view <run ID> --repo <owner>/<name> --log-failed`) and say what it asks for.
|
|
86
86
|
|
|
87
87
|
Then put the three secrets on the Worker. Each command reads its value from `tasks.env` and pipes it to Wrangler, so it never shows:
|
|
88
88
|
|
package/scripts/install/cli.js
CHANGED
|
@@ -13,9 +13,10 @@ import {
|
|
|
13
13
|
bumpBody,
|
|
14
14
|
deployPlan,
|
|
15
15
|
deployTarget,
|
|
16
|
-
isHealthy,
|
|
17
16
|
latestReleases,
|
|
17
|
+
newWorkerStop,
|
|
18
18
|
parseState,
|
|
19
|
+
pingHealth,
|
|
19
20
|
previousVersionId,
|
|
20
21
|
shapeOf,
|
|
21
22
|
updatePlan,
|
|
@@ -152,6 +153,15 @@ export async function runStep(step, opts, io) {
|
|
|
152
153
|
`Couldn't list the Worker's deployments, so the deploy stopped before changing anything. Check that CLOUDFLARE_ACCOUNT_ID is your account's ID and that CLOUDFLARE_API_TOKEN can read Workers on it. Wrangler said:\n${output.trim()}`,
|
|
153
154
|
);
|
|
154
155
|
}
|
|
156
|
+
// A Worker that doesn't exist on an install that already has a board is a changed or mistyped name, not a first
|
|
157
|
+
// deploy (BRK-141): a dispatch has no earlier config for `check` to compare with.
|
|
158
|
+
const stop = newWorkerStop({
|
|
159
|
+
worker: configIn(join(dir, 'breakaway.config.json')).worker,
|
|
160
|
+
variable: opts.variable || null,
|
|
161
|
+
running: opts.running || null,
|
|
162
|
+
at: opts.at || null,
|
|
163
|
+
});
|
|
164
|
+
if (stop) throw new Stop(stop, 2);
|
|
155
165
|
io.out('first=true');
|
|
156
166
|
return 0;
|
|
157
167
|
}
|
|
@@ -163,7 +173,12 @@ export async function runStep(step, opts, io) {
|
|
|
163
173
|
} catch {
|
|
164
174
|
ping = null;
|
|
165
175
|
}
|
|
166
|
-
|
|
176
|
+
// A first deploy answers before its secrets are on (the install puts them on the Worker it made), so there it
|
|
177
|
+
// passes and says so; any later deploy needs them (BRK-141).
|
|
178
|
+
const health = pingHealth(ping, opts.version);
|
|
179
|
+
const first = opts.first === true || opts.first === 'true';
|
|
180
|
+
io.out(`health=${health}`);
|
|
181
|
+
return health === 'healthy' || (first && health === 'secrets') ? 0 : 1;
|
|
167
182
|
}
|
|
168
183
|
if (step === 'update') {
|
|
169
184
|
const state = stateIn(dir);
|
package/scripts/install/lib.js
CHANGED
|
@@ -253,9 +253,34 @@ export function workerMissing(output) {
|
|
|
253
253
|
return /\[code: 10007\]|worker does not exist on your account/iu.test(String(output ?? ''));
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
/**
|
|
257
|
+
* The message that stops a deploy about to make a new Worker on an install that already has a board (BRK-141), or null
|
|
258
|
+
* when making one is a first deploy. A board is there when the repository variable BREAKAWAY_URL is set (`variable`: the
|
|
259
|
+
* install sets it once the board answers) or the address the deploy checked (`at`) answered /api/ping with a release
|
|
260
|
+
* (`running`). A new Worker then means the name changed or is mistyped, and deploying it would open a second, empty board.
|
|
261
|
+
* @param {{ worker: string, variable?: string | null, running?: string | null, at?: string | null }} options
|
|
262
|
+
*/
|
|
263
|
+
export function newWorkerStop({ worker, variable = null, running = null, at = null }) {
|
|
264
|
+
if (!variable && !running) return null;
|
|
265
|
+
const seen = running
|
|
266
|
+
? `${at || variable || 'its address'} answers as breakaway ${running}`
|
|
267
|
+
: `the repository variable BREAKAWAY_URL is set (${variable})`;
|
|
268
|
+
return `There is no Worker named ${worker} on this Cloudflare account, but this install already has a board: ${seen}. Deploying would make a new Worker with an empty board, so nothing was deployed. Put "worker" in breakaway.config.json back to the name the board runs as (Workers & Pages on Cloudflare lists it), then run Deploy again. If that board is gone and you mean to start an empty one, delete the repository variable BREAKAWAY_URL, then run Deploy again.`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* What `/api/ping`'s answer says about the new release: `healthy` when it runs and its secrets load (BRK-96), `secrets`
|
|
273
|
+
* when it runs but a bound secret can't be read yet, and `down` for anything else.
|
|
274
|
+
* @returns {'healthy' | 'secrets' | 'down'}
|
|
275
|
+
*/
|
|
276
|
+
export function pingHealth(ping, version) {
|
|
277
|
+
if (ping?.ok !== true || ping.release !== version) return 'down';
|
|
278
|
+
return ping.secrets?.ok === true ? 'healthy' : 'secrets';
|
|
279
|
+
}
|
|
280
|
+
|
|
256
281
|
/** Whether `/api/ping`'s answer says the new release is running and its secrets load (BRK-96). */
|
|
257
282
|
export function isHealthy(ping, version) {
|
|
258
|
-
return
|
|
283
|
+
return pingHealth(ping, version) === 'healthy';
|
|
259
284
|
}
|
|
260
285
|
|
|
261
286
|
/**
|
|
@@ -131,6 +131,10 @@ jobs:
|
|
|
131
131
|
cat check.out
|
|
132
132
|
if [ "$status" -ne 0 ]; then exit "$status"; fi
|
|
133
133
|
cat check.out >> "$GITHUB_OUTPUT"
|
|
134
|
+
# Where the running board was asked, and what it said: a Worker that doesn't exist is only a first deploy when
|
|
135
|
+
# no board answered there (BRK-141).
|
|
136
|
+
echo "running=$running" >> "$GITHUB_OUTPUT"
|
|
137
|
+
echo "address=$address" >> "$GITHUB_OUTPUT"
|
|
134
138
|
|
|
135
139
|
- name: Make the Worker config
|
|
136
140
|
env:
|
|
@@ -149,15 +153,38 @@ jobs:
|
|
|
149
153
|
fi
|
|
150
154
|
node "$CLI" install config --bundle bundle --out wrangler.generated.json
|
|
151
155
|
|
|
156
|
+
- name: Find the Worker
|
|
157
|
+
id: worker
|
|
158
|
+
env:
|
|
159
|
+
CLI: ${{ steps.release.outputs.cli }}
|
|
160
|
+
VARIABLE: ${{ vars.BREAKAWAY_URL }}
|
|
161
|
+
RUNNING: ${{ steps.check.outputs.running }}
|
|
162
|
+
CHECKED: ${{ steps.check.outputs.address }}
|
|
163
|
+
run: |
|
|
164
|
+
set -euo pipefail
|
|
165
|
+
# The version the deploy replaces. Only a Worker that doesn't exist is a first deploy: any other failure to list
|
|
166
|
+
# (a wrong account ID, a token that can't read Workers) stops here, because deploying over a live Worker with
|
|
167
|
+
# nothing to roll back to is not safe. And a Worker that doesn't exist on an install that already has a board
|
|
168
|
+
# (BREAKAWAY_URL set, or the address answered) is a changed or mistyped name: that stops too, before the dry run,
|
|
169
|
+
# since deploying it would make a second, empty board (BRK-141).
|
|
170
|
+
if npx --yes wrangler@4 deployments list --json -c wrangler.generated.json > deployments.json 2> list-error.txt; then
|
|
171
|
+
node "$CLI" install previous < deployments.json >> "$GITHUB_OUTPUT"
|
|
172
|
+
else
|
|
173
|
+
cat deployments.json list-error.txt | node "$CLI" install missing --variable "$VARIABLE" --running "$RUNNING" --at "$CHECKED"
|
|
174
|
+
echo "previous=" >> "$GITHUB_OUTPUT"
|
|
175
|
+
fi
|
|
176
|
+
|
|
152
177
|
- name: Dry run
|
|
153
178
|
if: ${{ inputs.dry-run }}
|
|
154
179
|
env:
|
|
155
180
|
DEPLOY: ${{ steps.check.outputs.deploy }}
|
|
156
181
|
CHANGES: ${{ steps.check.outputs.changes }}
|
|
182
|
+
PREVIOUS: ${{ steps.worker.outputs.previous }}
|
|
157
183
|
run: |
|
|
158
184
|
set -euo pipefail
|
|
159
185
|
npx --yes wrangler@4 deploy --dry-run --outdir "$RUNNER_TEMP/dry" -c wrangler.generated.json
|
|
160
|
-
if [ "$
|
|
186
|
+
if [ -z "$PREVIOUS" ]; then echo "This is the first deploy: it would make the Worker and its board."
|
|
187
|
+
elif [ "$DEPLOY" != wrangler ]; then echo "A deploy would upload a version."
|
|
161
188
|
elif [ -n "$CHANGES" ]; then echo "A deploy would run wrangler deploy for $CHANGES."
|
|
162
189
|
else echo "A deploy would run wrangler deploy, the release's manual step."; fi
|
|
163
190
|
|
|
@@ -170,19 +197,13 @@ jobs:
|
|
|
170
197
|
DEPLOY: ${{ steps.check.outputs.deploy }}
|
|
171
198
|
CHANGES: ${{ steps.check.outputs.changes }}
|
|
172
199
|
ADDRESS_CHANGED: ${{ steps.check.outputs.address_changed }}
|
|
200
|
+
PREVIOUS: ${{ steps.worker.outputs.previous }}
|
|
173
201
|
run: |
|
|
174
202
|
set -euo pipefail
|
|
203
|
+
previous=$PREVIOUS
|
|
175
204
|
wrangler() { npx --yes wrangler@4 "$@" -c wrangler.generated.json; }
|
|
176
|
-
# The
|
|
177
|
-
# Durable Object; every run after that uploads a version and deploys it, so it can be rolled back.
|
|
178
|
-
# that doesn't exist is a first run: any other failure to list (a wrong account ID, a token that can't read
|
|
179
|
-
# Workers) stops here, because deploying over a live Worker with nothing to roll back to is not safe.
|
|
180
|
-
if npx --yes wrangler@4 deployments list --json -c wrangler.generated.json > deployments.json 2> list-error.txt; then
|
|
181
|
-
previous=$(node "$CLI" install previous < deployments.json | sed 's/^previous=//')
|
|
182
|
-
else
|
|
183
|
-
cat deployments.json list-error.txt | node "$CLI" install missing > /dev/null
|
|
184
|
-
previous=""
|
|
185
|
-
fi
|
|
205
|
+
# The version the new one replaces (Find the Worker). A first run has none, and `wrangler deploy` makes the Worker
|
|
206
|
+
# and its Durable Object; every run after that uploads a version and deploys it, so it can be rolled back.
|
|
186
207
|
if [ -z "$previous" ]; then
|
|
187
208
|
wrangler deploy --var "BREAKAWAY_VERSION:$VERSION"
|
|
188
209
|
elif [ "$DEPLOY" = wrangler ]; then
|
|
@@ -200,25 +221,36 @@ jobs:
|
|
|
200
221
|
url=$(node -p "require('./breakaway.config.json').url || ''")
|
|
201
222
|
address=${ADDRESS:-$url}
|
|
202
223
|
tries=12
|
|
224
|
+
first=false
|
|
203
225
|
if [ "$ADDRESS_CHANGED" = true ]; then
|
|
204
226
|
# A new address: check it, not the old one, and give its custom domain five minutes for its certificate.
|
|
205
227
|
address=${url:-$ADDRESS}
|
|
206
228
|
tries=60
|
|
207
229
|
fi
|
|
230
|
+
if [ -z "$previous" ]; then
|
|
231
|
+
# A first deploy: its custom domain is new too, and its secrets go on the Worker it just made, so a release that
|
|
232
|
+
# answers without them passes, saying so (BRK-141).
|
|
233
|
+
first=true
|
|
234
|
+
if [ -n "$url" ]; then address=$url; tries=60; fi
|
|
235
|
+
fi
|
|
208
236
|
if [ -z "$address" ]; then
|
|
209
237
|
echo "::warning title=Not checked::There is no address to check. Set the repository variable BREAKAWAY_URL to where the board answers (a workers.dev address, say) and the next deploy checks it."
|
|
210
238
|
exit 0
|
|
211
239
|
fi
|
|
212
240
|
for attempt in $(seq 1 "$tries"); do
|
|
213
|
-
if curl -fsS --max-time 15 "$address/api/ping" | node "$CLI" install healthy --version "$VERSION"; then
|
|
214
|
-
|
|
241
|
+
if health=$(curl -fsS --max-time 15 "$address/api/ping" | node "$CLI" install healthy --version "$VERSION" --first "$first"); then
|
|
242
|
+
if [ "$health" = health=secrets ]; then
|
|
243
|
+
echo "::notice title=Waiting for its secrets::breakaway $VERSION is running at $address, and is waiting for its secrets. Put them on the Worker (npx breakaway init-secrets, then .dev.vars.example says where each goes); the board works once they're on."
|
|
244
|
+
else
|
|
245
|
+
echo "breakaway $VERSION is running at $address."
|
|
246
|
+
fi
|
|
215
247
|
exit 0
|
|
216
248
|
fi
|
|
217
249
|
sleep 5
|
|
218
250
|
done
|
|
219
251
|
waited=$([ "$tries" -gt 12 ] && echo "five minutes" || echo "a minute")
|
|
220
252
|
if [ -z "$previous" ]; then
|
|
221
|
-
echo "::error title=Failed its check::breakaway $VERSION didn't answer $address/api/ping
|
|
253
|
+
echo "::error title=Failed its check::breakaway $VERSION didn't answer $address/api/ping within $waited, and this was the first deploy, so there is no version to go back to. A new custom domain can take longer for its certificate: once $address/api/ping answers, the board is up, and its secrets go on next. If it never answers, read the Worker's logs on Cloudflare."
|
|
222
254
|
elif wrangler rollback "$previous" --message "breakaway $VERSION failed its check" --yes; then
|
|
223
255
|
kept=""
|
|
224
256
|
if [ "$DEPLOY" = wrangler ]; then kept=" Rolling back brings back the code only: what wrangler deploy changed${CHANGES:+ ($CHANGES)} stays as it left it."; fi
|
package/template/README.md
CHANGED
|
@@ -30,6 +30,8 @@ The board answers at {{address}}.
|
|
|
30
30
|
|
|
31
31
|
Both workflows run breakaway's CLI from the release's own source on GitHub, with Node. Apart from `wrangler`, nothing comes from npm.
|
|
32
32
|
|
|
33
|
+
If Deploy stops with "There is no Worker named …, but this install already has a board", it changed nothing: `worker` in `breakaway.config.json` isn't the name the board runs as, and deploying it would make a second, empty board. Put the name back. Only if that board is gone and you want an empty one, delete the repository variable `BREAKAWAY_URL`, then run Deploy again.
|
|
34
|
+
|
|
33
35
|
If Deploy stops with "Couldn't list the Worker's deployments", it changed nothing: only a Worker that doesn't exist yet counts as a first deploy. Check that `CLOUDFLARE_ACCOUNT_ID` is your account's ID (32 hex characters) and that `CLOUDFLARE_API_TOKEN` can read and edit Workers on it, then run Deploy again.
|
|
34
36
|
|
|
35
37
|
A release can need steps by hand (a Durable Object class deleted or renamed, say). The workflow stops with those steps in its message and deploys nothing; do them, then deploy with `wrangler`. A release whose only step is `wrangler deploy` says so in its notes, and Deploy runs it itself when it may ([below](#which-token-does-what)). The same goes for a new address in `breakaway.config.json`, which a version upload can't carry: without that, apply it yourself (`npx breakaway install config` makes the Worker config), then run Deploy again.
|