From 8f275936f12ec0d1cfc7d686f027ba51ff97f78c Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Tue, 6 Oct 2026 18:07:08 -0400 Subject: [PATCH 1/3] ci(release): consume shared pnpm-release-management toolchain Replace this repo's local release tooling with thin callers of the shared reusable workflows from systemfsoftware/pnpm-release-management, pinned at @prm/toolchain with tools-ref: prm/toolchain. The toolchain derives the release phase from repository state and reads the new release.jsonc (pnpm versioning, turbo build gate, base main, branch changeset-release/main). Delete scripts/{plan-release,tag-released-packages,create-github-releases, check-changeset,open-release-pr,lib/cycle,lib/run} and scripts/deno.{json,lock}. npm publishing is gone as a consequence: the shared workflow has no npm token, no OIDC, and no registry. Distribution is this repo's Nix flake outputs consumed from a git ref. Rewrite the publishing docs and drop the npm install path and version badge in favour of the Nix-flake-from-git path --- .changeset/README.md | 14 +- .github/workflows/changeset-check.yml | 21 +- .github/workflows/release.yml | 117 +------ README.md | 18 +- ...tandalone-release-drops-github-releases.md | 2 +- release.jsonc | 19 ++ scripts/check-changeset.ts | 64 ---- scripts/create-github-releases.ts | 120 ------- scripts/deno.json | 14 - scripts/deno.lock | 297 ------------------ scripts/lib/cycle.ts | 54 ---- scripts/lib/run.ts | 14 - scripts/open-release-pr.sh | 58 ---- scripts/plan-release.ts | 21 -- scripts/tag-released-packages.ts | 62 ---- 15 files changed, 61 insertions(+), 834 deletions(-) create mode 100644 release.jsonc delete mode 100755 scripts/check-changeset.ts delete mode 100755 scripts/create-github-releases.ts delete mode 100644 scripts/deno.json delete mode 100644 scripts/deno.lock delete mode 100644 scripts/lib/cycle.ts delete mode 100644 scripts/lib/run.ts delete mode 100755 scripts/open-release-pr.sh delete mode 100755 scripts/plan-release.ts delete mode 100755 scripts/tag-released-packages.ts diff --git a/.changeset/README.md b/.changeset/README.md index 21a794d..3cf6608 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -18,7 +18,13 @@ pnpm change --bump --summary "" [ - This README is NOT a changeset: the gate requires a file whose frontmatter parses as `"": `. -Publishing uses npm OIDC trusted publishing from `.github/workflows/release.yml`. -Register `@systemfsoftware/omp-claude-compat` as a trusted publisher on npmjs.com -pointing at this repository and that workflow filename before the first new -version can ship. OIDC cannot debut a package npm has never seen. +Releases are driven by the shared release toolchain +(`systemfsoftware/pnpm-release-management`), consumed as a reusable workflow from +`.github/workflows/release.yml` and configured by this repo's `release.jsonc`. +Distribution is this repository's Nix flake outputs consumed from a git ref +(pinned by `flake.lock` rev + narHash), not an npm registry: the release path +writes a `@systemfsoftware/omp-claude-compat@vX.Y.Z` git tag and a GitHub Release +for each unreleased version — there is no npm token, no OIDC trusted publishing, +and no registry to configure. The git tag is the durable record that a version +shipped, and tagging is idempotent, so a half-finished release resumes safely on +the next push to `main`. diff --git a/.github/workflows/changeset-check.yml b/.github/workflows/changeset-check.yml index 59ab9b6..34fc425 100644 --- a/.github/workflows/changeset-check.yml +++ b/.github/workflows/changeset-check.yml @@ -1,3 +1,8 @@ +# Thin caller of the shared changeset-check reusable workflow +# (systemfsoftware/pnpm-release-management). It requires a change intent for +# every publishable-package change in a pull request. +# +# tools-ref pins the toolchain revision the check runs from. name: Changeset Check on: @@ -6,16 +11,10 @@ on: permissions: contents: read + pull-requests: read jobs: - require-changeset: - if: github.head_ref != 'changeset-release/main' - name: a publishable-package change needs a changeset - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - uses: denoland/setup-deno@v2 - - name: Require a changeset for publishable-package changes - run: ./scripts/check-changeset.ts ${{ github.event.pull_request.base.sha }} + changeset-check: + uses: systemfsoftware/pnpm-release-management/.github/workflows/changeset-check.yml@prm/toolchain + with: + tools-ref: prm/toolchain diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 15f74be..9b57dff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,116 +1,23 @@ +# Thin caller of the shared release toolchain +# (systemfsoftware/pnpm-release-management). The reusable workflow is the single +# definition of the release flow; this caller only supplies the trigger and the +# permissions. The toolchain reads this repo's release.jsonc and derives the +# phase (version / release / none) from repository state. +# +# tools-ref pins the toolchain revision the release runs from. name: Release on: push: branches: [main] - -concurrency: - group: release-${{ github.ref }} - cancel-in-progress: false + workflow_dispatch: permissions: contents: write pull-requests: write jobs: - plan: - name: plan · derive phase - runs-on: ubuntu-latest - permissions: - contents: read - outputs: - phase: ${{ steps.plan.outputs.phase }} - env: - HUSKY: "0" - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - fetch-tags: true - - uses: denoland/setup-deno@v2 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v7 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Preflight intent consumption - run: pnpm version -r --dry-run - - id: plan - name: Decide release phase from repository state - run: ./scripts/plan-release.ts --output "$GITHUB_OUTPUT" - - version: - if: needs.plan.outputs.phase == 'version' - needs: [plan] - name: version · open release PR - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - env: - HUSKY: "0" - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - uses: denoland/setup-deno@v2 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v7 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Consume pending change intents - run: pnpm version -r - - name: Capture release set - run: ./scripts/tag-released-packages.ts --dry-run --json --output /tmp/captured.json && cat /tmp/captured.json - - name: Assert release notes - env: - GITHUB_TOKEN: ${{ github.token }} - run: ./scripts/create-github-releases.ts --assert --captured /tmp/captured.json - - name: Open or update the Release PR - env: - GH_TOKEN: ${{ github.token }} - BRANCH: changeset-release/main - BASE: ${{ github.event.repository.default_branch }} - run: ./scripts/open-release-pr.sh - - publish: - if: needs.plan.outputs.phase == 'publish' - permissions: - contents: write - id-token: write - name: publish · oidc · tag - needs: [plan] - runs-on: ubuntu-latest - env: - HUSKY: "0" - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - fetch-tags: true - - uses: denoland/setup-deno@v2 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v7 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Capture release set - run: ./scripts/tag-released-packages.ts --dry-run --json --output /tmp/captured.json - - name: Assert release notes - env: - GITHUB_TOKEN: ${{ github.token }} - run: ./scripts/create-github-releases.ts --assert --captured /tmp/captured.json - - name: Build - run: pnpm build - - name: Publish unpublished versions via OIDC - run: ./scripts/tag-released-packages.ts --unpublished --captured /tmp/captured.json --publish - - name: Tag released versions - run: ./scripts/tag-released-packages.ts --captured /tmp/captured.json - - name: GitHub Releases from authored changelogs - env: - GITHUB_TOKEN: ${{ github.token }} - run: ./scripts/create-github-releases.ts --captured /tmp/captured.json + release: + uses: systemfsoftware/pnpm-release-management/.github/workflows/release.yml@prm/toolchain + with: + tools-ref: prm/toolchain diff --git a/README.md b/README.md index 2be03ae..02e8159 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ `@systemfsoftware/omp-claude-compat` brings your Claude Code configuration into [Oh My Pi](https://github.com/can1357/oh-my-pi) (OMP). Your hooks keep firing. Your `CLAUDE.md` rules keep applying. You change nothing — install one plugin and your existing setup just works. -[![npm version](https://img.shields.io/npm/v/@systemfsoftware/omp-claude-compat?style=flat)](https://www.npmjs.com/package/@systemfsoftware/omp-claude-compat) -[![license](https://img.shields.io/npm/l/@systemfsoftware/omp-claude-compat?style=flat)](./LICENSE) +[![license](https://img.shields.io/badge/license-Apache--2.0-blue?style=flat)](./LICENSE) ## What is this? @@ -13,14 +12,15 @@ This is a plugin for Oh My Pi that understands Claude Code configuration. It run ## Install -**How do I use my Claude Code setup in Oh My Pi?** Install the package and list it as a plugin: +**How do I use my Claude Code setup in Oh My Pi?** Add it as a Nix flake input +and list it as a plugin. This package is **not** published to any npm registry — +distribution is this repository's Nix flake outputs, consumed from a git ref and +pinned by `flake.lock` rev + narHash: -```bash -pnpm add @systemfsoftware/omp-claude-compat -``` - -```bash -npm install @systemfsoftware/omp-claude-compat +```nix +{ + inputs.omp-claude-compat.url = "github:systemfsoftware/omp-claude-compat"; +} ``` ```json diff --git a/docs/solutions/integration-issues/standalone-release-drops-github-releases.md b/docs/solutions/integration-issues/standalone-release-drops-github-releases.md index 15bd1f0..b26fb92 100644 --- a/docs/solutions/integration-issues/standalone-release-drops-github-releases.md +++ b/docs/solutions/integration-issues/standalone-release-drops-github-releases.md @@ -49,7 +49,7 @@ Root-cause chain: the port preserved the plan/version/publish skeleton but dropp Architectural invariants: -- **Terminal-action completeness.** A release pipeline's terminal phase must produce every consumer-visible artifact (registry version, tag, Release object with notes and latest promotion). If the last step only moves a ref, the pipeline ships half a release by construction. +- **Terminal-action completeness.** A release pipeline's terminal phase must produce every consumer-visible artifact. For this repo that is the `@vX.Y.Z` git tag and the GitHub Release object with notes and latest promotion — distribution is this repository's Nix flake outputs consumed from a git ref, so the tag is the durable record that a version shipped and there is no registry version to produce. If the last step only moves a ref without cutting the Release, the pipeline ships half a release by construction. - **Generated-output capture.** Let $G$ be the generator's output set and $S$ the staged set. Update-only staging stages $\{f : \text{tracked}(f)\}$; generated files start untracked, so $S \cap G = \varnothing$ always. Completeness requires the staging predicate to cover $G$, not just tracked modifications. - **Assert at consumption.** Each phase asserts the artifacts it consumes before acting on them. A producer/consumer handoff (captured set, changelog files) with no consumer-side check converts every upstream drop into a silent downstream degradation. diff --git a/release.jsonc b/release.jsonc new file mode 100644 index 0000000..43f4ca3 --- /dev/null +++ b/release.jsonc @@ -0,0 +1,19 @@ +{ + // Config for the shared release toolchain + // (systemfsoftware/pnpm-release-management), consumed as a reusable workflow. + // The toolchain derives the release phase from repository state and reads this + // file; there is no local release tooling in this repo. + // + // Distribution is this repository's Nix flake outputs consumed from a git ref + // (pinned by flake.lock rev + narHash), not an npm registry. The release path + // writes a @vX.Y.Z git tag and a GitHub Release per unreleased version — + // no npm token, no OIDC, no registry. + "base": "main", + "branch": "changeset-release/main", + "changesetDir": ".changeset", + "changelogDir": ".changeset/changelogs", + "versioning": { + "strategy": "pnpm" + }, + "gate": { "strategy": "turbo", "task": "build" } +} diff --git a/scripts/check-changeset.ts b/scripts/check-changeset.ts deleted file mode 100755 index a69ac2e..0000000 --- a/scripts/check-changeset.ts +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env -S deno run --config=scripts/deno.json --allow-read --allow-run=git --allow-import --allow-net=jsr.io - -import { withoutAll } from '@std/collections/without-all' -import { extractYaml, test } from '@std/front-matter' -import { expandGlob } from '@std/fs/expand-glob' -import { basename } from '@std/path' -import { run } from './lib/run.ts' - -const BUMP: Record = { none: true, patch: true, minor: true, major: true } - -const intentPackages = (markdown: string) => { - if (!test(markdown)) return [] - return Object.entries(extractYaml>(markdown).attrs) - .filter(([, bump]) => typeof bump === 'string' && BUMP[bump]) - .map(([name]) => name) -} - -const publicPackages = async () => { - const names: string[] = [] - for await (const file of expandGlob('packages/*/package.json')) { - const pkg = JSON.parse(await Deno.readTextFile(file.path)) as { - name?: string - version?: string - private?: boolean - } - if (pkg.name && pkg.version && !pkg.private) names.push(pkg.name) - } - return names -} - -const namedIntents = async () => { - const named: string[] = [] - for await (const file of expandGlob('.changeset/*.md')) { - if (basename(file.path) === 'README.md') continue - named.push(...intentPackages(await Deno.readTextFile(file.path))) - } - return named -} - -const baseSha = Deno.args[0] -if (!baseSha) { - console.error('usage: ./scripts/check-changeset.ts ') - Deno.exit(2) -} - -const changed = (await run('git', ['diff', '--name-only', `${baseSha}...HEAD`])).split('\n').filter(Boolean) -const touched = changed.some((file) => file === 'packages' || file.startsWith('packages/')) - ? await publicPackages() - : [] -const missing = withoutAll(touched, await namedIntents()) - -if (missing.length === 0) { - console.log( - touched.length === 0 ? 'no publishable-package paths in the diff' : `changeset covers: ${touched.join(', ')}`, - ) - Deno.exit(0) -} - -console.error( - `::error::publishable package(s) changed with no changeset intent: ${ - missing.join(', ') - }. Author one with \`pnpm change --bump --summary "" ${missing[0]}\`.`, -) -Deno.exit(1) diff --git a/scripts/create-github-releases.ts b/scripts/create-github-releases.ts deleted file mode 100755 index 7cc5654..0000000 --- a/scripts/create-github-releases.ts +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env -S deno run --config=scripts/deno.json --allow-read --allow-write --allow-run=git --allow-net=api.github.com,registry.npmjs.org --allow-env=GH_TOKEN,GITHUB_TOKEN,GITHUB_REPOSITORY --allow-import - -import { parseArgs } from '@std/cli/parse-args' -import { Octokit, RequestError } from 'octokit' -import { type CycleEntry, loadCaptured, loadWorkspaceCycle } from './lib/cycle.ts' -import { run } from './lib/run.ts' - -const flags = parseArgs(Deno.args, { - boolean: ['dry-run', 'assert'], - string: ['captured'], -}) - -const cycle: CycleEntry[] = flags.captured ? await loadCaptured(flags.captured) : await loadWorkspaceCycle() - -if (cycle.length === 0) { - console.log('no this-cycle releases — empty captured set') - Deno.exit(0) -} - -const pending: { entry: CycleEntry; body: string }[] = [] -for (const entry of cycle) { - const { name, version, changelog } = entry - let raw: string | null = null - try { - raw = await Deno.readTextFile(changelog) - } catch { - raw = null - } - if (raw === null || raw.trim().length === 0) { - const state = raw === null ? 'Missing' : 'Empty' - console.error( - `::error::${state} changelog for ${name}@${version}: expected ${changelog} — body must be the pnpm-generated changelog.`, - ) - Deno.exit(1) - } - pending.push({ entry, body: raw.trim() }) -} - -if (flags.assert) { - console.log(`assert ok: ${cycle.length} changelog(s) present`) - Deno.exit(0) -} - -if (flags['dry-run']) { - for (const { entry } of pending) { - console.log(`would create release ${entry.tag} from ${entry.changelog}`) - } - console.log(`dry run: ${pending.length} release(s)`) - Deno.exit(0) -} - -const slug = Deno.env.get('GITHUB_REPOSITORY') ?? (await run('git', ['remote', 'get-url', 'origin'])) - .trim() - .replace(/^git@github\.com:/, 'https://github.com/') - .replace(/^https?:\/\/github\.com\//, '') - .replace(/\.git$/, '') -const [owner, repo] = slug.split('/') -const octokit = new Octokit({ auth: Deno.env.get('GITHUB_TOKEN') ?? Deno.env.get('GH_TOKEN') ?? undefined }) - -const created: { tag: string; id: number }[] = [] -let loopError: Error | null = null -for (const { entry, body } of pending) { - const { tag } = entry - let exists = false - try { - await octokit.rest.repos.getReleaseByTag({ owner, repo, tag }) - exists = true - } catch (error) { - if (!(error instanceof RequestError) || error.status !== 404) { - loopError = new Error( - `looking up ${tag} in ${owner}/${repo} failed: ${error instanceof Error ? error.message : String(error)}`, - ) - break - } - } - if (exists) { - console.log(`skip ${tag} — release exists`) - continue - } - try { - const res = await octokit.rest.repos.createRelease({ - owner, - repo, - tag_name: tag, - body, - prerelease: false, - make_latest: 'false', - }) - console.log(`created release ${tag}`) - created.push({ tag, id: res.data.id }) - } catch (error) { - if (error instanceof RequestError && error.status === 409) { - console.log(`skip ${tag} — release exists`) - continue - } - loopError = new Error( - `creating release ${tag} failed: ${error instanceof Error ? error.message : String(error)}`, - ) - break - } -} - -if (created.length > 0 && !loopError) { - try { - await octokit.rest.repos.updateRelease({ owner, repo, release_id: created[0].id, make_latest: 'true' }) - console.log(`reconciled make_latest true on ${created[0].tag}`) - } catch (error) { - const msg = `reconciling make_latest for ${created[0].tag} failed: ${ - error instanceof Error ? error.message : String(error) - }` - console.error(`::error::${msg}`) - loopError = new Error(msg) - } -} - -if (loopError) { - console.error(`::error::${loopError.message}`) - Deno.exit(1) -} -console.log(`created ${created.length} release(s), skipped ${cycle.length - created.length}`) diff --git a/scripts/deno.json b/scripts/deno.json deleted file mode 100644 index baadff9..0000000 --- a/scripts/deno.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "noImplicitOverride": true - }, - "imports": { - "@std/cli/parse-args": "jsr:@std/cli@1/parse-args", - "@std/collections/without-all": "jsr:@std/collections@1/without-all", - "@std/front-matter": "jsr:@std/front-matter@1", - "@std/fs/expand-glob": "jsr:@std/fs@1/expand-glob", - "@std/path": "jsr:@std/path@1", - "octokit": "npm:octokit@^4" - } -} diff --git a/scripts/deno.lock b/scripts/deno.lock deleted file mode 100644 index 5fb7206..0000000 --- a/scripts/deno.lock +++ /dev/null @@ -1,297 +0,0 @@ -{ - "version": "5", - "specifiers": { - "jsr:@std/cli@1": "1.0.32", - "jsr:@std/collections@1": "1.3.0", - "jsr:@std/collections@^1.1.3": "1.3.0", - "jsr:@std/front-matter@1": "1.0.9", - "jsr:@std/fs@1": "1.0.24", - "jsr:@std/internal@^1.0.14": "1.0.14", - "jsr:@std/path@1": "1.1.6", - "jsr:@std/path@^1.1.5": "1.1.6", - "jsr:@std/toml@^1.0.3": "1.0.11", - "jsr:@std/yaml@^1.0.5": "1.2.0", - "npm:octokit@4": "4.1.4" - }, - "jsr": { - "@std/cli@1.0.32": { - "integrity": "188b3a100d6202d64e3f5bd3d799c7fa4f6d77f92cc65eb7f641c1fa0aa92a66" - }, - "@std/collections@1.3.0": { - "integrity": "eb36b43d784477ea0b476483ac034a14bdd182aff921c812ecf662a1fcef9498" - }, - "@std/front-matter@1.0.9": { - "integrity": "ee6201d06674cbef137dda2252f62477450b48249e7d8d9ab57a30f85ff6f051", - "dependencies": [ - "jsr:@std/toml", - "jsr:@std/yaml" - ] - }, - "@std/fs@1.0.24": { - "integrity": "f3061b45b81673a2bece689da041df32d174be064c89eb6397fb5718d3fb7877", - "dependencies": [ - "jsr:@std/internal", - "jsr:@std/path@^1.1.5" - ] - }, - "@std/internal@1.0.14": { - "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" - }, - "@std/path@1.1.6": { - "integrity": "c68485c2a4dfbb5ae3cc74fae4e8c4e5d874cf8a8ed12927917235c758b46cbe", - "dependencies": [ - "jsr:@std/internal" - ] - }, - "@std/toml@1.0.11": { - "integrity": "e084988b872ca4bad6aedfb7350f6eeed0e8ba88e9ee5e1590621c5b5bb8f715", - "dependencies": [ - "jsr:@std/collections@^1.1.3" - ] - }, - "@std/yaml@1.2.0": { - "integrity": "20beb41e4983ba3437dbefac62b14061ab058e8a187596f19d28ff9035f6e6cf" - } - }, - "npm": { - "@octokit/app@15.1.6": { - "integrity": "sha512-WELCamoCJo9SN0lf3SWZccf68CF0sBNPQuLYmZ/n87p5qvBJDe9aBtr5dHkh7T9nxWZ608pizwsUbypSzZAiUw==", - "dependencies": [ - "@octokit/auth-app", - "@octokit/auth-unauthenticated", - "@octokit/core", - "@octokit/oauth-app", - "@octokit/plugin-paginate-rest", - "@octokit/types", - "@octokit/webhooks" - ] - }, - "@octokit/auth-app@7.2.2": { - "integrity": "sha512-p6hJtEyQDCJEPN9ijjhEC/kpFHMHN4Gca9r+8S0S8EJi7NaWftaEmexjxxpT1DFBeJpN4u/5RE22ArnyypupJw==", - "dependencies": [ - "@octokit/auth-oauth-app", - "@octokit/auth-oauth-user", - "@octokit/request", - "@octokit/request-error", - "@octokit/types", - "toad-cache", - "universal-github-app-jwt", - "universal-user-agent" - ] - }, - "@octokit/auth-oauth-app@8.1.4": { - "integrity": "sha512-71iBa5SflSXcclk/OL3lJzdt4iFs56OJdpBGEBl1wULp7C58uiswZLV6TdRaiAzHP1LT8ezpbHlKuxADb+4NkQ==", - "dependencies": [ - "@octokit/auth-oauth-device", - "@octokit/auth-oauth-user", - "@octokit/request", - "@octokit/types", - "universal-user-agent" - ] - }, - "@octokit/auth-oauth-device@7.1.5": { - "integrity": "sha512-lR00+k7+N6xeECj0JuXeULQ2TSBB/zjTAmNF2+vyGPDEFx1dgk1hTDmL13MjbSmzusuAmuJD8Pu39rjp9jH6yw==", - "dependencies": [ - "@octokit/oauth-methods", - "@octokit/request", - "@octokit/types", - "universal-user-agent" - ] - }, - "@octokit/auth-oauth-user@5.1.6": { - "integrity": "sha512-/R8vgeoulp7rJs+wfJ2LtXEVC7pjQTIqDab7wPKwVG6+2v/lUnCOub6vaHmysQBbb45FknM3tbHW8TOVqYHxCw==", - "dependencies": [ - "@octokit/auth-oauth-device", - "@octokit/oauth-methods", - "@octokit/request", - "@octokit/types", - "universal-user-agent" - ] - }, - "@octokit/auth-token@5.1.2": { - "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==" - }, - "@octokit/auth-unauthenticated@6.1.3": { - "integrity": "sha512-d5gWJla3WdSl1yjbfMpET+hUSFCE15qM0KVSB0H1shyuJihf/RL1KqWoZMIaonHvlNojkL9XtLFp8QeLe+1iwA==", - "dependencies": [ - "@octokit/request-error", - "@octokit/types" - ] - }, - "@octokit/core@6.1.6": { - "integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==", - "dependencies": [ - "@octokit/auth-token", - "@octokit/graphql", - "@octokit/request", - "@octokit/request-error", - "@octokit/types", - "before-after-hook", - "universal-user-agent" - ] - }, - "@octokit/endpoint@10.1.4": { - "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", - "dependencies": [ - "@octokit/types", - "universal-user-agent" - ] - }, - "@octokit/graphql@8.2.2": { - "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", - "dependencies": [ - "@octokit/request", - "@octokit/types", - "universal-user-agent" - ] - }, - "@octokit/oauth-app@7.1.6": { - "integrity": "sha512-OMcMzY2WFARg80oJNFwWbY51TBUfLH4JGTy119cqiDawSFXSIBujxmpXiKbGWQlvfn0CxE6f7/+c6+Kr5hI2YA==", - "dependencies": [ - "@octokit/auth-oauth-app", - "@octokit/auth-oauth-user", - "@octokit/auth-unauthenticated", - "@octokit/core", - "@octokit/oauth-authorization-url", - "@octokit/oauth-methods", - "@types/aws-lambda", - "universal-user-agent" - ] - }, - "@octokit/oauth-authorization-url@7.1.1": { - "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==" - }, - "@octokit/oauth-methods@5.1.5": { - "integrity": "sha512-Ev7K8bkYrYLhoOSZGVAGsLEscZQyq7XQONCBBAl2JdMg7IT3PQn/y8P0KjloPoYpI5UylqYrLeUcScaYWXwDvw==", - "dependencies": [ - "@octokit/oauth-authorization-url", - "@octokit/request", - "@octokit/request-error", - "@octokit/types" - ] - }, - "@octokit/openapi-types@25.1.0": { - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==" - }, - "@octokit/openapi-webhooks-types@11.0.0": { - "integrity": "sha512-ZBzCFj98v3SuRM7oBas6BHZMJRadlnDoeFfvm1olVxZnYeU6Vh97FhPxyS5aLh5pN51GYv2I51l/hVUAVkGBlA==" - }, - "@octokit/plugin-paginate-graphql@5.2.4_@octokit+core@6.1.6": { - "integrity": "sha512-pLZES1jWaOynXKHOqdnwZ5ULeVR6tVVCMm+AUbp0htdcyXDU95WbkYdU4R2ej1wKj5Tu94Mee2Ne0PjPO9cCyA==", - "dependencies": [ - "@octokit/core" - ] - }, - "@octokit/plugin-paginate-rest@12.0.0_@octokit+core@6.1.6": { - "integrity": "sha512-MPd6WK1VtZ52lFrgZ0R2FlaoiWllzgqFHaSZxvp72NmoDeZ0m8GeJdg4oB6ctqMTYyrnDYp592Xma21mrgiyDA==", - "dependencies": [ - "@octokit/core", - "@octokit/types" - ] - }, - "@octokit/plugin-rest-endpoint-methods@14.0.0_@octokit+core@6.1.6": { - "integrity": "sha512-iQt6ovem4b7zZYZQtdv+PwgbL5VPq37th1m2x2TdkgimIDJpsi2A6Q/OI/23i/hR6z5mL0EgisNR4dcbmckSZQ==", - "dependencies": [ - "@octokit/core", - "@octokit/types" - ] - }, - "@octokit/plugin-retry@7.2.1_@octokit+core@6.1.6": { - "integrity": "sha512-wUc3gv0D6vNHpGxSaR3FlqJpTXGWgqmk607N9L3LvPL4QjaxDgX/1nY2mGpT37Khn+nlIXdljczkRnNdTTV3/A==", - "dependencies": [ - "@octokit/core", - "@octokit/request-error", - "@octokit/types", - "bottleneck" - ] - }, - "@octokit/plugin-throttling@10.0.0_@octokit+core@6.1.6": { - "integrity": "sha512-Kuq5/qs0DVYTHZuBAzCZStCzo2nKvVRo/TDNhCcpC2TKiOGz/DisXMCvjt3/b5kr6SCI1Y8eeeJTHBxxpFvZEg==", - "dependencies": [ - "@octokit/core", - "@octokit/types", - "bottleneck" - ] - }, - "@octokit/request-error@6.1.8": { - "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", - "dependencies": [ - "@octokit/types" - ] - }, - "@octokit/request@9.2.4": { - "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", - "dependencies": [ - "@octokit/endpoint", - "@octokit/request-error", - "@octokit/types", - "fast-content-type-parse", - "universal-user-agent" - ] - }, - "@octokit/types@14.1.0": { - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "dependencies": [ - "@octokit/openapi-types" - ] - }, - "@octokit/webhooks-methods@5.1.1": { - "integrity": "sha512-NGlEHZDseJTCj8TMMFehzwa9g7On4KJMPVHDSrHxCQumL6uSQR8wIkP/qesv52fXqV1BPf4pTxwtS31ldAt9Xg==" - }, - "@octokit/webhooks@13.9.1": { - "integrity": "sha512-Nss2b4Jyn4wB3EAqAPJypGuCJFalz/ZujKBQQ5934To7Xw9xjf4hkr/EAByxQY7hp7MKd790bWGz7XYSTsHmaw==", - "dependencies": [ - "@octokit/openapi-webhooks-types", - "@octokit/request-error", - "@octokit/webhooks-methods" - ] - }, - "@types/aws-lambda@8.10.163": { - "integrity": "sha512-+4zuoEB3S8RIhimtOFT7zAEk2SbpwrKjjGl9CyYnQR6k08uynxfauIIB0pDU1ssr05F8oyGiETwpn+8eXZMqPw==" - }, - "before-after-hook@3.0.2": { - "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==" - }, - "bottleneck@2.19.5": { - "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" - }, - "fast-content-type-parse@2.0.1": { - "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==" - }, - "octokit@4.1.4": { - "integrity": "sha512-cRvxRte6FU3vAHRC9+PMSY3D+mRAs2Rd9emMoqp70UGRvJRM3sbAoim2IXRZNNsf8wVfn4sGxVBHRAP+JBVX/g==", - "dependencies": [ - "@octokit/app", - "@octokit/core", - "@octokit/oauth-app", - "@octokit/plugin-paginate-graphql", - "@octokit/plugin-paginate-rest", - "@octokit/plugin-rest-endpoint-methods", - "@octokit/plugin-retry", - "@octokit/plugin-throttling", - "@octokit/request-error", - "@octokit/types", - "@octokit/webhooks" - ] - }, - "toad-cache@3.7.4": { - "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==" - }, - "universal-github-app-jwt@2.2.2": { - "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==" - }, - "universal-user-agent@7.0.3": { - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==" - } - }, - "workspace": { - "dependencies": [ - "jsr:@std/cli@1", - "jsr:@std/collections@1", - "jsr:@std/front-matter@1", - "jsr:@std/fs@1", - "jsr:@std/path@1", - "npm:octokit@4" - ] - } -} diff --git a/scripts/lib/cycle.ts b/scripts/lib/cycle.ts deleted file mode 100644 index f043bcf..0000000 --- a/scripts/lib/cycle.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { join } from '@std/path' -import { run } from './run.ts' - -export type CycleEntry = { - name: string - version: string - tag: string - changelog: string -} - -type Pkg = { - name?: string - version?: string - private?: boolean -} - -export const loadWorkspaceCycle = async (): Promise => { - const pkgs = JSON.parse(await run('pnpm', ['ls', '-r', '--json', '--depth=-1'])) as Pkg[] - const remote = new Set( - (await run('git', ['ls-remote', '--tags', 'origin'])) - .split('\n') - .filter(Boolean) - .map((line) => line.replace(/.*refs\/tags\//, '').replace(/\^\{\}$/, '')), - ) - const cycle: CycleEntry[] = [] - for (const pkg of pkgs) { - if (!pkg.name || !pkg.version || pkg.private) continue - const tag = `${pkg.name}@v${pkg.version}` - if (remote.has(tag)) continue - cycle.push({ - name: pkg.name, - version: pkg.version, - tag, - changelog: join('.changeset', 'changelogs', `${pkg.name.replace('/', '!')}@${pkg.version}.md`), - }) - } - return cycle -} - -export const loadCaptured = async (path: string): Promise => { - const raw: unknown = JSON.parse(await Deno.readTextFile(path)) - if (!Array.isArray(raw)) throw new Error('captured file must be a JSON array') - return raw as CycleEntry[] -} - -export const unpublishedOf = async (cycle: CycleEntry[]) => { - const published = await Promise.all( - cycle.map(async (entry) => { - const res = await fetch(`https://registry.npmjs.org/${entry.name.replace('/', '%2F')}/${entry.version}`) - return res.ok - }), - ) - return cycle.filter((_, i) => !published[i]) -} diff --git a/scripts/lib/run.ts b/scripts/lib/run.ts deleted file mode 100644 index e7295fa..0000000 --- a/scripts/lib/run.ts +++ /dev/null @@ -1,14 +0,0 @@ -const dec = new TextDecoder() -const enc = new TextEncoder() - -export const run = async (cmd: string, args: string[]) => { - const out = await new Deno.Command(cmd, { args, stdout: 'piped', stderr: 'inherit' }).output() - const stdout = dec.decode(out.stdout) - if (!out.success) { - if (stdout.length > 0) { - Deno.stderr.writeSync(enc.encode(stdout.endsWith('\n') ? stdout : `${stdout}\n`)) - } - throw new Error(`${cmd} ${args.join(' ')} failed (exit ${out.code})\n${stdout}`) - } - return stdout -} diff --git a/scripts/open-release-pr.sh b/scripts/open-release-pr.sh deleted file mode 100755 index 8300e9c..0000000 --- a/scripts/open-release-pr.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -: "${BRANCH:?}" -: "${BASE:?}" -: "${GH_TOKEN:?}" - -existing=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty') - -if [ -z "$(git status --porcelain)" ]; then - echo "no pending change intents — nothing to release" - if [ -n "$existing" ]; then - gh pr close "$existing" --delete-branch --comment "No pending change intents remain." - fi - exit 0 -fi - -git config user.name 'github-actions[bot]' -git config user.email '41898282+github-actions[bot]@users.noreply.github.com' -git switch --force-create "$BRANCH" -git add -A -- packages .changeset pnpm-lock.yaml -git commit -m 'chore(release): version packages' -git push --force origin "$BRANCH" - -body=$(mktemp) -trap 'rm -f "$body"' EXIT -cat > "$body" <<'BODY' -Consumes pending `.changeset/` intents via `pnpm version -r`. - -Merging runs the gate, then builds, publishes (OIDC + provenance), -and tags the changed packages. - -Review every consumed `none` intent before merging — a `none` on a -behavior-visible change is a silent non-release. - -Packages not registered as npm trusted publishers fail at publish -with an OIDC auth error; register them at https://www.npmjs.com -against workflow `release.yml` in this repository. -BODY - -gh label create release \ - --color 0E8A16 \ - --description 'Automated version-packages release PR' \ - --force - -if [ -n "$existing" ]; then - gh pr edit "$existing" \ - --title 'chore(release): version packages' \ - --body-file "$body" \ - --add-label release -else - gh pr create \ - --base "$BASE" \ - --head "$BRANCH" \ - --title 'chore(release): version packages' \ - --body-file "$body" \ - --label release -fi diff --git a/scripts/plan-release.ts b/scripts/plan-release.ts deleted file mode 100755 index ecf83ec..0000000 --- a/scripts/plan-release.ts +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env -S deno run --config=scripts/deno.json --allow-read --allow-write --allow-run=git,pnpm --allow-import --allow-net=jsr.io - -import { parseArgs } from '@std/cli/parse-args' -import { expandGlob } from '@std/fs/expand-glob' -import { basename } from '@std/path' -import { loadWorkspaceCycle } from './lib/cycle.ts' - -let pending = 0 -for await (const entry of expandGlob('.changeset/*.md')) { - if (basename(entry.path) !== 'README.md') pending++ -} - -const owed = (await loadWorkspaceCycle()).length -const phase = owed > 0 ? 'publish' : pending > 0 ? 'version' : 'none' -const outputs = [`phase=${phase}`, `pending_intents=${pending}`, `this_cycle=${owed}`].join('\n') - -console.error(`plan-release: pending_intents=${pending} this_cycle=${owed} -> phase=${phase}`) - -const { output } = parseArgs(Deno.args, { string: ['output'] }) -if (output) await Deno.writeTextFile(output, `${outputs}\n`, { append: true }) -else console.log(outputs) diff --git a/scripts/tag-released-packages.ts b/scripts/tag-released-packages.ts deleted file mode 100755 index d4a5e37..0000000 --- a/scripts/tag-released-packages.ts +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env -S deno run --config=scripts/deno.json --allow-read --allow-write --allow-run=git,pnpm --allow-net=jsr.io,registry.npmjs.org --allow-import - -import { parseArgs } from '@std/cli/parse-args' -import { loadCaptured, loadWorkspaceCycle, unpublishedOf } from './lib/cycle.ts' -import { run } from './lib/run.ts' - -const flags = parseArgs(Deno.args, { - boolean: ['dry-run', 'json', 'unpublished', 'publish'], - string: ['output', 'captured'], -}) - -const loaded = flags.captured ? await loadCaptured(flags.captured) : await loadWorkspaceCycle() -const cycle = flags.unpublished ? await unpublishedOf(loaded) : loaded - -if (flags.publish) { - if (cycle.length === 0) { - console.log('every captured version is already on npm — tagging only') - Deno.exit(0) - } - console.log(`publishing ${cycle.map((entry) => `${entry.name}@${entry.version}`).join(', ')}`) - const published = await new Deno.Command('pnpm', { - args: ['publish', '-r', '--provenance', '--access', 'public', '--no-git-checks'], - stdout: 'inherit', - stderr: 'inherit', - }).output() - if (!published.success) { - console.error( - `::error::pnpm publish -r --provenance --access public --no-git-checks failed (exit ${published.code})`, - ) - Deno.exit(published.code || 1) - } - Deno.exit(0) -} - -if (flags.output) { - await Deno.writeTextFile(flags.output, JSON.stringify(cycle, null, 2)) - console.error(`wrote ${cycle.length} captured package(s) to ${flags.output}`) -} - -if (flags.json) { - console.log(JSON.stringify(cycle)) - Deno.exit(0) -} - -if (flags['dry-run'] || flags.output) { - for (const { tag } of cycle) console.log(`would tag ${tag}`) - console.log(`dry run: ${cycle.length} tag(s)`) - Deno.exit(0) -} - -if (cycle.length === 0) { - console.log('no new tags to push') - Deno.exit(0) -} - -const made: string[] = [] -for (const { tag } of cycle) { - await run('git', ['tag', tag]) - made.push(tag) -} -await run('git', ['push', 'origin', ...made.map((t) => `refs/tags/${t}`)]) -console.log(`pushed ${made.length} tag(s): ${made.join(', ')}`) From fe4bed04b70ddd8cc173bbc4122ddf89ea943fb5 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Tue, 6 Oct 2026 18:11:25 -0400 Subject: [PATCH 2/3] chore(release): add none intent for the tooling change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared changeset gate flags the publishable package as touched by this CI/tooling change; record a none intent so the gate passes without a version bump — a script-only, release-nothing touch is the canonical none class --- .changeset/unify-release-tooling.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/unify-release-tooling.md diff --git a/.changeset/unify-release-tooling.md b/.changeset/unify-release-tooling.md new file mode 100644 index 0000000..5302a43 --- /dev/null +++ b/.changeset/unify-release-tooling.md @@ -0,0 +1,5 @@ +--- +"@systemfsoftware/omp-claude-compat": none +--- + +Switch the release pipeline to the shared pnpm-release-management toolchain (reusable workflows + release.jsonc) and drop npm-registry publishing. Tooling/CI only — no change to the published package, so this releases nothing From a7117fb3cf9cfa45b261fa5b6993bf29143396a1 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Tue, 6 Oct 2026 18:16:57 -0400 Subject: [PATCH 3/3] fix(release): use paths gate and format release.jsonc The shared changeset-check and plan jobs run the gate without installing this repo's deps, so a turbo gate fails on a missing turbo pin. Switch the gate strategy to paths (pure git change evidence, no turbo needed). Drop the none intent: a paths gate does not mark the package touched for a root-only tooling change, so no intent is owed. Also commit the dprint-formatted release.jsonc (trailing commas) that the prior commit missed --- .changeset/unify-release-tooling.md | 5 ----- release.jsonc | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 .changeset/unify-release-tooling.md diff --git a/.changeset/unify-release-tooling.md b/.changeset/unify-release-tooling.md deleted file mode 100644 index 5302a43..0000000 --- a/.changeset/unify-release-tooling.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@systemfsoftware/omp-claude-compat": none ---- - -Switch the release pipeline to the shared pnpm-release-management toolchain (reusable workflows + release.jsonc) and drop npm-registry publishing. Tooling/CI only — no change to the published package, so this releases nothing diff --git a/release.jsonc b/release.jsonc index 43f4ca3..75a6450 100644 --- a/release.jsonc +++ b/release.jsonc @@ -13,7 +13,7 @@ "changesetDir": ".changeset", "changelogDir": ".changeset/changelogs", "versioning": { - "strategy": "pnpm" + "strategy": "pnpm", }, - "gate": { "strategy": "turbo", "task": "build" } + "gate": { "strategy": "paths" }, }