diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 80c2336..e390599 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,7 +7,7 @@ { "name": "lvbt-contributions", "description": "Create readable LVBT GitHub issues and pull requests.", - "version": "0.5.0", + "version": "0.5.1", "source": "./packages/cli/plugins/lvbt-contributions" } ] diff --git a/docs/reference/release-0-5-1.md b/docs/reference/release-0-5-1.md new file mode 100644 index 0000000..dfa23ba --- /dev/null +++ b/docs/reference/release-0-5-1.md @@ -0,0 +1,15 @@ +# Repository tooling 0.5.1 + +Nothing changes in how your repository works. This patch fixes the maintainer script that gives a +repository its first `Standard update` workflow, and makes a missing `workflow_dispatch` trigger +easy to spot. + +- Rerunning `standards/propose.ts` from a checkout still on the update branch no longer closes that + branch's own pull request. An unchanged run now closes a same-release pull request only when the + default branch on GitHub already vendors the release. +- A tracking ref left from an update branch GitHub has deleted no longer makes the next push fail. +- When `ci.yml` has no `workflow_dispatch` trigger, the `Standard update` run now says so and fails + after opening its pull request, instead of stopping with an unexplained error. + +This is a patch release, so its update pull request merges itself once `Validate` passes. It is the +first release to reach repositories through their own `Standard update` workflow. diff --git a/examples/basic/.claude/settings.json b/examples/basic/.claude/settings.json index 22dbad4..edfe297 100644 --- a/examples/basic/.claude/settings.json +++ b/examples/basic/.claude/settings.json @@ -4,7 +4,7 @@ "source": { "source": "github", "repo": "LasVegasForTransit/repository-tooling", - "ref": "v0.5.0" + "ref": "v0.5.1" } } }, diff --git a/examples/basic/package.json b/examples/basic/package.json index ed616f4..b3ff9b4 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -21,8 +21,8 @@ "*": "prettier --write --ignore-unknown" }, "devDependencies": { - "@lasvegasfortransit/cli": "0.5.0", - "@lasvegasfortransit/prettier-config": "0.5.0", + "@lasvegasfortransit/cli": "0.5.1", + "@lasvegasfortransit/prettier-config": "0.5.1", "lint-staged": "catalog:", "markdownlint-cli2": "catalog:", "markdownlint-rule-relative-links": "catalog:", diff --git a/examples/basic/packages/example/package.json b/examples/basic/packages/example/package.json index 10b4c09..1ec6203 100644 --- a/examples/basic/packages/example/package.json +++ b/examples/basic/packages/example/package.json @@ -16,9 +16,9 @@ "test": "vitest run" }, "devDependencies": { - "@lasvegasfortransit/eslint-config": "0.5.0", - "@lasvegasfortransit/typescript-config": "0.5.0", - "@lasvegasfortransit/vitest-config": "0.5.0", + "@lasvegasfortransit/eslint-config": "0.5.1", + "@lasvegasfortransit/typescript-config": "0.5.1", + "@lasvegasfortransit/vitest-config": "0.5.1", "@types/node": "catalog:", "eslint": "catalog:", "typescript": "catalog:", diff --git a/examples/with-astro/.claude/settings.json b/examples/with-astro/.claude/settings.json index 22dbad4..edfe297 100644 --- a/examples/with-astro/.claude/settings.json +++ b/examples/with-astro/.claude/settings.json @@ -4,7 +4,7 @@ "source": { "source": "github", "repo": "LasVegasForTransit/repository-tooling", - "ref": "v0.5.0" + "ref": "v0.5.1" } } }, diff --git a/examples/with-astro/apps/site/package.json b/examples/with-astro/apps/site/package.json index 19ac71d..cea3d2c 100644 --- a/examples/with-astro/apps/site/package.json +++ b/examples/with-astro/apps/site/package.json @@ -21,10 +21,10 @@ }, "devDependencies": { "@astrojs/check": "catalog:", - "@lasvegasfortransit/eslint-config": "0.5.0", - "@lasvegasfortransit/playwright-config": "0.5.0", - "@lasvegasfortransit/typescript-config": "0.5.0", - "@lasvegasfortransit/vitest-config": "0.5.0", + "@lasvegasfortransit/eslint-config": "0.5.1", + "@lasvegasfortransit/playwright-config": "0.5.1", + "@lasvegasfortransit/typescript-config": "0.5.1", + "@lasvegasfortransit/vitest-config": "0.5.1", "@playwright/test": "catalog:", "@types/node": "catalog:", "eslint": "catalog:", diff --git a/examples/with-astro/package.json b/examples/with-astro/package.json index 50671a2..ee5c183 100644 --- a/examples/with-astro/package.json +++ b/examples/with-astro/package.json @@ -23,8 +23,8 @@ "*": "prettier --write --ignore-unknown" }, "devDependencies": { - "@lasvegasfortransit/cli": "0.5.0", - "@lasvegasfortransit/prettier-config": "0.5.0", + "@lasvegasfortransit/cli": "0.5.1", + "@lasvegasfortransit/prettier-config": "0.5.1", "lint-staged": "catalog:", "markdownlint-cli2": "catalog:", "markdownlint-rule-relative-links": "catalog:", diff --git a/examples/with-vite-react/.claude/settings.json b/examples/with-vite-react/.claude/settings.json index 22dbad4..edfe297 100644 --- a/examples/with-vite-react/.claude/settings.json +++ b/examples/with-vite-react/.claude/settings.json @@ -4,7 +4,7 @@ "source": { "source": "github", "repo": "LasVegasForTransit/repository-tooling", - "ref": "v0.5.0" + "ref": "v0.5.1" } } }, diff --git a/examples/with-vite-react/apps/app/package.json b/examples/with-vite-react/apps/app/package.json index dc885d2..6022f32 100644 --- a/examples/with-vite-react/apps/app/package.json +++ b/examples/with-vite-react/apps/app/package.json @@ -17,10 +17,10 @@ "react-dom": "catalog:" }, "devDependencies": { - "@lasvegasfortransit/eslint-config": "0.5.0", - "@lasvegasfortransit/playwright-config": "0.5.0", - "@lasvegasfortransit/typescript-config": "0.5.0", - "@lasvegasfortransit/vitest-config": "0.5.0", + "@lasvegasfortransit/eslint-config": "0.5.1", + "@lasvegasfortransit/playwright-config": "0.5.1", + "@lasvegasfortransit/typescript-config": "0.5.1", + "@lasvegasfortransit/vitest-config": "0.5.1", "@playwright/test": "catalog:", "@tailwindcss/vite": "catalog:", "@types/node": "catalog:", diff --git a/examples/with-vite-react/package.json b/examples/with-vite-react/package.json index 2199229..ecda05d 100644 --- a/examples/with-vite-react/package.json +++ b/examples/with-vite-react/package.json @@ -23,8 +23,8 @@ "*": "prettier --write --ignore-unknown" }, "devDependencies": { - "@lasvegasfortransit/cli": "0.5.0", - "@lasvegasfortransit/prettier-config": "0.5.0", + "@lasvegasfortransit/cli": "0.5.1", + "@lasvegasfortransit/prettier-config": "0.5.1", "lint-staged": "catalog:", "markdownlint-cli2": "catalog:", "markdownlint-rule-relative-links": "catalog:", diff --git a/package.json b/package.json index 87ef2b5..9acc987 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lvbt-repository-tooling", "private": true, - "version": "0.5.0", + "version": "0.5.1", "type": "module", "description": "Source of truth for the LVBT repository standard: the shared @lasvegasfortransit packages and the example repositories create-turbo copies.", "packageManager": "pnpm@11.25.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 500ac05..49acae7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/cli", - "version": "0.5.0", + "version": "0.5.1", "description": "The lvbt command every LVBT repository runs for bootstrap, preflight, and deploy, plus the production platform setup, the shared git hooks, and the lvbt-contributions agent plugin.", "license": "MIT", "type": "module", diff --git a/packages/cli/plugins/lvbt-contributions/.claude-plugin/plugin.json b/packages/cli/plugins/lvbt-contributions/.claude-plugin/plugin.json index 18c099d..ac5dc3a 100644 --- a/packages/cli/plugins/lvbt-contributions/.claude-plugin/plugin.json +++ b/packages/cli/plugins/lvbt-contributions/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "lvbt-contributions", - "version": "0.5.0", + "version": "0.5.1", "description": "Create readable LVBT GitHub issues and pull requests through the organization workflow.", "author": { "name": "Las Vegans for Better Transit", diff --git a/packages/cli/plugins/lvbt-contributions/.codex-plugin/plugin.json b/packages/cli/plugins/lvbt-contributions/.codex-plugin/plugin.json index 5c68dc0..03f60e4 100644 --- a/packages/cli/plugins/lvbt-contributions/.codex-plugin/plugin.json +++ b/packages/cli/plugins/lvbt-contributions/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "lvbt-contributions", - "version": "0.5.0", + "version": "0.5.1", "description": "Create readable LVBT GitHub issues and pull requests through the organization workflow.", "author": { "name": "Las Vegans for Better Transit", diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 50b0c72..5e9c3d2 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/eslint-config", - "version": "0.5.0", + "version": "0.5.1", "description": "The ESLint configurations every LVBT repository uses.", "license": "MIT", "type": "module", diff --git a/packages/playwright-config/package.json b/packages/playwright-config/package.json index f2108b1..d6ae7c0 100644 --- a/packages/playwright-config/package.json +++ b/packages/playwright-config/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/playwright-config", - "version": "0.5.0", + "version": "0.5.1", "description": "The Playwright configuration every LVBT repository spreads into its own: end-to-end tests under tests/e2e, desktop and mobile projects, traces on failure.", "license": "MIT", "type": "module", diff --git a/packages/prettier-config/package.json b/packages/prettier-config/package.json index 6428c0f..e3cdf8d 100644 --- a/packages/prettier-config/package.json +++ b/packages/prettier-config/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/prettier-config", - "version": "0.5.0", + "version": "0.5.1", "description": "The Prettier configuration every LVBT repository uses.", "license": "MIT", "type": "module", diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index da0e97c..1330efa 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/typescript-config", - "version": "0.5.0", + "version": "0.5.1", "description": "TypeScript configurations every LVBT repository extends.", "license": "MIT", "repository": { diff --git a/packages/vitest-config/package.json b/packages/vitest-config/package.json index 35ab091..e077e5d 100644 --- a/packages/vitest-config/package.json +++ b/packages/vitest-config/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/vitest-config", - "version": "0.5.0", + "version": "0.5.1", "description": "The Vitest configuration every LVBT repository spreads into its own.", "license": "MIT", "type": "module", diff --git a/packages/web-platform/package.json b/packages/web-platform/package.json index bff470a..94ca12f 100644 --- a/packages/web-platform/package.json +++ b/packages/web-platform/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/web-platform", - "version": "0.5.0", + "version": "0.5.1", "description": "Provider-neutral deployment, provisioning, and validation primitives for LVBT web repositories.", "license": "MIT", "type": "module", diff --git a/standards/propose.ts b/standards/propose.ts index f2b5720..f86fdd8 100644 --- a/standards/propose.ts +++ b/standards/propose.ts @@ -29,7 +29,12 @@ function remoteBranchState( branch: string, runner: Runner, ): 'absent' | 'current' | 'stale' | 'edited' { - if (!runner('git', ['ls-remote', '--heads', 'origin', branch], target)) return 'absent'; + if (!runner('git', ['ls-remote', '--heads', 'origin', branch], target)) { + // A tracking ref left from a branch GitHub has since deleted would make --force-with-lease + // refuse the push, so forget it. + runner('git', ['update-ref', '-d', `refs/remotes/origin/${branch}`], target); + return 'absent'; + } runner( 'git', ['fetch', '--quiet', 'origin', `+refs/heads/${branch}:refs/remotes/origin/${branch}`], @@ -136,6 +141,33 @@ function createOrEdit(options: { return (JSON.parse(runner('node', args, target)) as { number: number }).number; } +/** The release the repository's default branch vendors, read from GitHub rather than the checkout. */ +function defaultBranchRelease(target: string, runner: Runner): string | null { + const manifest = runner( + 'gh', + [ + 'api', + 'repos/{owner}/{repo}/contents/.lvbt/web-platform.json', + '-H', + 'Accept: application/vnd.github.raw', + ], + target, + ); + return (JSON.parse(manifest) as { release: string | null }).release; +} + +/** Runs Validate on the update branch; a workflow token's push alone starts no workflow. */ +function dispatchValidation(target: string, branch: string, runner: Runner): void { + try { + runner('gh', ['workflow', 'run', 'ci.yml', '--ref', branch], target); + } catch { + process.stderr.write( + `Could not run ci.yml on ${branch}. Give ci.yml a workflow_dispatch trigger, as the examples' has, so update pull requests get their Validate check.\n`, + ); + process.exitCode = 1; + } +} + /** * Pushes the update branch, opens or refreshes its pull request, enables auto-merge, and closes * update pull requests for older releases. @@ -166,8 +198,10 @@ export async function proposeRelease(options: { if (automerge) runner('gh', ['pr', 'merge', String(number), '--auto', '--rebase'], target); // A push made with a repository's own GITHUB_TOKEN starts no workflow, but a dispatch always // does, and its Validate check lands on the branch's head commit. - if (pushed) runner('gh', ['workflow', 'run', 'ci.yml', '--ref', updateBranch(tag)], target); - } else { + if (pushed) dispatchValidation(target, updateBranch(tag), runner); + } else if (defaultBranchRelease(target, runner) === tag) { + // Only a default branch that already carries the release makes its update pull request moot; + // a checkout that happens to be on the update branch does not. superseded.push(...open.filter(({ headRefName }) => headRefName === updateBranch(tag))); } diff --git a/tests/propose.test.mjs b/tests/propose.test.mjs new file mode 100644 index 0000000..2db2bad --- /dev/null +++ b/tests/propose.test.mjs @@ -0,0 +1,79 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { proposeRelease } from '../standards/propose.ts'; + +const entry = { name: 'example', requiredStatus: 'Validate', kind: 'consumer' }; + +/** A runner that records every command and answers from a table of canned outputs. */ +function fakeRunner(answers) { + const calls = []; + const runner = (command, args) => { + const line = [command, ...args].join(' '); + calls.push(line); + for (const [pattern, answer] of answers) { + if (!pattern.test(line)) continue; + if (answer instanceof Error) throw answer; + return answer; + } + return ''; + }; + return { calls, runner }; +} + +const options = (runner, overrides = {}) => ({ + source: '/nonexistent/source', + tooling: '/nonexistent/tooling', + target: '/nonexistent/target', + entry, + tag: 'v0.5.0', + changed: false, + from: 'v0.4.5', + runner, + ...overrides, +}); + +const openUpdate = JSON.stringify([ + { number: 7, headRefName: 'automation/repository-standard-v0.5.0' }, +]); + +test('an unchanged checkout closes its update only when the default branch has the release', async () => { + const behind = fakeRunner([ + [/^gh pr list --state open/, openUpdate], + [/web-platform\.json/, JSON.stringify({ release: 'v0.4.5' })], + ]); + await proposeRelease(options(behind.runner)); + assert.ok(!behind.calls.some((call) => call.startsWith('gh pr close'))); + + const current = fakeRunner([ + [/^gh pr list --state open/, openUpdate], + [/web-platform\.json/, JSON.stringify({ release: 'v0.5.0' })], + ]); + await proposeRelease(options(current.runner)); + assert.ok(current.calls.some((call) => call.startsWith('gh pr close 7'))); +}); + +test('a branch GitHub deleted is pushed afresh and a failed dispatch is reported, not thrown', async (t) => { + const exitCode = process.exitCode; + t.after(() => { + process.exitCode = exitCode; + }); + const { calls, runner } = fakeRunner([ + [/^gh pr list --state open/, '[]'], + [/^git ls-remote --heads/, ''], + [/^gh pr list --head/, JSON.stringify([{ number: 12 }])], + [/^gh workflow run ci\.yml/, new Error('no workflow_dispatch trigger')], + ]); + const outcome = await proposeRelease( + options(runner, { changed: true, from: 'v0.5.0', tag: 'v0.5.1' }), + ); + assert.equal(outcome, 'example: #12'); + const forget = calls.findIndex((call) => call.startsWith('git update-ref -d')); + const push = calls.findIndex((call) => call.startsWith('git push --force-with-lease')); + assert.ok(forget !== -1 && forget < push, 'the stale tracking ref is forgotten before the push'); + assert.ok( + calls.some((call) => call === 'gh pr merge 12 --auto --rebase'), + 'a patch merges itself', + ); + assert.equal(process.exitCode, 1); +});