Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,22 @@ updates:
- "minor"
- "patch"

# Enable version updates for GitHub Actions
# Enable version updates for GitHub Actions. Weekly and grouped into one
# pull request: the third-party actions that hold a credential are pinned
# by commit sha, so a release reaches them only through this update.
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
interval: "weekly"
groups:
# Majors stay separate pull requests: one declined major (the
# changesets/action v1 pin is deliberate) must not hold the group.
github-actions:
patterns:
- "*"
update-types:
- "minor"
- "patch"
labels:
- "dependencies"
- "github-actions"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ jobs:
# shells out to pnpm, and this workflow pins no toolchain.

- name: Generate changelog
uses: orhun/git-cliff-action@v4
uses: orhun/git-cliff-action@a9a95522b26fe6403f7bb24031f21fb573d0f5ff # v4.9.1
id: changelog
with:
config: cliff.toml
Expand Down
24 changes: 17 additions & 7 deletions .github/workflows/changeset-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -880,9 +880,9 @@ jobs:
# plausibly the shape that makes the clear step unnecessary — which is an
# argument for revisiting the pin eventually, not for bumping it blind.
#
# ⛔ WHAT NOTICES A BUMP. This `@v1` ref is pinned by exactly one thing:
# ⛔ WHAT NOTICES A BUMP. This v1 ref is pinned by exactly one thing:
# `scripts/__tests__/changeset-release-action-ref-pin.test.ts`. It fails on
# a bump of EITHER `uses: changesets/action@v1` line in this file and names
# a bump of EITHER `uses: changesets/action@...` line in this file and names
# the two properties above as what a bump must re-establish. There is no
# second guard — the type checker and the linters do not read this file at
# all — so ⛔ do not delete that test as collateral of some other cleanup.
Expand All @@ -895,9 +895,19 @@ jobs:
# `CLAUDE.md` / `GEMINI.md` and on `.changeset/pre/`). objectui#6081
# deleted that mirror, because the bundled copy is a minified chunk behind
# a MOVING tag and cannot be pinned from here at any effort — a retag of
# `v1` in place changed it and nothing here failed. A ref pin cannot catch
# a retag either, but it does not pretend to: it catches the DELIBERATE
# bump, which is the only moment anyone can act on.
# `v1` in place changed it and nothing here failed. A ref pin catches the
# DELIBERATE bump, which is the only moment anyone can act on.
#
# ⭐ THE REF IS A COMMIT SHA, NOT THE MOVING `v1`. Both `uses:` lines name
# `a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d`, the commit the `v1` branch
# and the `v1.9.0` tag both resolved to when this was written, and the
# same commit the git-surface reading above was taken at. This step runs
# with `NPM_TOKEN`, so a retag of `v1` would otherwise hand the publish
# credential to code nobody here has read; a sha cannot move. A bump is
# still a deliberate edit of both lines to another sha (the trailing
# `# vX.Y.Z` comment names the release it is), and it still has to
# re-establish the two properties above. The sha is a declared exception
# to `scripts/check-action-ref-convention.mjs`'s floating-tag default.

# ⚠️ THE STEP THAT LETS THE PREDICATE REACH THE ACTION (objectui#5442).
#
Expand Down Expand Up @@ -1019,7 +1029,7 @@ jobs:
- name: Publish to npm
id: changesets-publish
if: github.event_name == 'push'
uses: changesets/action@v1
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
with:
publish: pnpm changeset:publish
env:
Expand Down Expand Up @@ -1437,7 +1447,7 @@ jobs:
- name: Refresh the version PR
id: changesets-version
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
uses: changesets/action@v1
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
with:
version: pnpm changeset:version
title: 'chore: release packages'
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/check-links.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ on:
# - main
# pull_request:

# Least privilege: the job checks out and reads public pages. The token lychee
# receives is for GitHub's API rate limit on link checks, a read.
permissions:
contents: read

jobs:
check-links:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -145,7 +150,7 @@ jobs:
uses: actions/checkout@v7

- name: Check links with Lychee
uses: lycheeverse/lychee-action@v2
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
with:
# Both trees:
# content/docs/** — the published site's documentation (the fumadocs
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ on:
# which on a queue build is the queue's own generation ref
# (`gh-readonly-queue/main/pr-<n>-<sha>`) — colliding with
# neither a PR group nor a push group, exactly as before.
# Least privilege for the job token. Every job without its own `permissions:`
# block (changeset-check, type-check, test, test-dist-pins, test-coverage,
# e2e, docs) only checks out, builds, tests, and saves caches and artifacts;
# none of those calls the API with this token (actions/cache and
# actions/upload-artifact use the runner's own token), so `contents: read` is
# all they get. The two jobs that read the Actions API (test-aggregate,
# coverage-report) declare their own block, which replaces this one.
permissions:
contents: read

concurrency:
group: >-
ci-${{ github.workflow }}-${{ github.event.pull_request.number
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ jobs:

- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v3
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"

Expand Down
44 changes: 31 additions & 13 deletions scripts/__tests__/changeset-release-action-ref-pin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,30 @@ import { parse as parseYaml } from 'yaml';
* on that ref. Move the assertion somewhere that survives first — do not just
* drop it.
*
* ## What this pin does NOT do
* ## The ref is a commit sha, so a retag cannot reach it
*
* It cannot catch a **retag of `v1` in place**. `changesets/action@v1` is a
* moving tag whose bundled `@changesets/read` is a minified chunk nothing here
* installs, so no test can execute it and no lockfile records it — that gap is
* objectui#6081's subject, and it is the reason the mirror was deleted rather
* than given a better referent. This pin catches the DELIBERATE bump, which is
* the only moment anyone can act on anyway.
* `changesets/action@v1` names a moving branch whose bundled `@changesets/read`
* is a minified chunk nothing here installs, so no test can execute it and no
* lockfile records it — that gap is objectui#6081's subject, and it is the
* reason the mirror was deleted rather than given a better referent. Both
* `uses:` lines now name the v1.9.0 commit instead, so a retag of `v1` in place
* no longer changes what runs; this pin catches the DELIBERATE bump, which is
* the only moment anyone can act on anyway, and a step that drifts back onto
* the moving `v1`.
*/
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const workflowPath = path.join(repoRoot, '.github/workflows/changeset-release.yml');
const workflowSource = fs.readFileSync(workflowPath, 'utf8');

const PINNED_REF = 'changesets/action@v1';
/**
* The v1 commit, not the moving `v1` branch: `v1` and the `v1.9.0` tag both
* resolved to this sha when the pin moved off the branch, and it is the commit
* the workflow's git-surface reading was taken at. The publish step runs with
* the npm token, so the ref it executes must not be able to move under it.
*/
const PINNED_SHA = 'a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d';
const PINNED_REF = `changesets/action@${PINNED_SHA}`;
const PINNED_LINE = ` uses: ${PINNED_REF} # v1.9.0`;

interface Step {
name?: string;
Expand Down Expand Up @@ -93,7 +103,7 @@ function changesetsActionSteps(source: string): Array<{ job: string; step: strin
return found;
}

describe('changeset-release.yml pins changesets/action to @v1', () => {
describe('changeset-release.yml pins changesets/action to the v1.9.0 commit', () => {
it('still uses the action at all — the pin is not vacuous', () => {
// Without this, every assertion below passes over a workflow that dropped
// the action entirely, which is the one way a "toContain" pin lies.
Expand Down Expand Up @@ -145,15 +155,15 @@ describe('the pin can fail (non-vacuity)', () => {
}

it('goes red when the publish step is bumped to @v2', () => {
const bumped = mutate(' uses: changesets/action@v1\n with:\n publish:', ' uses: changesets/action@v2\n with:\n publish:');
const bumped = mutate(`${PINNED_LINE}\n with:\n publish:`, ' uses: changesets/action@v2\n with:\n publish:');
const offenders = changesetsActionSteps(bumped).filter((s) => s.uses !== PINNED_REF);
expect(offenders).toEqual([
{ job: 'release', step: 'Publish to npm', uses: 'changesets/action@v2' },
]);
});

it('goes red when the refresh step is bumped to @v2', () => {
const bumped = mutate(' uses: changesets/action@v1\n with:\n version:', ' uses: changesets/action@v2\n with:\n version:');
const bumped = mutate(`${PINNED_LINE}\n with:\n version:`, ' uses: changesets/action@v2\n with:\n version:');
const offenders = changesetsActionSteps(bumped).filter((s) => s.uses !== PINNED_REF);
expect(offenders).toEqual([
{ job: 'release', step: 'Refresh the version PR', uses: 'changesets/action@v2' },
Expand All @@ -164,15 +174,23 @@ describe('the pin can fail (non-vacuity)', () => {
// The shape a real v2 migration takes: `changesets/action/publish@v2`. A
// pin that matched only the exact old string would find nothing here and
// pass.
const bumped = mutate(' uses: changesets/action@v1\n with:\n publish:', ' uses: changesets/action/publish@v2\n with:\n publish:');
const bumped = mutate(`${PINNED_LINE}\n with:\n publish:`, ' uses: changesets/action/publish@v2\n with:\n publish:');
const offenders = changesetsActionSteps(bumped).filter((s) => s.uses !== PINNED_REF);
expect(offenders).toEqual([
{ job: 'release', step: 'Publish to npm', uses: 'changesets/action/publish@v2' },
]);
});

it('goes red when the publish step goes back to the moving v1 branch', () => {
const floating = mutate(`${PINNED_LINE}\n with:\n publish:`, ' uses: changesets/action@v1\n with:\n publish:');
const offenders = changesetsActionSteps(floating).filter((s) => s.uses !== PINNED_REF);
expect(offenders).toEqual([
{ job: 'release', step: 'Publish to npm', uses: 'changesets/action@v1' },
]);
});

it('goes red when the action is dropped from the workflow entirely', () => {
const stripped = mutate(' uses: changesets/action@v1', ' uses: actions/checkout@v7');
const stripped = mutate(PINNED_LINE, ' uses: actions/checkout@v7');
expect(changesetsActionSteps(stripped).length).toBeLessThan(2);
});
});
39 changes: 39 additions & 0 deletions scripts/check-action-ref-convention.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,45 @@ export const DECLARED_EXCEPTIONS = [
'reviewed moment; a floating ref would re-adopt every upstream change on the next scheduled ' +
'run, with nothing in this repository to roll back to.',
},
// The four below share one reason, said once: each is a third-party action
// that runs holding a credential, so the code a floating tag resolves to
// would receive that credential on the next run after the tag moved, with
// nothing here reviewing it. A commit sha cannot move; the trailing
// `# vX.Y.Z` comment names the release it is. First-party `actions/*` refs
// stay on the floating-tag default.
{
workflow: 'changeset-release.yml',
action: 'changesets/action',
issue: 'objectui#13430',
reason:
'runs with NPM_TOKEN and GITHUB_TOKEN in the publish and version-PR steps; pinned to the ' +
'v1.9.0 commit, the one the workflow comment and the ref-pin test say both v1 properties ' +
'were read at (scripts/__tests__/changeset-release-action-ref-pin.test.ts holds the sha).',
},
{
workflow: 'check-links.yml',
action: 'lycheeverse/lychee-action',
issue: 'objectui#13430',
reason:
'receives GITHUB_TOKEN through its env and its token input default; pinned to the v2.9.0 ' +
'commit so the link checker cannot change under that token between reviews.',
},
{
workflow: 'dependabot-auto-merge.yml',
action: 'dependabot/fetch-metadata',
issue: 'objectui#13430',
reason:
'receives GITHUB_TOKEN as its github-token input in a job that approves and enqueues ' +
'Dependabot pull requests; pinned to the v3.1.0 commit.',
},
{
workflow: 'changelog.yml',
action: 'orhun/git-cliff-action',
issue: 'objectui#13430',
reason:
'defaults its github_token input to the job token, which holds contents: write in this ' +
'job; pinned to the v4.9.1 commit.',
},
];

/**
Expand Down
Loading