breakaway 1.4.0-main.26 → 1.4.0-main.28

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.26",
3
+ "version": "1.4.0-main.28",
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",
@@ -56,6 +56,25 @@ export function nextPrerelease(current, tags, prefix = 'v') {
56
56
  return { version, base, tag: `${prefix}${version}` };
57
57
  }
58
58
 
59
+ /**
60
+ * The version a stable release's pull request sets package.json to (BRK-118, WEB-39): the next minor or major after
61
+ * the stable, or null when there is nothing to set. A patch is null, since the pre-releases count patches by
62
+ * themselves, and so is a package.json already at or past the choice (main moved on before an older pre-release was
63
+ * released).
64
+ * @param {string} stable the version just released, like 1.3.0
65
+ * @param {string} next patch, minor, or major
66
+ * @param {string} current package.json's version on the default branch
67
+ * @returns {string | null}
68
+ */
69
+ export function nextVersion(stable, next, current) {
70
+ const [maj, min] = parts(stable);
71
+ parts(current);
72
+ if (next === 'patch') return null;
73
+ if (next !== 'minor' && next !== 'major') throw new Error(`next is patch, minor, or major, not "${next}".`);
74
+ const version = next === 'major' ? `${maj + 1}.0.0` : `${maj}.${min + 1}.0`;
75
+ return compareVersions(current, version) >= 0 ? null : version;
76
+ }
77
+
59
78
  /** The pre-release among `tags` (the tags on one commit), if one was already staged from it. */
60
79
  export function prereleaseAmong(tags, prefix = 'v') {
61
80
  const prerelease = prereleasePattern(prefix);
@@ -7,13 +7,15 @@
7
7
  * exists=true when that commit already has a pre-release (a second run for one merge stages nothing)
8
8
  * node scripts/package-release.mjs stable <pre-release tag> --prefix <tag prefix>
9
9
  * the stable version and tag it becomes, and the commit it was staged from; stops if that stable exists
10
+ * node scripts/package-release.mjs next <stable> <patch|minor|major> --dir <path>
11
+ * the version to set <path>/package.json to after the stable, empty for none (WEB-39)
10
12
  * Reads the tags with git, so run it in a checkout with its tags (fetch-depth: 0). Copied by `repos init`.
11
13
  */
12
14
  import { execFileSync } from 'node:child_process';
13
15
  import { readFileSync } from 'node:fs';
14
16
  import { join } from 'node:path';
15
17
  import { parseArgs } from 'node:util';
16
- import { nextPrerelease, prereleaseAmong, stableOf } from './lib/package-release.js';
18
+ import { nextPrerelease, nextVersion, prereleaseAmong, stableOf } from './lib/package-release.js';
17
19
 
18
20
  const { positionals, values: o } = parseArgs({
19
21
  allowPositionals: true,
@@ -41,9 +43,15 @@ try {
41
43
  throw new Error(`${o.prefix}${version} is already released. Pick a newer pre-release.`);
42
44
  const [commit] = git('rev-list', '-n', '1', `refs/tags/${tag}`);
43
45
  console.log(`version=${version}\ntag=${o.prefix}${version}\ncommit=${commit}`);
46
+ } else if (command === 'next' && tag) {
47
+ const current = JSON.parse(readFileSync(join(o.dir, 'package.json'), 'utf8')).version;
48
+ const version = nextVersion(tag, positionals[2] ?? 'patch', current);
49
+ if (!version && positionals[2] && positionals[2] !== 'patch')
50
+ console.error(`package.json already says ${current}, at or past the next ${positionals[2]} after ${tag}.`);
51
+ console.log(`version=${version ?? ''}`);
44
52
  } else {
45
53
  throw new Error(
46
- 'Usage: package-release.mjs prerelease --dir <path> --prefix <p> [--sha <commit>] | stable <tag> --prefix <p>',
54
+ 'Usage: package-release.mjs prerelease --dir <path> --prefix <p> [--sha <commit>] | stable <tag> --prefix <p> | next <stable> <patch|minor|major> --dir <path>',
47
55
  );
48
56
  }
49
57
  } catch (error) {
@@ -106,18 +106,27 @@ export function pullAgentRequest(action, number, { repo = null, problem, note, f
106
106
  }
107
107
 
108
108
  /**
109
- * `npx breakaway github release <pre-release>` (BRK-103): the owner releases a package's pre-release as its stable, as
110
- * Release on the GitHub page does. The board starts the repository's release.yml stable job, and npm waits for the
111
- * owner's 2FA; it refuses an agent, so the request always says who asks.
109
+ * `npx breakaway github release <pre-release> [--next patch|minor|major]` (BRK-103, WEB-39): the owner releases a
110
+ * package's pre-release as its stable, as Release on the GitHub page does, with what the default branch works toward
111
+ * next. The board starts the repository's release.yml stable job, and npm waits for the owner's 2FA; it refuses an
112
+ * agent, so the request always says who asks, and refuses a pre-release whose stable is already out (409).
112
113
  * @param {string | undefined} version the pre-release, like 1.4.0-main.5 (or its tag)
113
- * @param {{ repo?: string | null, by?: string }} [options]
114
+ * @param {{ repo?: string | null, by?: string, next?: string | null }} [options]
114
115
  * @returns {{ error?: string, request?: [string, string, Record<string, string>] }}
115
116
  */
116
- export function packageReleaseRequest(version, { repo = null, by } = {}) {
117
+ export function packageReleaseRequest(version, { repo = null, by, next = null } = {}) {
117
118
  const v = String(version ?? '').trim();
118
119
  if (!/^(?:\S+@|v)?\d+\.\d+\.\d+-main\.\d+$/u.test(v))
119
120
  return { error: 'say which pre-release: npx breakaway github release <version>, like 1.4.0-main.5' };
120
- return { request: ['POST', 'github/release', { version: v, ...(repo ? { repo } : {}), ...(by ? { by } : {}) }] };
121
+ if (next !== null && next !== undefined && !['patch', 'minor', 'major'].includes(String(next)))
122
+ return { error: '--next is patch, minor, or major' };
123
+ return {
124
+ request: [
125
+ 'POST',
126
+ 'github/release',
127
+ { version: v, ...(next ? { next: String(next) } : {}), ...(repo ? { repo } : {}), ...(by ? { by } : {}) },
128
+ ],
129
+ };
121
130
  }
122
131
 
123
132
  export const REVIEW_VERDICTS = ['ready', 'follow-up', 'changes'];
@@ -576,7 +576,10 @@ export function lintWorkflow(text) {
576
576
  if (!(need in doc.jobs)) problems.push(`${where} needs ${need}, which isn't a job`);
577
577
  const { steps = [], ...rest } = job;
578
578
  checkExpressions(stringsIn({ if: rest.if }), where, [], true);
579
- checkExpressions(stringsIn({ ...rest, if: undefined }), where, [], false);
579
+ checkExpressions(stringsIn({ ...rest, if: undefined, outputs: undefined }), where, [], false);
580
+ // A job's outputs read its steps once they've all run.
581
+ const stepIds = Array.isArray(steps) ? steps.map((s) => s?.id).filter(Boolean) : [];
582
+ checkExpressions(stringsIn({ outputs: rest.outputs }), `${where}'s outputs`, stepIds, false);
580
583
  if (stringsIn(job).some(({ text: value }) => /\bsecrets\./u.test(value)) && !job.environment)
581
584
  problems.push(`${where} reads a secret outside an environment`);
582
585
  if (!Array.isArray(steps) || !steps.length) {
package/scripts/tasks.mjs CHANGED
@@ -181,7 +181,9 @@ Reading (list, next, claim, and add work in this checkout's repos
181
181
  Review with an agent does; on a Dependabot one it tests the update, as Safe to merge? does
182
182
  (owner) [--note <text>] [--repo <slug>] [--force]
183
183
  github release <pre-release> release a package's pre-release (1.4.0-main.5) as its stable on latest, as Release on the
184
- GitHub page does: the board starts release.yml's stable job, and npm waits for your 2FA (owner) [--repo <slug>]
184
+ GitHub page does: the board starts release.yml's stable job, and npm waits for your 2FA (owner);
185
+ --next minor or major opens the pull request that sets the next version (patch, the default, counts
186
+ by itself). Refused once that stable is out [--next patch|minor|major] [--repo <slug>]
185
187
  specs the repository's specs, newest first: each one's status and its tasks [--repo <slug>]
186
188
  specs show <path> one spec: its status, last change, Markdown, and the tasks that link it [--repo <slug>]
187
189
  github the checkout's repository on GitHub: open pull requests, checks, reviews, CI, deploys, alerts [--sync] [--repo <slug>]
@@ -1441,13 +1443,15 @@ const commands = {
1441
1443
  const built = packageReleaseRequest(args[1], {
1442
1444
  repo: (await checkoutRepo()).slug,
1443
1445
  by: opts.as ?? setting('AGENT'),
1446
+ next: typeof opts.next === 'string' ? opts.next : null,
1444
1447
  });
1445
1448
  if (built.error || !built.request) fail(built.error ?? 'bad request');
1446
1449
  const answer = await call(...built.request);
1450
+ const next = built.request[2].next;
1447
1451
  print(
1448
1452
  answer,
1449
1453
  () =>
1450
- `Started ${answer.workflow}'s stable job for ${args[1]}. It stages the stable on npm's latest, where it waits for your approval with 2FA.`,
1454
+ `Started ${answer.workflow}'s stable job for ${args[1]}. It stages the stable on npm's latest, where it waits for your approval with 2FA.${next && next !== 'patch' ? ` Then a pull request sets package.json to the next ${next}.` : ''}`,
1451
1455
  );
1452
1456
  return;
1453
1457
  }
@@ -4,5 +4,5 @@
4
4
  * and how to update it. scripts/tasks/version.test.js fails when the copied files change and this doesn't:
5
5
  * so it lives in the board's package (CLD-135) and the CLI imports it from here.
6
6
  */
7
- export const CLI_VERSION = 71;
8
- export const CLI_FINGERPRINT = '908852b62fff3574';
7
+ export const CLI_VERSION = 72;
8
+ export const CLI_FINGERPRINT = '393b3864ca66fb71';
@@ -19,6 +19,14 @@ on:
19
19
  description: The pre-release to release as stable, like {{prefixName}}1.4.0-main.5
20
20
  required: true
21
21
  type: string
22
+ # Pre-releases count patches from the stable by themselves. With minor or major, once the stable is staged, the
23
+ # next-version job opens the pull request that sets package.json to the next minor or major after it.
24
+ next:
25
+ description: What {{branchName}} works toward next. Patch opens nothing; minor or major opens a pull request that sets package.json
26
+ required: false
27
+ default: patch
28
+ type: choice
29
+ options: [patch, minor, major]
22
30
 
23
31
  permissions: {}
24
32
 
@@ -139,6 +147,8 @@ jobs:
139
147
  permissions:
140
148
  contents: write
141
149
  id-token: write
150
+ outputs:
151
+ version: ${{ steps.version.outputs.version }}
142
152
  env:
143
153
  GITHUB_TOKEN: ${{ github.token }}
144
154
  GH_TOKEN: ${{ github.token }}
@@ -210,3 +220,64 @@ jobs:
210
220
  TAG: ${{ steps.version.outputs.tag }}
211
221
  COMMIT: ${{ steps.version.outputs.commit }}
212
222
  run: gh api -X POST "repos/$GITHUB_REPOSITORY/git/refs" -f "ref=refs/tags/$TAG" -f "sha=$COMMIT" > /dev/null
223
+
224
+ # With next minor or major, a pull request sets package.json to the next minor or major after the stable, so the next
225
+ # merge stages X.Y.0-main.1 instead of the stable's next patch. It runs after the stable is staged, in its own job with
226
+ # no environment and no secrets, and writes through GitHub's API, so no credentials are kept in a checkout. A pull
227
+ # request opened with GITHUB_TOKEN starts no workflows, so its checks run once the owner closes and reopens it, or
228
+ # pushes to it.
229
+ next-version:
230
+ name: next version
231
+ needs: stable
232
+ if: inputs.next == 'minor' || inputs.next == 'major'
233
+ runs-on: ubuntu-latest
234
+ timeout-minutes: 5
235
+ permissions:
236
+ contents: write
237
+ pull-requests: write
238
+ env:
239
+ GH_TOKEN: ${{ github.token }}
240
+ STABLE: ${{ needs.stable.outputs.version }}
241
+ NEXT: ${{ inputs.next }}
242
+ steps:
243
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
244
+ with:
245
+ ref: {{branchName}}
246
+ persist-credentials: false
247
+ - name: Open the pull request that sets the version
248
+ run: |
249
+ set -euo pipefail
250
+ version=$(node scripts/package-release.mjs next "$STABLE" "$NEXT" --dir "$DIRECTORY" | sed 's/^version=//')
251
+ if [ -z "$version" ]; then
252
+ echo "::notice title=No version to set::{{branchName}}'s package.json is already at or past the next $NEXT after $STABLE."
253
+ exit 0
254
+ fi
255
+ branch="next-version/$version"
256
+ if [ -n "$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$branch" --state open --json number --jq '.[].number')" ]; then
257
+ echo "::notice title=Already open::A pull request from $branch sets $version."
258
+ exit 0
259
+ fi
260
+ file=package.json
261
+ if [ "$DIRECTORY" != . ]; then file="$DIRECTORY/package.json"; fi
262
+ # A run GitHub refused the pull request leaves its branch: open the pull request from it.
263
+ if ! gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/$branch" >/dev/null 2>&1; then
264
+ base=$(git rev-parse HEAD)
265
+ blob=$(git rev-parse "HEAD:$file")
266
+ export V=$version F=$file
267
+ node -e "const f=process.env.F,p=JSON.parse(require('fs').readFileSync(f));p.version=process.env.V;require('fs').writeFileSync(f,JSON.stringify(p,null,2)+'\n')"
268
+ gh api -X POST "repos/$GITHUB_REPOSITORY/git/refs" -f "ref=refs/heads/$branch" -f "sha=$base" >/dev/null
269
+ gh api -X PUT "repos/$GITHUB_REPOSITORY/contents/$file" -f "branch=$branch" -f "sha=$blob" \
270
+ -f "message=Set $PACKAGE's version to $version" -f "content=$(base64 -w0 "$file")" >/dev/null
271
+ fi
272
+ printf '%s\n' \
273
+ "$PACKAGE@$STABLE is staged, and the Release workflow was run with next $NEXT: this sets $file's version to $version." \
274
+ "" \
275
+ "Once it merges, the next pre-release is $version-main.1 instead of $STABLE's next patch." \
276
+ "" \
277
+ "Opened with the workflow's token, which starts no workflows: close and reopen this pull request, or push to it, for its checks to run." \
278
+ > "$RUNNER_TEMP/body.md"
279
+ if ! gh pr create --repo "$GITHUB_REPOSITORY" --base {{branchName}} --head "$branch" \
280
+ --title "Set $PACKAGE's version to $version" --body-file "$RUNNER_TEMP/body.md"; then
281
+ echo "::error title=GitHub refused the pull request::$branch sets $version, but the repository doesn't let Actions open pull requests. Turn on Settings, Actions, General, Allow GitHub Actions to create and approve pull requests, then re-run this job, or open the pull request from $branch yourself."
282
+ exit 1
283
+ fi