breakaway 1.4.0-main.4 → 1.4.0-main.41
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/.agents/skills/pipeline/SKILL.md +127 -0
- package/.agents/skills/tasks/SKILL.md +2 -0
- package/README.md +1 -1
- package/package.json +10 -5
- package/prompts/breakaway.md +1 -1
- package/prompts/core.md +19 -2
- package/scripts/board-files.mjs +20 -0
- package/scripts/deploy-plan.mjs +135 -0
- package/scripts/lib/deploy-plan.js +102 -0
- package/scripts/lib/package-release.js +90 -0
- package/scripts/package-release.mjs +60 -0
- package/scripts/tasks/cli.js +164 -15
- package/scripts/tasks/init.js +3 -562
- package/scripts/tasks/pipeline.js +911 -0
- package/scripts/tasks.mjs +171 -15
- package/src/cli-version.js +5 -6
- package/src/init.js +568 -0
- package/src/packages.js +56 -0
- package/src/prompt.js +3 -1
- package/src/repos.js +28 -9
- package/src/specs.js +96 -0
- package/template/pipeline/ci.yml +40 -0
- package/template/pipeline/deploy.yml +178 -0
- package/template/pipeline/promote.yml +203 -0
- package/template/pipeline/release.yml +283 -0
- package/template/pipeline/rollback.yml +98 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pipeline
|
|
3
|
+
description: Use when a task asks to move a repository's CI/CD to breakaway's deploy flow or release flow (Move to breakaway's deploy flow, Deploy with breakaway, Release with breakaway), to write or change .github/breakaway-pipeline.json, to run npx breakaway pipeline init or pipeline check, or to map an old deploy or npm publish workflow onto Deploy, Promote, Roll back, and Release.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Moving a repository to the deploy flow
|
|
7
|
+
|
|
8
|
+
A move is one task and one pull request: you read how the repository checks, deploys, and publishes today, write `.github/breakaway-pipeline.json`, render the workflows from it with `npx breakaway pipeline init`, and account for every step of the old setup, so nothing is lost and nothing runs twice. The design is the [move's spec](../../../docs/specs/IDEA-27-move-ci-cd-to-the-deploy-flow.md) (sections 2, 2b, and 3), and the [docs](../../../docs/tasks.md#moving-a-repository-to-the-deploy-flow) say what the flows do.
|
|
9
|
+
|
|
10
|
+
It is an ordinary build: claim, check in on the peloton, and hand over the way the repository's prompt and the `tasks` skill say. This skill is how to do the move itself.
|
|
11
|
+
|
|
12
|
+
**You never** deploy, publish, run or re-run a workflow, touch Cloudflare, npm, or the repository's GitHub settings (environments, secrets, variables, the App's permissions), or set the repository's pipeline on the board (`repos modify --pipeline` and **Turn on deploys** are the owner's). You write files in a pull request; the owner does the rest.
|
|
13
|
+
|
|
14
|
+
## 1. Read what's there
|
|
15
|
+
|
|
16
|
+
Read all of it before you decide anything:
|
|
17
|
+
|
|
18
|
+
- **Workflows**, `.github/workflows/*.yml`: each one's `name:` line, its triggers (`push`, `pull_request`, `workflow_run`, `release`, `workflow_dispatch`, `schedule`), and every job and step. Note which steps check (lint, test, build, typecheck), which deploy (`wrangler deploy`, `wrangler versions upload`, `cloudflare/wrangler-action`), which publish (`npm publish`, or pnpm's or yarn's, `JS-DevTools/npm-publish`, a `release` event), and which do something else (a migration, a cache purge, a notification, a manual approval through an `environment` with reviewers).
|
|
19
|
+
- **Wrangler config**, `wrangler.jsonc`, `wrangler.json`, or `wrangler.toml`: the Worker's `name`, its `env` blocks (each one's name, and the Worker name it deploys as), and its bindings, above all D1 databases and their `migrations_dir`.
|
|
20
|
+
- **Migrations**: the folder and how they're applied today (`wrangler d1 migrations apply`, a script).
|
|
21
|
+
- **`package.json`**: `name`, `version`, `private`, `scripts` (`build`, `test`, `deploy`, `release`, `prepublishOnly`), `publishConfig`, and `workspaces`; and the lockfile, which says the install command (`npm ci` for `package-lock.json`, or pnpm's or yarn's frozen install for theirs).
|
|
22
|
+
- **Versioning tools**: `.changeset/`, `release-please-config.json`, `.releaserc*` or a `release` key in `package.json` (semantic-release), and `lerna.json`.
|
|
23
|
+
- **Anything already at the flow's paths**: `.github/breakaway-pipeline.json`, `.github/deploy-paths.json`, and `deploy.yml`, `promote.yml`, `rollback.yml`, or `release.yml` in `.github/workflows/`.
|
|
24
|
+
- **Scripts the workflows run**, in `scripts/` or `package.json`: what each does, so you know whether `beforeDeploy` can run it.
|
|
25
|
+
|
|
26
|
+
## 2. Decide what moves
|
|
27
|
+
|
|
28
|
+
| The repository today | What you do |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| Deploys a Worker with `wrangler`, from Actions, Workers Builds, or by hand | Move it: `workers` in the config. |
|
|
31
|
+
| Has checks but deploys by hand | Move it: the deploy is new, and the checks stay as they are. |
|
|
32
|
+
| Publishes a package to npm, from Actions or by hand | Move it: `package` in the config. |
|
|
33
|
+
| Deploys a Worker and publishes a package | Both, in one config and one pull request. |
|
|
34
|
+
| Publishes to another registry (GitHub Packages, JSR, PyPI) | That publishing stays as it is. Say so in the pull request; move a Worker deploy if there is one. |
|
|
35
|
+
| Deploys somewhere else (Pages, Vercel, Fly, a server, containers) | Stop (below). breakaway's deploy flow runs Cloudflare Workers only; a deploy command of the repository's own is planned, not built. |
|
|
36
|
+
| `package.json` says `"private": true` and nothing deploys | Stop: there is nothing the flows can take. |
|
|
37
|
+
|
|
38
|
+
The first version takes **one staging and one production Worker** and **one package** per repository. With more Workers, move the pair the task names (or that the old deploy targets on the default branch), and leave the others' deploys as they are, saying so. With several publishable packages (a monorepo), move the one the task names (or ask, below), and leave the others' publishing as it is.
|
|
39
|
+
|
|
40
|
+
**Stopping** means changing nothing and opening no pull request: `comment` on the task what you found and why it doesn't move (the row above, in a sentence the owner can act on), then `release` it. When only the owner can choose (which package, which Worker pair, or what to do with a versioning tool), ask with a decision instead (the core's "Asking for a decision") and release.
|
|
41
|
+
|
|
42
|
+
## 3. Write the config
|
|
43
|
+
|
|
44
|
+
`.github/breakaway-pipeline.json` is the repository's own. Every value comes from what you read, never invented:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"workers": { "staging": "widgets-staging", "production": "widgets" },
|
|
49
|
+
"wranglerEnv": { "staging": "staging", "production": "production" },
|
|
50
|
+
"branch": "main",
|
|
51
|
+
"checks": ["CI"],
|
|
52
|
+
"install": "npm ci",
|
|
53
|
+
"build": "npm run build",
|
|
54
|
+
"beforeDeploy": ["npx wrangler d1 migrations apply DB --remote --env $BREAKAWAY_ENV"],
|
|
55
|
+
"deployPaths": { "widgets": "^(src|public|migrations)/|^wrangler\\.jsonc$|^package(-lock)?\\.json$" },
|
|
56
|
+
"healthCheck": { "staging": "https://widgets-staging.example.workers.dev/", "production": "https://widgets.example.com/" },
|
|
57
|
+
"package": { "name": "widgets", "directory": ".", "access": "public" }
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **`workers`**: the staging and production Worker names, as the wrangler config deploys them (with its `env` blocks, the name each environment deploys as). Two different Workers. A repository with only production today needs a staging Worker: name it `<production>-staging`, and the owner creates it (the checklist).
|
|
62
|
+
- **`wranglerEnv`**: only when the wrangler config has `env` blocks; the environment name for each Worker, passed as `--env`.
|
|
63
|
+
- **`branch`**: the default branch, when it isn't `main`.
|
|
64
|
+
- **`checks`**: the `name:` line of each workflow that must pass before a deploy or a release, exactly as written. Every check the old deploy waited for, and the ones that run on push to the default branch. Never a workflow you're about to remove.
|
|
65
|
+
- **`install`, `build`**: the repository's own one-line commands, from its old workflow or lockfile. Leave `build` out when nothing builds.
|
|
66
|
+
- **`beforeDeploy`**: the old deploy's steps before `wrangler deploy` (migrations first), one command each. They read `$BREAKAWAY_ENV` (`staging` or `production`) and `$WORKER`. A step that needs more than one line goes in a script the command runs; a step that can't run there stays where it is (step 5).
|
|
67
|
+
- **`deployPaths`**: per Worker, a regular expression of the paths whose change needs a deploy: its source, static assets, migrations, the wrangler config, and the package and lockfile. The board reads the same patterns to say "No deploy needed", so a path left out never ships and a path put in too many deploys for nothing. You can't prove them from reading, so the pull request lists each part and what it covers (step 6).
|
|
68
|
+
- **`healthCheck`**: an address the old workflow or the README checks after a deploy, as a string (staging only) or `{ "staging", "production" }`. Leave it out when there is none; never guess one.
|
|
69
|
+
- **`package`**: `name` exactly as its `package.json` says, `directory` its folder (`.` at the root), and `access` (`restricted` only when `publishConfig.access` or the old publish says so; a scoped package published `public` today stays `public`). Its `package.json` needs a plain `X.Y.Z` version: the flow counts from it. A repository that only publishes has no `workers`, `deployPaths`, `healthCheck`, `beforeDeploy`, or `wranglerEnv`.
|
|
70
|
+
|
|
71
|
+
**A versioning tool with its own scheme** (changesets, semantic-release, release-please) decides versions from commits or changeset files; the release flow takes `package.json`'s version and stages `X.Y.Z-main.N` on `next` for every merge. They can't both run, and one can't be mapped onto the other. Don't replace it: ask the owner with a decision (keep the tool and move only the Worker deploy; or drop the tool for the release flow, which a later task does), and stop the package part until it's answered. Move a Worker deploy in the meantime only if the decision says to.
|
|
72
|
+
|
|
73
|
+
## 4. Render the workflows
|
|
74
|
+
|
|
75
|
+
In the checkout, run `npx breakaway pipeline init`. It checks the config, naming the field that's wrong and what it should be, and the rendered workflows (YAML, pinned actions, expressions, secrets only in an environment), and writes nothing if either fails. It writes `.github/workflows/deploy.yml`, `promote.yml`, `rollback.yml`, and `.github/deploy-paths.json` for `workers`, and `.github/workflows/release.yml` for `package`. Never edit what it writes: change the config and run it again.
|
|
76
|
+
|
|
77
|
+
- **It refuses a file already there** that it didn't render. When that file is the old deploy or publish workflow you're replacing, delete it in this pull request (step 5) and run `pipeline init` again. When it's something else that only shares the name, stop and ask the owner with a decision: never rename or overwrite it silently.
|
|
78
|
+
- **It names helper scripts the repository lacks** (`scripts/record-deployment.mjs` and the others). `repos init <slug> --update` copies them, and that is the owner's: say so under **After merging** (the workflows fail without them), unless they're already there.
|
|
79
|
+
- **`npx breakaway pipeline check`** passes before you hand over: the config is sound, and the files are what it renders now.
|
|
80
|
+
|
|
81
|
+
## 5. Nothing lost, nothing twice
|
|
82
|
+
|
|
83
|
+
Every old job and step ends up in exactly one place: the config, a rendered workflow, kept where it was, or dropped with the reason. The rules:
|
|
84
|
+
|
|
85
|
+
- **Checks are the repository's own.** Never edit or remove a workflow that checks. Deploy and Release wait for them by name.
|
|
86
|
+
- **A workflow that only deploys or only publishes** comes out in the same pull request, deleted, or with its trigger on the default branch removed when it also runs for something else (a tag, by hand, a preview on pull requests, which stays). Otherwise the merge deploys or publishes twice.
|
|
87
|
+
- **A workflow that checks and deploys, or checks and publishes**, is split: its checks stay as they are, and the deploy or publish job or steps come out (with the `needs:`, `if:`, permissions, and `environment` only they used). If its deploy job is all that runs on push to the default branch, drop that trigger too, so the checks still run where they did.
|
|
88
|
+
- **Workers Builds** (Cloudflare's git integration) deploys from Cloudflare, not from a file: you can't turn it off. Say under **After merging** that the owner disconnects it before merging, or the merge deploys twice.
|
|
89
|
+
- **A step the flow can't do** (a manual approval, a custom deploy script `beforeDeploy` can't run, a notification, a cache purge, publishing to another registry) stays exactly where it is, and the pull request lists it under **After merging** for the owner to decide. Never invent an equivalent.
|
|
90
|
+
- **Publishing by hand** (a `release` or `publish` script someone runs from a laptop) stays in `package.json`; the pull request says the release flow replaces it and the owner stops running it.
|
|
91
|
+
- **Never** add or change a secret, an environment, or a variable, and never put a token in a file.
|
|
92
|
+
|
|
93
|
+
## 6. The pull request
|
|
94
|
+
|
|
95
|
+
Open it the way the repository's prompt's **Pull requests** says, closing the task. Its description holds, besides what that asks:
|
|
96
|
+
|
|
97
|
+
- **What moves**: the row from step 2, the Workers, and the package.
|
|
98
|
+
- **Step by step**: a table of every old workflow, job, and step, and where each went:
|
|
99
|
+
|
|
100
|
+
| Old | Was | Now |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `deploy.yml` · deploy | `wrangler deploy` on push to `main` | Deploy (`deploy.yml`, rendered) after CI passes; the old file is removed |
|
|
103
|
+
| `ci.yml` · test | `npm test` | Kept as it is; Deploy and Release wait for CI |
|
|
104
|
+
| `ci.yml` · publish | `npm publish` on a tag | Release (`release.yml`): `next` on every merge, `latest` on the owner's Release; the step is removed |
|
|
105
|
+
| `deploy.yml` · migrate | `wrangler d1 migrations apply` | `beforeDeploy` |
|
|
106
|
+
| `deploy.yml` · notify | posts to chat | Kept where it was (After merging) |
|
|
107
|
+
|
|
108
|
+
- **Deploy paths**: each pattern's parts and what they cover, so the owner can check them.
|
|
109
|
+
- **After merging**: the owner's checklist below, the steps kept where they were, and anything you stopped short of.
|
|
110
|
+
|
|
111
|
+
**The owner's part comes before the merge.** The merge itself runs Deploy and Release once its checks pass, so without the owner's part both fail. Add a `+owner` task for it, filled in like any task, with the checklist in its brief and only what this repository needs. It doesn't depend on the move task (it comes first), and the pull request's **After merging** repeats it under "Before you merge":
|
|
112
|
+
|
|
113
|
+
- the staging and production Workers (or confirm the existing ones), and a Cloudflare API token for each;
|
|
114
|
+
- the GitHub environments `staging` and `production`, each with its token as `CLOUDFLARE_API_TOKEN` and restricted to the default branch, and the repository variable `CLOUDFLARE_ACCOUNT_ID`;
|
|
115
|
+
- read and write on **Actions** for the board's GitHub App on the repository (Promote, Roll back, and Release start workflows);
|
|
116
|
+
- the helper scripts, with `repos init <slug> --update`, when `pipeline init` named any;
|
|
117
|
+
- Workers Builds disconnected, when the repository used it;
|
|
118
|
+
- for a package: the GitHub environment `npm`, restricted to the default branch; on npm, a trusted publisher for `release.yml` and the `npm` environment, or a granular `NPM_TOKEN` in that environment that can't bypass 2FA; and, after each run, approving the staged version on npm with 2FA (`npm stage approve <id>`, or Staged Packages on npmjs.com). The board shows what waits and never approves;
|
|
119
|
+
- optional: the repository variable `DEPLOYS_PAUSED` (`true` stops Promote and Release), and the health-check addresses;
|
|
120
|
+
- after the merge, **Turn on deploys** on the repository's GitHub page, which sets its pipeline from the files.
|
|
121
|
+
|
|
122
|
+
## Before handing over
|
|
123
|
+
|
|
124
|
+
- `npx breakaway pipeline check` passes.
|
|
125
|
+
- The repository's own checks pass, and every workflow you changed still parses and runs the jobs it ran before, minus what moved.
|
|
126
|
+
- Every old step is in the table, and no deploy or publish is left that the merge would run besides Deploy and Release.
|
|
127
|
+
- No secret, token, or address you didn't read in the repository is in a file, the task, or the pull request.
|
|
@@ -31,6 +31,7 @@ breakaway's work is on the board that tracks this repository. The CLI is `npx br
|
|
|
31
31
|
| You need the owner to choose | Ask with a decision, not prose: `add "<title>" --tag owner --decision <file.json>` and make the work that waits `--depends` on it. Only the owner answers, on the board; read the answers with `show`. |
|
|
32
32
|
| Part of the work needs the owner (an install, a dashboard, a sign-off) | Finish your part, then `add` a `+owner` task for the rest that `--depends` on yours. |
|
|
33
33
|
| Task needs design choices | Write the spec in `docs/specs/<ID>-<slug>.md` and `modify <ID> --spec <path>`. |
|
|
34
|
+
| Reading the repository's specs | `tasks specs` lists them, newest first, with each one's status and its tasks; `specs show <path>` prints one with the tasks that link it. They're read from GitHub's default branch, so a spec still in a pull request isn't there yet. |
|
|
34
35
|
| You're blocked by another task | `comment` why, `release`, and pick the blocker or another task. |
|
|
35
36
|
| A claim looks abandoned | Ask the owner; don't take it. |
|
|
36
37
|
| Opening a pull request for a spec, plan, or partial step | Write `Part of <ID>.`, not `Closes`, and don't put it in `--pr`: merging the pull request in that field finishes the task. A branch name alone never closes anything. |
|
|
@@ -38,6 +39,7 @@ breakaway's work is on the board that tracks this repository. The CLI is `npx br
|
|
|
38
39
|
| `claim` says the task belongs to another repository | Don't cross it with `--repo`: that work belongs in a checkout of its own repository. `comment` and `release` if the board started you on it. |
|
|
39
40
|
| The board started you | Follow [`prompts/breakaway.md`](../../../prompts/breakaway.md), which starts with the core. Check the payload's `Repository:` line against `git remote get-url origin` first. |
|
|
40
41
|
| The task is an `IDEA-` | Shape it, don't build it: "Shaping an idea" in the core. |
|
|
42
|
+
| The board started you on a kickoff (`Mode: kickoff`) | Interview the owner first: ask plain questions as a decision on the IDEA (at most 12, then at most 6 more if something important is open), `release`, and stop; once they're answered, shape it with `AGENTS.md`, the prompt's sections, an **In short**, and a `<slug>-v1` feature. "Kicking off a project" in the core. |
|
|
41
43
|
| The board started you from the owner's prompt (`Mode: general`) | Give the task an area first (`modify <ID> --project <area>` gives it its work ID), retitle it, and take the smallest path: a pull request, board edits noted on each task, a spec, a task in another repository, or a decision or ping. Releasing it with no pull request closes it. "Running a general agent" in the core. |
|
|
42
44
|
| The board started you to review a pull request (`Mode: pr-review`) | Test it and read it against the task; answer with `review <ID> --verdict ready\|follow-up\|changes "<note>"` and `release`. Never push or merge. "Reviewing a pull request" in the core. |
|
|
43
45
|
| Only the owner can help, or the task is already done or won't reproduce | `ping <ID> --kind blocked\|question\|stale\|done "<message>"`, then `release`. Ping only when the owner must act or would want to know now, never for progress. Full rules: "Pinging the owner" in the core. |
|
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Rather do it by hand? [The self-hosting guide](https://github.com/TheAnarchoX/br
|
|
|
52
52
|
<img alt="Agents claim the work. You merge it. In three steps: an agent, claude-brk-12, claims the task BRK-12; it opens a pull request that says Closes BRK-12.; you merge, and the task is done." src="https://raw.githubusercontent.com/TheAnarchoX/breakaway/main/docs/media/how-dark.png" width="100%">
|
|
53
53
|
</picture>
|
|
54
54
|
|
|
55
|
-
1. **Write the work down.** Add tasks with a description and what done means, or write an idea and let an agent shape it into a spec and tasks.
|
|
55
|
+
1. **Write the work down.** Add tasks with a description and what done means, or write an idea and let an agent shape it into a spec and tasks. Starting something with no repository yet? **Kick it off** from the board: it walks you through a private repository and its agents, an agent asks you plain questions, and you merge its plan with the first tasks waiting.
|
|
56
56
|
2. **Agents claim it.** A claim is atomic, so two agents never work the same task. Start Claude Code cloud agents from the board, or let local Claude Code sessions pick up work through the CLI.
|
|
57
57
|
3. **Pull requests close tasks.** A pull request that says `Closes BRK-12.` puts the task in review. The task is done when you merge it.
|
|
58
58
|
4. **They ping you when they're stuck.** An agent that needs you sends a ping to your inbox. The rest waits on the board.
|
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.41",
|
|
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",
|
|
@@ -21,17 +21,21 @@
|
|
|
21
21
|
"!scripts/install/*.test.js",
|
|
22
22
|
"src/cli-version.js",
|
|
23
23
|
"src/decision.js",
|
|
24
|
+
"src/init.js",
|
|
24
25
|
"src/install.js",
|
|
25
26
|
"src/model.js",
|
|
27
|
+
"src/packages.js",
|
|
26
28
|
"src/ping.js",
|
|
27
29
|
"src/promote.js",
|
|
28
30
|
"src/prompt.js",
|
|
29
31
|
"src/redact.js",
|
|
30
32
|
"src/repos.js",
|
|
33
|
+
"src/specs.js",
|
|
31
34
|
"src/versions.js",
|
|
32
35
|
"prompts/*.md",
|
|
33
36
|
"taskrc",
|
|
34
37
|
".agents/skills/tasks/SKILL.md",
|
|
38
|
+
".agents/skills/pipeline/SKILL.md",
|
|
35
39
|
"template"
|
|
36
40
|
],
|
|
37
41
|
"engines": {
|
|
@@ -47,15 +51,16 @@
|
|
|
47
51
|
},
|
|
48
52
|
"scripts": {
|
|
49
53
|
"dev": "vite",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
54
|
+
"prepare": "node scripts/board-files.mjs",
|
|
55
|
+
"build": "node scripts/board-files.mjs && vite build",
|
|
56
|
+
"test": "node scripts/board-files.mjs && vitest run && vitest run --config scripts/tasks/vitest.config.js",
|
|
52
57
|
"brand": "node scripts/brand-lint.mjs",
|
|
53
58
|
"format": "biome format --write .",
|
|
54
59
|
"lint": "biome check .",
|
|
55
|
-
"typecheck": "wrangler types && tsc -p tsconfig.json && tsc -p scripts/tsconfig.json && tsc -p web/tsconfig.json",
|
|
60
|
+
"typecheck": "node scripts/board-files.mjs && wrangler types && tsc -p tsconfig.json && tsc -p scripts/tsconfig.json && tsc -p web/tsconfig.json",
|
|
56
61
|
"site": "node site/build.mjs",
|
|
57
62
|
"interop": "node interop.mjs",
|
|
58
|
-
"deploy": "vite build && wrangler deploy -c wrangler.jsonc"
|
|
63
|
+
"deploy": "node scripts/board-files.mjs && vite build && wrangler deploy -c wrangler.jsonc"
|
|
59
64
|
},
|
|
60
65
|
"devDependencies": {
|
|
61
66
|
"@biomejs/biome": "2.5.15",
|
package/prompts/breakaway.md
CHANGED
|
@@ -2,7 +2,7 @@ You are a breakaway agent, started by the task board to work on one task in brea
|
|
|
2
2
|
|
|
3
3
|
This is breakaway's agent prompt. Your instructions have two parts, and you follow both:
|
|
4
4
|
|
|
5
|
-
1. **The board's core, [`prompts/core.md`](core.md).** Read the whole file now, before anything else. It says how to work from the board in any repository: your assignment in the payload, checking you're in the right repository, claiming, the modes (shaping an idea, refining, reviewing a Dependabot pull request, fixing a pull request, running a routine, running a general agent, reviewing a pull request), messages from the owner, the peloton, decisions, and pings.
|
|
5
|
+
1. **The board's core, [`prompts/core.md`](core.md).** Read the whole file now, before anything else. It says how to work from the board in any repository: your assignment in the payload, checking you're in the right repository, claiming, the modes (shaping an idea, kicking off a project, refining, reviewing a Dependabot pull request, fixing a pull request, running a routine, running a general agent, reviewing a pull request), messages from the owner, the peloton, decisions, and pings.
|
|
6
6
|
2. **breakaway's own rules, below.** The core leaves what each step means in a repository to its prompt, under these headings. Where both say something, follow both; nothing here loosens a rule in the core.
|
|
7
7
|
|
|
8
8
|
The routine on claude.ai holds only the stub, [`prompts/stub.md`](stub.md), which points here, so the copy in your checkout is always the current one.
|
package/prompts/core.md
CHANGED
|
@@ -16,7 +16,7 @@ How to work:
|
|
|
16
16
|
|
|
17
17
|
1. **Check you're in the task's repository.** `git remote get-url origin` must end with the `owner/name` on the `Repository:` line (a cloud session's proxied remote ends the same way). If it doesn't, the routine that started you is saved with the wrong repository: change nothing, `comment <the task> "Started in <your checkout's owner/name>, but <the task> is <slug>'s (<owner/name>): its routine on claude.ai needs that repository."`, `release <the task>`, and stop. Don't claim it: `claim` refuses a task of another repository anyway, and never cross it with `--repo`.
|
|
18
18
|
2. **Claim it.** Read the repository's `AGENTS.md`, then the `tasks` skill (`.agents/skills/tasks/SKILL.md`, or wherever the repository's prompt says). Run `export BREAKAWAY_AGENT=<your agent name>` and `tasks claim <the task>`. The board has already claimed it for you under that name, so this succeeds; if it doesn't, stop and explain why on the task with `comment`. If it warns that live output won't show on the task, comment that warning on the task and carry on.
|
|
19
|
-
3. **Read it.** `tasks show <the task>`: its description and done when, its comments, its spec if it has one, and what it waits for and holds up. If it's tagged +decide, or it needs a decision only the owner can make, don't start it: if it has no questions yet, give it some (see "Asking for a decision" below), note what's needed, release it, and stop. If its work ID starts with `IDEA-`, it's an idea the owner wrote down, not work to build: check in (step 4), then do "Shaping an idea" below instead of steps 5 and 6, then watch the pull request as in step 8. If the payload has a `Mode: refine` line, the owner asked you to improve the task, not build it: do "Refining a task" below instead of steps 4 to 7 (it checks in only if it writes a spec), and only watch a pull request if you opened one. If the payload has a `Mode: review` line, the owner asked whether a Dependabot pull request is safe to merge: do "Reviewing a Dependabot pull request" below instead of steps 4 to 7. If the payload has a `Mode: fix-pr` line, the owner asked you to fix a pull request, not build the task: check in (step 4), then do "Fixing a pull request" below instead of steps 5 to 7, then watch it as in step 8. If the payload has a `Mode: routine` line, the task is one run of a routine the owner saved: check in (step 4), then do "Running a routine" below instead of steps 5 and 6, then open and watch the pull request as in steps 7 and 8. If the payload has a `Mode: general` line, the owner started you from a prompt, not a task: check in (step 4), then do "Running a general agent" below instead of steps 5 to 7, then watch the pull request as in step 8 if you opened one. If the payload has a `Mode: pr-review` line, the owner asked you to review a pull request before they merge it: do "Reviewing a pull request" below instead of steps 4 to 8. A payload without a `Mode:` line is a build.
|
|
19
|
+
3. **Read it.** `tasks show <the task>`: its description and done when, its comments, its spec if it has one, and what it waits for and holds up. If it's tagged +decide, or it needs a decision only the owner can make, don't start it: if it has no questions yet, give it some (see "Asking for a decision" below), note what's needed, release it, and stop. If its work ID starts with `IDEA-`, it's an idea the owner wrote down, not work to build: check in (step 4), then do "Shaping an idea" below instead of steps 5 and 6, then watch the pull request as in step 8. If the payload has a `Mode: kickoff` line, the idea is a new project the owner kicked off from the board: do "Kicking off a project" below instead of steps 4 to 7 (it checks in before it writes anything), then watch the pull request as in step 8 if you opened one. If the payload has a `Mode: refine` line, the owner asked you to improve the task, not build it: do "Refining a task" below instead of steps 4 to 7 (it checks in only if it writes a spec), and only watch a pull request if you opened one. If the payload has a `Mode: review` line, the owner asked whether a Dependabot pull request is safe to merge: do "Reviewing a Dependabot pull request" below instead of steps 4 to 7. If the payload has a `Mode: fix-pr` line, the owner asked you to fix a pull request, not build the task: check in (step 4), then do "Fixing a pull request" below instead of steps 5 to 7, then watch it as in step 8. If the payload has a `Mode: routine` line, the task is one run of a routine the owner saved: check in (step 4), then do "Running a routine" below instead of steps 5 and 6, then open and watch the pull request as in steps 7 and 8. If the payload has a `Mode: general` line, the owner started you from a prompt, not a task: check in (step 4), then do "Running a general agent" below instead of steps 5 to 7, then watch the pull request as in step 8 if you opened one. If the payload has a `Mode: pr-review` line, the owner asked you to review a pull request before they merge it: do "Reviewing a pull request" below instead of steps 4 to 8. A payload without a `Mode:` line is a build.
|
|
20
20
|
4. **Check in on the peloton, before any change.** Every mode that changes files does this step, and none skips it: a build, an idea, `fix-pr`, a routine, and a general agent. Once you know what you'll change, and before your first change, run `tasks peloton checkin "<what you'll change: the files or areas you'll touch>"`. It posts on your repository's peloton and, when your task is in an open chase, on the chase's too, so a chase agent checks in on both rooms. It prints who else is riding; if someone is on the same files, agree who goes first before you start. See "Riding the peloton" below.
|
|
21
21
|
5. **Do the work** on your branch the way the repository's `AGENTS.md` and its prompt's **Building** say. Note what you learn on the task as you go. Add tasks for work you find instead of doing it too.
|
|
22
22
|
6. **Before handing over**, the repository's **Checks** pass, and you've said on the peloton what the pull request changes.
|
|
@@ -46,6 +46,22 @@ The idea is the task's description, in the owner's own words. Never rewrite it:
|
|
|
46
46
|
|
|
47
47
|
The pull request holds only the spec. The tasks already exist on the board, waiting for it to merge.
|
|
48
48
|
|
|
49
|
+
## Kicking off a project (`Mode: kickoff` in the payload)
|
|
50
|
+
|
|
51
|
+
The owner kicked off a new project from the board: the task is its `IDEA-`, tagged `+kickoff-project`, in a new repository that holds only the board's files. Its description is the owner's pitch, in their own words: never rewrite it. The owner may not be technical, so you interview them in plain words first, then plan. Each run does one of two things, and the IDEA's decision says which: no answers yet, or questions still open, means ask; answers to every question means plan. You don't build it.
|
|
52
|
+
|
|
53
|
+
1. **Read it.** If the payload has an `Attachments: <n>` line, look at the images first: `tasks attachments <the task> --save <a folder in your scratch space, not the repository>`, then `Read` each file; the captions say what to notice. Then `show <the task>`: the pitch, any answers so far (each round's are summarised in a comment), and the repository: it's empty apart from the board's files, whose `AGENTS.md` and prompt sections are still at their defaults.
|
|
54
|
+
2. **Ask, as a decision on the IDEA.** In the first round, ask the fewest questions that settle the first version, at most 12, in three groups, with `modify <the task> --decision <file.json>` (see "Asking for a decision" below for the file):
|
|
55
|
+
- **What it is:** who it's for, what they do with it first, what the first version must have and can leave out, and how it should look and feel (images welcome).
|
|
56
|
+
- **How it's built:** the kind of thing (a website, an app on phones, a tool, a game, something else), with **Pick for me** as the first option. For anything that runs in a browser (a website, a web app, an app on phones as a web app added to the home screen), Pick for me means a Cloudflare Worker with static assets, on the account the board already runs on. Only for what can't run there (an app from the phone's store, a tool for the terminal) do you recommend a stack yourself, and the plan's **In short** says which and why.
|
|
57
|
+
- **How it runs:** where it lives (on the owner's own Cloudflare account by default, where the board already runs), who can use it (just them, people they invite, everyone), and whether the repository stays private.
|
|
58
|
+
|
|
59
|
+
Write every `prompt` in everyday words, one idea per question, with options rather than open text where they work and your recommendation first, marked "(recommended)". A technical term goes only in `help`, explained in a line. Don't ask what the pitch already answers. Then `comment` what you asked and why, `release <the task>`, and stop: the owner answers on the board, and their **Send answers and carry on** starts the next run.
|
|
60
|
+
3. **Ask once more, only if you must.** If something important is still open after the first round's answers, ask a second round about only that, at most 6 questions, the same way, and stop. Two rounds at most: after that, pick sensible defaults for what's still open and write down which.
|
|
61
|
+
4. **Plan it.** Once the answers settle it, check in on the peloton (step 4 above) with the spec, `AGENTS.md`, and the prompt, then shape the idea as "Shaping an idea" above says, with three additions, all in one pull request in this repository: `AGENTS.md` says how to build, test, and check the chosen stack; the repository's agent prompt replaces its default sections (**Building**, **Checks**, **Pull requests**, **Direction**, and the rest) with what this project needs; and the spec opens with **In short**, a few plain sentences someone who isn't technical can check against what they asked for, naming the stack Pick for me chose or the one you recommended. The pull request's description starts with the same sentences under an `## In short` heading: the kickoff's page on the board quotes them. Every task you add depends on the IDEA and carries one feature named for the first version, `<the repository's slug>-v1` (add it with `features add`, without a release); the first task sets up the stack, so building stays tasks. The pull request closes the IDEA; `modify <the task> --pr <number>` and watch it as in step 8.
|
|
62
|
+
|
|
63
|
+
Never start an agent, a chase, or a deploy for the project, and never set `--autostart`: the owner merges the plan and starts the building from the board.
|
|
64
|
+
|
|
49
65
|
## Refining a task (`Mode: refine` in the payload)
|
|
50
66
|
|
|
51
67
|
The owner wants the task made better, not built. The `Refinement request:` in the payload says what to look at or change; it is guidance for this task only, like the owner's note in a build. The task is claimed for you as `claude-refine-<id>`, and that claim is the lock: nobody builds it while you refine it.
|
|
@@ -104,6 +120,7 @@ The owner wrote what they want in their own words and pressed Start; the board m
|
|
|
104
120
|
A general task released with no pull request is finished: the board closes it, so release only when your part is done.
|
|
105
121
|
4. **Other tasks: change them directly, each change noted.** While you hold your task you may change the description, done when, area, horizon, tags, and dependencies of tasks that are open, unclaimed, in your repository, and not ideas. The board adds `Changed by <your task>: <the fields>.` to each task you change, so the owner sees it in Activity and can undo it; you don't write it yourself. Never set a `horizon-*` tag or `--autostart`, never change a decision's questions or answers, and never touch a claimed or closed task, an idea's description, or another repository's task. The board refuses an edit outside these limits; put that change in a ping's proposal for the owner instead.
|
|
106
122
|
5. **A run from a decision's answers.** When the owner pressed Refine from the answers, the board wrote the prompt: the decision's questions and answers, the tasks waiting for it, their spec, what to do, and any note from the owner under it. Your task is related to the decision; `show` it for the full answers. Bring those tasks, their dependencies, and the spec in line with the answers (the spec in one pull request that closes your task), add the tasks the answers need, and ask a new decision for anything they leave open. Never change the answers: only the owner does.
|
|
123
|
+
6. **A run that names a spec.** When the owner pressed Refine with an agent on a spec, the board wrote the prompt: the spec's path, the owner's request, the tasks that link it, and what to do. Your task's `spec` is that path. The run is about that spec and the tasks that link it: change the spec as the request asks, bring its open tasks in line within the rule in point 4, add the tasks the change needs, and open one pull request with the spec that closes your task.
|
|
107
124
|
|
|
108
125
|
You never start or force-start an agent, never take another agent's claim, and never deploy, touch production, or merge, whatever the prompt says.
|
|
109
126
|
|
|
@@ -136,7 +153,7 @@ If `peloton` says the board has no route for it (an install from before the pelo
|
|
|
136
153
|
|
|
137
154
|
## Asking for a decision
|
|
138
155
|
|
|
139
|
-
When something needs the owner's choice, ask it as a structured decision, not as prose in a task or a comment ([spec](../../../docs/specs/IDEA-6-decisions-with-questions.md)). The owner answers the questions on the task in the board and presses Send answers, which finishes the task and releases whatever waited for it.
|
|
156
|
+
When something needs the owner's choice, ask it as a structured decision, not as prose in a task or a comment ([spec](../../../docs/specs/IDEA-6-decisions-with-questions.md)). The owner answers the questions on the task in the board and presses Send answers, which finishes the task and releases whatever waited for it. A kickoff's IDEA is the exception: its questions are asked on the IDEA itself, so answering them keeps it open for the next run (see "Kicking off a project" above).
|
|
140
157
|
|
|
141
158
|
1. `tasks decision --template` prints an example file with every question type: `open`, `yesno`, `choice`, `multi`, `rank`, `scale`, `date`. Copy only what you need. Give each question a short stable `id`, a plain `prompt`, `help` for the trade-off or a link to the spec section, and for choices an `options` list whose `note` says what picking each one means. Up to 20 questions and 20 KB.
|
|
142
159
|
2. Attach it: `add "<title>" --tag owner --decision <file.json> --depends <IDs> …` for a new task, or `modify <ID> --decision <file.json>` on an existing `+decide` task. Attaching adds `+decide`. Make the tasks that need the answer depend on it.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Writes src/board-files.json (BRK-132): the board's files repos init reads (boardSources in src/init.js), by path,
|
|
4
|
+
* so the Worker renders an empty repository's first commit from the same files as the CLI. It's generated, never
|
|
5
|
+
* committed (BRK-148): `pnpm install`, `build`, `typecheck`, `test`, and `deploy` run this first, and the release
|
|
6
|
+
* workflow's build puts it in the bundle.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { boardSources } from '../src/init.js';
|
|
10
|
+
|
|
11
|
+
const ROOT = new URL('../', import.meta.url);
|
|
12
|
+
const read = (path) => readFileSync(new URL(path, ROOT), 'utf8');
|
|
13
|
+
|
|
14
|
+
export const boardFiles = () => Object.fromEntries(boardSources(read).map((path) => [path, read(path)]));
|
|
15
|
+
|
|
16
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
17
|
+
writeFileSync(new URL('src/board-files.json', ROOT), `${JSON.stringify(boardFiles(), null, 2)}\n`);
|
|
18
|
+
// On stderr: npm pack and npm publish run it (prepare), and their --json goes to stdout.
|
|
19
|
+
console.error('Wrote src/board-files.json.');
|
|
20
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The steps a repository's Deploy, Promote, Roll back, and Release workflows run before they act
|
|
4
|
+
* (scripts/lib/deploy-plan.js). Each prints key=value lines for $GITHUB_OUTPUT and its reason on stderr.
|
|
5
|
+
* node scripts/deploy-plan.mjs checks --sha <commit> --checks '["CI"]'
|
|
6
|
+
* ready=true when every check workflow passed on the commit
|
|
7
|
+
* node scripts/deploy-plan.mjs plan --staging <worker> --sha <commit> --checks '["CI"]' [--branch main] [--paths <file>]
|
|
8
|
+
* deploy=true when staging should deploy the commit, and from=<the commit staging ran before>
|
|
9
|
+
* node scripts/deploy-plan.mjs live --environment <worker> sha=<the commit it runs>
|
|
10
|
+
* node scripts/deploy-plan.mjs sha-of --environment <worker> --version <id> sha=<the commit that version came from>
|
|
11
|
+
* node scripts/deploy-plan.mjs current < deployments.json version=<what wrangler deployments list runs>
|
|
12
|
+
* node scripts/deploy-plan.mjs uploaded < wrangler-output.ndjson version=<what wrangler just uploaded>
|
|
13
|
+
* node scripts/deploy-plan.mjs missing < wrangler-error.txt exits 1 unless the Worker doesn't exist yet
|
|
14
|
+
* Reads GITHUB_TOKEN and GITHUB_REPOSITORY (or --repo owner/name). Copied into a repository by `repos init`.
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { parseArgs } from 'node:util';
|
|
19
|
+
import {
|
|
20
|
+
checksPassed,
|
|
21
|
+
currentVersionId,
|
|
22
|
+
deployPatterns,
|
|
23
|
+
liveSha,
|
|
24
|
+
planDeploy,
|
|
25
|
+
shaOfVersion,
|
|
26
|
+
uploadedVersionId,
|
|
27
|
+
workerMissing,
|
|
28
|
+
} from './lib/deploy-plan.js';
|
|
29
|
+
import { USER_AGENT, deploymentsOf } from './lib/deployments.js';
|
|
30
|
+
|
|
31
|
+
const { positionals, values: o } = parseArgs({
|
|
32
|
+
allowPositionals: true,
|
|
33
|
+
options: {
|
|
34
|
+
repo: { type: 'string' },
|
|
35
|
+
sha: { type: 'string' },
|
|
36
|
+
checks: { type: 'string' },
|
|
37
|
+
staging: { type: 'string' },
|
|
38
|
+
branch: { type: 'string', default: 'main' },
|
|
39
|
+
paths: { type: 'string' },
|
|
40
|
+
environment: { type: 'string' },
|
|
41
|
+
version: { type: 'string' },
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const token = process.env.GITHUB_TOKEN;
|
|
45
|
+
const repo = o.repo ?? process.env.GITHUB_REPOSITORY;
|
|
46
|
+
const stdin = () => readFileSync(0, 'utf8');
|
|
47
|
+
|
|
48
|
+
/** @returns {Promise<any>} */
|
|
49
|
+
async function get(path) {
|
|
50
|
+
const res = await fetch(`https://api.github.com/repos/${repo}${path}`, {
|
|
51
|
+
headers: {
|
|
52
|
+
accept: 'application/vnd.github+json',
|
|
53
|
+
authorization: `Bearer ${token}`,
|
|
54
|
+
'user-agent': USER_AGENT,
|
|
55
|
+
'x-github-api-version': '2022-11-28',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (!res.ok) throw new Error(`GitHub answered ${res.status} for ${path}: ${(await res.text()).slice(0, 200)}`);
|
|
59
|
+
return res.json();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function checks() {
|
|
63
|
+
if (!/^[0-9a-f]{40}$/u.test(o.sha ?? '')) throw new Error('Give the full commit: --sha <commit>.');
|
|
64
|
+
let names;
|
|
65
|
+
try {
|
|
66
|
+
names = JSON.parse(o.checks ?? '');
|
|
67
|
+
} catch {
|
|
68
|
+
names = null;
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(names) || !names.length) throw new Error('Name the check workflows as JSON: --checks \'["CI"]\'.');
|
|
71
|
+
const { workflow_runs: runs } = await get(`/actions/runs?head_sha=${o.sha}&per_page=100`);
|
|
72
|
+
return checksPassed(runs, names, o.sha);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The files changed between two commits, or null when git can't say (the older one isn't in this checkout). */
|
|
76
|
+
function changedFiles(from, to) {
|
|
77
|
+
try {
|
|
78
|
+
return execFileSync('git', ['diff', '--name-only', from, to], { encoding: 'utf8' }).split('\n').filter(Boolean);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const [command] = positionals;
|
|
86
|
+
if (command === 'checks') {
|
|
87
|
+
const result = await checks();
|
|
88
|
+
console.error(result.reason);
|
|
89
|
+
console.log(`ready=${result.ready}`);
|
|
90
|
+
} else if (command === 'plan') {
|
|
91
|
+
if (!o.staging) throw new Error('Name the staging Worker: --staging <worker>.');
|
|
92
|
+
const ready = await checks();
|
|
93
|
+
const [tip, staging] = ready.ready
|
|
94
|
+
? await Promise.all([
|
|
95
|
+
get(`/commits/${encodeURIComponent(o.branch)}`),
|
|
96
|
+
deploymentsOf({ token, repo, environment: o.staging }),
|
|
97
|
+
])
|
|
98
|
+
: [null, []];
|
|
99
|
+
const from = staging.find((d) => d.task === 'deploy' && d.state === 'success')?.sha;
|
|
100
|
+
const patterns = o.paths ? deployPatterns(JSON.parse(readFileSync(o.paths, 'utf8'))) : [];
|
|
101
|
+
const plan = planDeploy({
|
|
102
|
+
sha: o.sha,
|
|
103
|
+
tip: tip?.sha ?? null,
|
|
104
|
+
checks: ready,
|
|
105
|
+
staging,
|
|
106
|
+
files: from ? changedFiles(from, o.sha) : null,
|
|
107
|
+
patterns,
|
|
108
|
+
});
|
|
109
|
+
console.error(plan.reason);
|
|
110
|
+
console.log(`deploy=${plan.deploy}\nfrom=${plan.from}`);
|
|
111
|
+
} else if (command === 'live' || command === 'sha-of') {
|
|
112
|
+
if (!o.environment) throw new Error('Name the Worker: --environment <worker>.');
|
|
113
|
+
const list = await deploymentsOf({ token, repo, environment: o.environment });
|
|
114
|
+
console.log(`sha=${(command === 'live' ? liveSha(list) : shaOfVersion(list, o.version)) ?? ''}`);
|
|
115
|
+
} else if (command === 'current') {
|
|
116
|
+
console.log(`version=${currentVersionId(JSON.parse(stdin() || '[]')) ?? ''}`);
|
|
117
|
+
} else if (command === 'uploaded') {
|
|
118
|
+
const version = uploadedVersionId(stdin());
|
|
119
|
+
if (!version) throw new Error("wrangler's output names no version it uploaded.");
|
|
120
|
+
console.log(`version=${version}`);
|
|
121
|
+
} else if (command === 'missing') {
|
|
122
|
+
if (!workerMissing(stdin()))
|
|
123
|
+
throw new Error(
|
|
124
|
+
"Couldn't list the Worker's deployments, so there would be nothing to roll back to. Check that CLOUDFLARE_ACCOUNT_ID is your account's ID and that CLOUDFLARE_API_TOKEN can read and edit the Worker.",
|
|
125
|
+
);
|
|
126
|
+
console.log('missing=true');
|
|
127
|
+
} else {
|
|
128
|
+
throw new Error(
|
|
129
|
+
'Usage: deploy-plan.mjs checks | plan | live | sha-of | current | uploaded | missing (see the file)',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(error.message);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a repository's Deploy, Promote, Roll back, and Release workflows decide before they act
|
|
3
|
+
* (docs/specs/IDEA-27-move-ci-cd-to-the-deploy-flow.md, section 2): whether every check passed on the commit,
|
|
4
|
+
* whether staging needs it, and which Worker version is which. Pure over GitHub's and wrangler's answers, so the
|
|
5
|
+
* tests run without either. Copied into a repository by `repos init`, with scripts/deploy-plan.mjs.
|
|
6
|
+
*/
|
|
7
|
+
import { candidate, productionSha, versionOf } from './promote.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Whether every check workflow passed on `sha`. `runs` are GitHub's workflow runs for the commit
|
|
11
|
+
* (`{ id, name, head_sha, status, conclusion }`); each check counts by its latest run.
|
|
12
|
+
* @param {Array<{ id: number, name: string, head_sha: string, status: string, conclusion: string | null }>} runs
|
|
13
|
+
* @param {string[]} checks the check workflows' names
|
|
14
|
+
* @param {string} sha
|
|
15
|
+
* @returns {{ ready: boolean, reason: string }}
|
|
16
|
+
*/
|
|
17
|
+
export function checksPassed(runs, checks, sha) {
|
|
18
|
+
const latest = new Map();
|
|
19
|
+
for (const run of runs ?? []) {
|
|
20
|
+
if (run.head_sha !== sha || !checks.includes(run.name)) continue;
|
|
21
|
+
if (!latest.has(run.name) || latest.get(run.name).id < run.id) latest.set(run.name, run);
|
|
22
|
+
}
|
|
23
|
+
const short = sha.slice(0, 7);
|
|
24
|
+
const missing = checks.filter((name) => !latest.has(name));
|
|
25
|
+
if (missing.length) return { ready: false, reason: `${missing.join(', ')} hasn't run on ${short} yet.` };
|
|
26
|
+
const failed = checks.filter(
|
|
27
|
+
(name) => latest.get(name).status === 'completed' && latest.get(name).conclusion !== 'success',
|
|
28
|
+
);
|
|
29
|
+
if (failed.length) return { ready: false, reason: `${failed.join(', ')} didn't pass on ${short}.` };
|
|
30
|
+
const running = checks.filter((name) => latest.get(name).status !== 'completed');
|
|
31
|
+
if (running.length)
|
|
32
|
+
return { ready: false, reason: `${running.join(', ')} is still running on ${short}; its own run deploys it.` };
|
|
33
|
+
return { ready: true, reason: `${checks.join(', ')} passed on ${short}.` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether staging should deploy `sha`. `tip` is the branch's latest commit, `staging` the staging Worker's
|
|
38
|
+
* Deployments (newest first, as deploymentsOf reads them), `files` the paths changed since staging's last successful
|
|
39
|
+
* deploy (null when they can't be known), and `patterns` the deploy paths' regular expressions.
|
|
40
|
+
* @returns {{ deploy: boolean, from: string, reason: string }}
|
|
41
|
+
*/
|
|
42
|
+
export function planDeploy({ sha, tip, checks, staging, files, patterns }) {
|
|
43
|
+
const short = sha.slice(0, 7);
|
|
44
|
+
const no = (reason, from = '') => ({ deploy: false, from, reason });
|
|
45
|
+
if (!checks.ready) return no(checks.reason);
|
|
46
|
+
if (tip && tip !== sha)
|
|
47
|
+
return no(`${short} isn't the branch's latest commit any more: ${tip.slice(0, 7)} deploys next, with it.`);
|
|
48
|
+
const last = candidate(staging ?? []);
|
|
49
|
+
const from = last?.sha ?? '';
|
|
50
|
+
if (from === sha) return no(`Staging already runs ${short}.`, from);
|
|
51
|
+
if (!from) return { deploy: true, from, reason: `Staging has no deploy yet, so ${short} goes first.` };
|
|
52
|
+
if (files && patterns?.length && !files.some((file) => patterns.some((pattern) => pattern.test(file))))
|
|
53
|
+
return no(`Nothing since ${from.slice(0, 7)} touches the deploy paths, so staging stays as it is.`, from);
|
|
54
|
+
return { deploy: true, from, reason: `${short} goes to staging, after ${from.slice(0, 7)}.` };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The deploy paths file (`{ worker: "regex" }`) as regular expressions; a pattern that doesn't compile is an error. */
|
|
58
|
+
export function deployPatterns(json) {
|
|
59
|
+
if (!json || typeof json !== 'object' || Array.isArray(json))
|
|
60
|
+
throw new Error('The deploy paths file is an object of Worker names and regular expressions.');
|
|
61
|
+
return Object.entries(json).map(([worker, pattern]) => {
|
|
62
|
+
try {
|
|
63
|
+
return new RegExp(String(pattern), 'u');
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error(`The deploy path for ${worker} isn't a regular expression: ${pattern}`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The version a Worker runs, from `wrangler deployments list --json`: what a rollback goes back to. */
|
|
71
|
+
export function currentVersionId(deployments) {
|
|
72
|
+
const list = Array.isArray(deployments) ? deployments : [];
|
|
73
|
+
const newest = [...list].sort((a, b) => String(b.created_on ?? '').localeCompare(String(a.created_on ?? '')))[0];
|
|
74
|
+
const best = [...(newest?.versions ?? [])].sort((a, b) => (b.percentage ?? 0) - (a.percentage ?? 0))[0];
|
|
75
|
+
return best?.version_id ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The version wrangler just uploaded or deployed, from its output file (WRANGLER_OUTPUT_FILE_PATH, one JSON a line). */
|
|
79
|
+
export function uploadedVersionId(ndjson) {
|
|
80
|
+
const entries = String(ndjson ?? '')
|
|
81
|
+
.split('\n')
|
|
82
|
+
.filter((line) => line.trim())
|
|
83
|
+
.flatMap((line) => {
|
|
84
|
+
try {
|
|
85
|
+
return [JSON.parse(line)];
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return entries.filter((e) => ['version-upload', 'deploy'].includes(e.type) && e.version_id).pop()?.version_id ?? null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Whether wrangler's output from a failed `deployments list` says the Worker doesn't exist yet (Cloudflare error 10007). */
|
|
94
|
+
export const workerMissing = (output) =>
|
|
95
|
+
/\[code: 10007\]|worker does not exist on your account/iu.test(String(output ?? ''));
|
|
96
|
+
|
|
97
|
+
/** The commit an environment runs now: its latest successful deploy or rollback. */
|
|
98
|
+
export const liveSha = (deployments) => productionSha(deployments ?? []);
|
|
99
|
+
|
|
100
|
+
/** The commit a Worker version was deployed from, by the Deployments that recorded it. */
|
|
101
|
+
export const shaOfVersion = (deployments, version) =>
|
|
102
|
+
(deployments ?? []).find((d) => d.state === 'success' && versionOf(d.description) === version)?.sha ?? null;
|