Skip to content
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
11 changes: 11 additions & 0 deletions .github/pinact.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# pinact (https://github.com/suzuki-shunsuke/pinact) pins third-party actions to
# a commit SHA and updates them: `pinact run --update`. See CONTRIBUTING.md.
version: 3
# Skip versions published in the last week, like `pnpm dep:check`.
min_age:
value: 7
rules:
# GitHub's own actions stay on version tags (see .github/zizmor.yml).
- ignore: true
conditions:
- expr: ActionRepoOwner in ["actions", "github"]
27 changes: 25 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,27 @@ on:
permissions: {}

jobs:
# Security audit of the workflows (.github/zizmor.yml), including that
# third-party actions are pinned to a commit SHA. Same config as
# `pnpm lint:gha`.
lint-gha:
name: Lint GitHub Actions
runs-on: ubuntu-latest
permissions:
contents: read # to check out repo (actions/checkout)
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
with:
version: 1.31.1
# The default, set explicitly: online audits include impostor-commit,
# which checks that each SHA pin is a commit in the action's own repo.
online-audits: true
advanced-security: false
annotations: true

test:
name: Test on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -96,9 +117,11 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Changes the working tree only; nothing is committed.
# Changes the working tree only; nothing is committed. Outside the
# lockfile on purpose, but still under `minimumReleaseAge`, and the job
# has no secrets and read-only permissions.
- name: Install React 18.0.0 and Preact 10
run: |
run: | # zizmor: ignore[adhoc-packages]
pnpm add --save-dev react@18.0.0 react-dom@18.0.0 preact@^10
node -e 'for (const [p, v] of [["react", "18.0.0"], ["react-dom", "18.0.0"], ["preact", "10."]]) { const got = require(`${p}/package.json`).version; console.log(`${p} ${got}`); if (!got.startsWith(v)) throw new Error(`${p} is ${got}, expected ${v}`); }'
- name: Test
Expand Down
12 changes: 12 additions & 0 deletions .github/zizmor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# zizmor (https://docs.zizmor.sh) audits GitHub Actions workflows: `pnpm lint:gha`
# locally, and the lint job in ci.yml.
rules:
unpinned-uses:
config:
# GitHub's own actions use version tags. Third-party actions are pinned
# to a commit SHA, with the version in a trailing comment. Refresh them
# with pinact (see CONTRIBUTING.md).
policies:
actions/*: ref-pin
github/*: ref-pin
'*': hash-pin
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,33 @@ $ pnpm run lint:fix

`index.js` stays ES5 in 3.x, so don't apply ES2015+ fixes (`let`/`const`, arrow functions) to it.

### GitHub Actions

Workflows reference GitHub's own actions (`actions/*`, `github/*`) by major
version tag, such as `actions/checkout@v7`. Third-party actions are pinned to a
commit SHA with the version in a comment, such as
`pnpm/action-setup@<sha> # v6.1.0`, because a tag can be moved to different
code. [zizmor](https://docs.zizmor.sh) enforces this (`.github/zizmor.yml`)
along with its other security checks.

CI runs zizmor in the `lint-gha` job, at the `version` pinned there.
`pnpm lint:gha` (part of `pnpm check`) runs it locally if it's installed, and
skips otherwise. Newer zizmor releases add audits, so a different version
locally can report findings that CI doesn't. To match CI, install that version,
e.g. `uv tool install zizmor==1.31.1` (or `brew install zizmor` for the
latest). Set `GH_TOKEN` to include zizmor's online audits.

To update actions:

- Third-party actions: run `pinact run --update`
([pinact](https://github.com/suzuki-shunsuke/pinact), `brew install pinact`).
It updates SHAs and their version comments, skipping versions published in
the last week (`.github/pinact.yml`). Review each action's release notes.
- GitHub's actions: check each one's releases page for a new major and read its
breaking changes.
- zizmor itself: bump `version` on the zizmor step in
`.github/workflows/ci.yml`.

### Published package

`pnpm pack:check` (part of `pnpm check`) packs the package and checks what it publishes:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
"main": "index.js",
"typings": "index.d.ts",
"scripts": {
"check": "pnpm format && pnpm lint && pnpm types:check && pnpm test && pnpm pack:check",
"check": "pnpm format && pnpm lint && pnpm lint:gha && pnpm types:check && pnpm test && pnpm pack:check",
"benchmark": "node benchmark",
"format": "oxfmt --check",
"format:fix": "oxfmt",
"lint": "oxlint --report-unused-disable-directives-severity=error",
"lint:fix": "oxlint --report-unused-disable-directives-severity=error --fix",
"lint:gha": "node scripts/lint-gha.mjs",
"types:check": "node scripts/types-check.mjs",
"test": "vitest run",
"changeset": "changeset",
Expand Down
15 changes: 15 additions & 0 deletions scripts/lint-gha.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Runs zizmor (https://docs.zizmor.sh) on .github/ with .github/zizmor.yml.
// zizmor isn't a Node tool, so this skips with a hint if it isn't installed;
// CI runs it in the lint-gha job (ci.yml).
import { spawnSync } from 'node:child_process';

const result = spawnSync('zizmor', ['.'], { stdio: 'inherit' });

if (result.error?.code === 'ENOENT') {
process.stdout.write(
"zizmor is not installed, skipping (CI runs it). To match CI's version, see CONTRIBUTING.md.\n"
);
process.exit(0);
}
if (result.error) throw result.error;
process.exit(result.status ?? 1);
Loading