From e42858cf1475c41793f74da1014f472d20d2ccd1 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Sun, 27 Sep 2026 21:10:42 -0400 Subject: [PATCH 1/4] ci(release): make the Changesets version PR pass its own required checks --- .changeset/config.json | 2 +- .changeset/release-ci-triggering.md | 4 +++ .github/workflows/changesets.yml | 6 +++++ scripts/changeset-commit-message.cjs | 37 ++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .changeset/release-ci-triggering.md create mode 100644 scripts/changeset-commit-message.cjs diff --git a/.changeset/config.json b/.changeset/config.json index af4947d3d..1552ebb3f 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -6,7 +6,7 @@ "repo": "cameri/nostream" } ], - "commit": true, + "commit": ["../scripts/changeset-commit-message.cjs", { "skipCI": false }], "privatePackages": { "tag": true }, diff --git a/.changeset/release-ci-triggering.md b/.changeset/release-ci-triggering.md new file mode 100644 index 000000000..a4e4fe147 --- /dev/null +++ b/.changeset/release-ci-triggering.md @@ -0,0 +1,4 @@ +--- +--- + +Make the Changesets version PR pass its own required checks: give its commit message a conventional subject and drop the `[skip ci]` that suppressed every workflow run for it. diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml index 66edfc3bd..4eed2da40 100644 --- a/.github/workflows/changesets.yml +++ b/.github/workflows/changesets.yml @@ -16,10 +16,16 @@ jobs: contents: write pull-requests: write steps: + # The default GITHUB_TOKEN must not be the credential behind the version + # branch push: GitHub starts no workflow runs for events a GITHUB_TOKEN + # creates, so the version PR would never run its own required checks. + # RELEASE_PAT is the environment secret this job already hands to the + # action for its API calls; the push needs the same credential. - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 + token: ${{ secrets.RELEASE_PAT }} - uses: pnpm/action-setup@v4 diff --git a/scripts/changeset-commit-message.cjs b/scripts/changeset-commit-message.cjs new file mode 100644 index 000000000..4951b60ba --- /dev/null +++ b/scripts/changeset-commit-message.cjs @@ -0,0 +1,37 @@ +/** + * Conventional-commit messages for Changesets. + * + * Changesets' stock messages are `RELEASING: Releasing N package(s)` with a + * trailing `[skip ci]`. Both halves break the version PR this repo's release + * flow depends on: + * + * - `Lint commits` (commitlint, `@commitlint/config-conventional`) rejects + * `RELEASING:` as an unknown type, so a required check fails on a message no + * human wrote. It only ever looked green because a `workflow_dispatch` run + * carries no pull request context and returns a vacuous pass. + * - `[skip ci]` makes GitHub skip both push and pull_request runs for that + * commit, so the version PR's other five required checks never report either + * and the pull request cannot satisfy the branch's required-status-checks + * ruleset. Replacing the head commit with a `[skip ci]`-free one is the only + * thing that made those checks run. + * + * `changeset add` keeps the stock `docs(changeset): ` subject. The + * version commit becomes `chore: release @`. `skipCI` is + * still honoured, so the documented option keeps meaning what it says. + */ + +const skipCIMarker = (options, phase) => + options?.skipCI === true || options?.skipCI === phase ? '\n\n[skip ci]\n' : '' + +module.exports = { + getAddMessage: (changeset, options) => `docs(changeset): ${changeset.summary}${skipCIMarker(options, 'add')}`, + + getVersionMessage: (releasePlan, options) => { + const released = releasePlan.releases.filter((release) => release.type !== 'none') + const subject = released.length + ? `release ${released.map((release) => `${release.name}@${release.newVersion}`).join(', ')}` + : 'version packages' + + return `chore: ${subject}${skipCIMarker(options, 'version')}` + }, +} From efae4d2d7592b00648ca4870bcd312d129055560 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Sun, 27 Sep 2026 21:13:43 -0400 Subject: [PATCH 2/4] test(release): assert the version commit stays conventional and free of [skip ci] --- .../unit/ci/changesets-commit-message.spec.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 test/unit/ci/changesets-commit-message.spec.ts diff --git a/test/unit/ci/changesets-commit-message.spec.ts b/test/unit/ci/changesets-commit-message.spec.ts new file mode 100644 index 000000000..916661c35 --- /dev/null +++ b/test/unit/ci/changesets-commit-message.spec.ts @@ -0,0 +1,65 @@ +import { readFileSync } from 'fs' +import { createRequire } from 'module' +import { join } from 'path' + +import chai from 'chai' + +const { expect } = chai + +type CommitModule = { + getAddMessage: (changeset: { summary: string }, options: unknown) => string + getVersionMessage: (releasePlan: { releases: unknown[] }, options: unknown) => string +} + +describe('changesets commit message', () => { + const config = JSON.parse(readFileSync(join(process.cwd(), '.changeset', 'config.json'), 'utf-8')) as { + commit: unknown + } + + // createRequire works whether mocha loads this spec as CommonJS or as an ES + // module, which `require` itself does not. + const loadFromRepo = createRequire(join(process.cwd(), 'index.js')) + + // Changesets resolves the specifier in the `commit` tuple as if it were + // imported from `/.changeset/x.mjs`, so that is the base used here too. + const commitModule = (): CommitModule => { + expect(config.commit, '`commit` in .changeset/config.json must be a [module, options] tuple').to.be.an('array') + const specifier = (config.commit as unknown[])[0] + expect(specifier, 'that tuple must name a module').to.be.a('string') + + return loadFromRepo(join(process.cwd(), '.changeset', specifier as string)) as CommitModule + } + + const releasePlan = { + releases: [ + { name: 'nostream', type: 'minor', oldVersion: '3.0.0', newVersion: '3.1.0' }, + { name: 'nostream-extra', type: 'none', oldVersion: '1.0.0', newVersion: '1.0.0' }, + ], + } + + // The stock message is `RELEASING: Releasing N package(s)`, which commitlint + // rejects for type-case and type-enum. + it('writes a conventional subject', () => { + expect(commitModule().getVersionMessage(releasePlan, { skipCI: false })).to.equal('chore: release nostream@3.1.0') + }) + + // `[skip ci]` makes GitHub skip both push and pull_request runs for the + // commit, which is why the version PR's required checks never reported. + it('writes no [skip ci]', () => { + expect(commitModule().getVersionMessage(releasePlan, { skipCI: false })).not.to.include('skip ci') + }) + + it('names only the packages that are actually released', () => { + expect(commitModule().getVersionMessage(releasePlan, { skipCI: false })).not.to.include('nostream-extra') + }) + + it('still honours skipCI when it is asked for', () => { + expect(commitModule().getVersionMessage(releasePlan, { skipCI: true })).to.include('[skip ci]') + }) + + it('keeps changeset add conventional', () => { + expect(commitModule().getAddMessage({ summary: 'a summary' }, { skipCI: false })).to.equal( + 'docs(changeset): a summary', + ) + }) +}) From ab195e3ac39b0f7e5da6fab983f69d7302337bb1 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Sun, 27 Sep 2026 21:17:11 -0400 Subject: [PATCH 3/4] chore: trigger ci for the pull request From 453af6e2a97d57e9d5c0b8796147815ba787126c Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Sun, 27 Sep 2026 21:22:40 -0400 Subject: [PATCH 4/4] test(release): assert the version message against the configured skipCI --- test/unit/ci/changesets-commit-message.spec.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/unit/ci/changesets-commit-message.spec.ts b/test/unit/ci/changesets-commit-message.spec.ts index 916661c35..63e464f30 100644 --- a/test/unit/ci/changesets-commit-message.spec.ts +++ b/test/unit/ci/changesets-commit-message.spec.ts @@ -30,6 +30,11 @@ describe('changesets commit message', () => { return loadFromRepo(join(process.cwd(), '.changeset', specifier as string)) as CommitModule } + // The options Changesets actually passes to the module, so a change to them + // (say `skipCI: true`) cannot quietly put `[skip ci]` back on the version + // commit without failing these assertions. + const configuredOptions = (config.commit as unknown[])[1] ?? null + const releasePlan = { releases: [ { name: 'nostream', type: 'minor', oldVersion: '3.0.0', newVersion: '3.1.0' }, @@ -40,17 +45,17 @@ describe('changesets commit message', () => { // The stock message is `RELEASING: Releasing N package(s)`, which commitlint // rejects for type-case and type-enum. it('writes a conventional subject', () => { - expect(commitModule().getVersionMessage(releasePlan, { skipCI: false })).to.equal('chore: release nostream@3.1.0') + expect(commitModule().getVersionMessage(releasePlan, configuredOptions)).to.equal('chore: release nostream@3.1.0') }) // `[skip ci]` makes GitHub skip both push and pull_request runs for the // commit, which is why the version PR's required checks never reported. it('writes no [skip ci]', () => { - expect(commitModule().getVersionMessage(releasePlan, { skipCI: false })).not.to.include('skip ci') + expect(commitModule().getVersionMessage(releasePlan, configuredOptions)).not.to.include('skip ci') }) it('names only the packages that are actually released', () => { - expect(commitModule().getVersionMessage(releasePlan, { skipCI: false })).not.to.include('nostream-extra') + expect(commitModule().getVersionMessage(releasePlan, configuredOptions)).not.to.include('nostream-extra') }) it('still honours skipCI when it is asked for', () => {