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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "breakaway",
3
- "version": "1.4.0-main.55",
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",
@@ -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
 
@@ -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
- return isHealthy(ping, opts.version) ? 0 : 1;
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);
@@ -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 Boolean(ping) && ping.ok === true && ping.release === version && ping.secrets?.ok === true;
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 [ "$DEPLOY" != wrangler ]; then echo "A deploy would upload a version."
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 Worker the new version replaces. A first run has none, and `wrangler deploy` makes the Worker and its
177
- # Durable Object; every run after that uploads a version and deploys it, so it can be rolled back. Only a Worker
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
- echo "breakaway $VERSION is running at $address."
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, with its secrets readable, within $waited, and this was the first deploy, so there is no version to go back to. Read the Worker's logs on Cloudflare."
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
@@ -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.