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
8 changes: 4 additions & 4 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ ast-grep's rules (`sgconfig.yml`, in `tools/ast-grep/rules/` with their cases in

`cargo remove` also deletes the root `Cargo.toml`'s `[profile.dev.package.*]` entries, with their comments, for every crate no member depends on directly: its clean-up after a removal ([rust-lang/cargo#11194](https://github.com/rust-lang/cargo/pull/11194), since Cargo 1.67) only keeps the entries for direct dependencies. `tools/policy.sh` fails while one is missing and its crate is still in `Cargo.lock`, so put it back with `git checkout -p Cargo.toml`. A new entry goes in `dev_optimised` in `tools/policy/cargo.rego` too.

CI (`ci.yml`) runs a tool's checks only when its files (or the shared ones) change, and ends with a "CI passed" check that reports either way, the check `main` requires before a merge. On a draft pull request, nothing runs until you mark it ready for review, so open a pull request as a draft while it isn't next to merge (a stacked one, say). "CI passed" fails on a draft, since none of its checks ran. Marking it ready starts a new run, and if one doesn't show up, re-run the workflow.
CI (`ci.yml`) runs a tool's checks only when its files (or the shared ones) change, and ends with a "CI passed" check, the one `main` requires before a merge. It's skipped, which counts as passing, unless a check failed. On a draft pull request, nothing runs until you mark it ready for review, so open a pull request as a draft while it isn't next to merge (a stacked one, say). "CI passed" fails on a draft, since none of its checks ran. Marking it ready starts a new run, and if one doesn't show up, use "Re-run all jobs" (re-running only the failed jobs keeps the draft's result). To re-run CI after `main` moves, merge `main` into the branch rather than closing and reopening the pull request.

Pull requests run on Linux. locallibrary's macOS and Windows checks run in the merge queue: once a pull request is approved and passing, add it to the queue, which runs them on it together with the pull requests ahead of it and merges it when they pass. To run them on a pull request first, add the `all-os` label and re-run CI. The slow suites run weekly in the `*-scheduled.yml` workflows, and you can start them on any branch from the Actions tab. A weekly run that fails opens an issue labelled `scheduled-failure`, and the next weekly run that passes closes it.
Pull requests run on Linux. locallibrary's macOS and Windows checks run in the merge queue: once a pull request is approved and passing, add it to the queue, which runs them on it together with the pull requests ahead of it and merges it when they pass. To run them on a pull request first, add the `all-os` label and re-run CI. A push to `main` runs only what pull requests use from it: the coverage and sizes they're compared with, the caches, CodeQL, and the ruleset check when the ruleset changed. The slow suites run weekly in the `*-scheduled.yml` workflows, each skipping the week when nothing it tests changed since its last run that passed, and you can start them on any branch from the Actions tab. A weekly run that fails opens an issue labelled `scheduled-failure`, and the next weekly run that passes closes it.

On a pull request, each tool's workflow keeps one comment up to date with its coverage: the Rust workspace's, with the Hegel property tests' alone beside it, and the website's and the extension's against `main`, with each changed file's. The website's and the extension's comments also show how the change moves what they ship against `main`: the website's client JavaScript and CSS, and the extension's zip. A pull request from a fork can't be commented on, so it finds the same in the run's summary. The website's Lighthouse scores are in the run's summary too, when the change touches what its pages are built from.

Pull requests also get a dependency review (`dependency-review.yml`). It fails when a dependency the pull request adds or updates (dev tooling aside) has a known advisory or a licence `deny.toml` doesn't allow (fonts may also be OFL-1.1), and then comments on the pull request with what it found.
Pull requests that change a lockfile, a manifest or a workflow also get a dependency review (`dependency-review.yml`). It fails when a dependency the pull request adds or updates (dev tooling aside) has a known advisory or a licence `deny.toml` doesn't allow (fonts may also be OFL-1.1), and then comments on the pull request with what it found.

CodeQL scans the workflows, the TypeScript and the Rust on every pull request and merge queue group (`codeql.yml`), and `main` won't take a pull request that adds a high or critical security alert, or an error. The Rust tests are left out (`.github/codeql/codeql-config.yml`), so a new `#[cfg(test)]` module needs one of the names listed there, which `ast-grep scan` checks.
CodeQL (`codeql.yml`, which `ci.yml` calls) scans the workflows on every pull request into `main`, merge queue group and push to `main`, and the TypeScript and the Rust when their code changed, and `main` won't take a pull request that adds a high or critical security alert, or an error. The Rust tests are left out (`.github/codeql/codeql-config.yml`), so a new `#[cfg(test)]` module needs one of the names listed there, which `ast-grep scan` checks.

## Pull requests

Expand Down
18 changes: 9 additions & 9 deletions .github/NEW_PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This covers what the repository runs on and every place a new project has to be

Most of the steps are checked, so a missed one fails CI with a message that says what to add:

- `tools/policy.sh` (the Repo checks workflow) runs Rego policies in `tools/policy/` over the config files with [conftest](https://www.conftest.dev). They check each TypeScript app's scripts, `package.json` fields and git hooks; that every workflow on pull requests (but the benchmarks, the release plan and dependency review) has the `changes`, work and `passed` jobs and no `paths` in its trigger, that the workflows it calls have no concurrency group or push or pull request trigger and have report jobs that can't fail the call, and that one of them runs when each app, package, crate or plugin changes; that `.github/rulesets/protect-main.json` requires every `passed` check; that the labels, labeler and issue forms know every app and every file gets a label; that Rust packages share the workspace's settings and the dev profile keeps its entries for the crates in `Cargo.lock`; that lexicons are filed by NSID; and that an app with a Dockerfile has a Railway service in `.railway/railway.ts`, which has to type-check against the railway SDK. It needs Docker, and Node with the root's dev dependencies locally. The policies' own tests have to cover every line.
- `tools/policy.sh` (the Repo checks workflow) runs Rego policies in `tools/policy/` over the config files with [conftest](https://www.conftest.dev). They check each TypeScript app's scripts, `package.json` fields and git hooks; that every workflow on pull requests (but dependency review) has the `changes`, work and `passed` jobs and no `paths` in its trigger, that the workflows it calls have no concurrency group or push or pull request trigger and have report jobs that can't fail the call, that one of them runs when each app, package, crate or plugin changes, and that the `locallibrary` filter has every file the Rust code embeds from outside its crate; that `.github/rulesets/protect-main.json` requires every `passed` check; that the labels, labeler and issue forms know every app and every file gets a label; that Rust packages share the workspace's settings and the dev profile keeps its entries for the crates in `Cargo.lock`; that lexicons are filed by NSID; and that an app with a Dockerfile has a Railway service in `.railway/railway.ts`, which has to type-check against the railway SDK. It needs Docker, and Node with the root's dev dependencies locally. The policies' own tests have to cover every line.
- The website's tests check that every folder in `apps/` has a tool page and is named in the root README, CONTRIBUTING, SECURITY, the website's README and the `web: tools` label (`apps/web/src/lib/tools/docs.test.ts`).
- locallibrary's build fails while a folder in `plugins/` isn't one of its dependencies, or a folder in `plugins/accounts/` has no manifest (`apps/locallibrary/build.rs`), and its tests check that every one ships and is in its docs (`apps/locallibrary/tests/it/plugins.rs`).
- clippy's `cargo_common_metadata` wants full metadata on every crate that isn't `publish = false`, and the release workflow's plan fails while it would release anything but locallibrary.
Expand Down Expand Up @@ -65,13 +65,13 @@ Pull requests, merge queue groups and pushes to `main` run one workflow, `ci.yml

1. a `changes` job that runs [dorny/paths-filter](https://github.com/dorny/paths-filter) over each tool's files, including the shared ones it depends on (the lockfile, `.node-version`, the lint and format configs, its workflow), with one output per tool. On a push it diffs against the last commit the workflow passed on (`.github/actions/last-passed`), so a run that a newer push cancelled, or one that failed, leaves its files to the next run, and a merge queue group against the commit it goes on top of. A draft pull request runs nothing until it's marked ready for review;
2. a job per tool that calls its workflow, `<tool>-ci.yml`, when the tool's output is true. Copy `localtoolbox-ci.yml`, the smallest: it triggers only on `workflow_call`, has no concurrency group (a call gets the caller's, and would cancel it), and its jobs do the work;
3. a `passed` job named "CI passed", with `if: always()`, that fails if any call failed or was cancelled, was skipped while its output was true, or the pull request was a draft. A call's result covers every job in it, so a job skipped because one it needs didn't finish fails its tool's call.
3. a `passed` job named "CI passed" whose `if` is `always() && (...)`, with the conditions it fails on: any call failed or was cancelled, was skipped while its output was true, or the pull request was a draft. Otherwise it's skipped, which GitHub counts as passing, so it takes no runner. A call's result covers every job in it, so a job skipped because one it needs didn't finish fails its tool's call.

Jobs that only report have `continue-on-error: true`, so they never fail their call or block a merge: the coverage `report` jobs that comment on the pull request, and locallibrary-ci's `semver`. `ci.yml`'s concurrency group cancels the run a new push supersedes, on `main` too.
Jobs that only report have `continue-on-error: true`, so they never fail their call or block a merge: the coverage `report` jobs that comment on the pull request, and locallibrary-ci's `semver`. `ci.yml`'s concurrency group cancels the run a new push to a pull request supersedes. On `main` a run finishes, since what it saves is what pull requests restore.

Pull requests and pushes to `main` run on Linux. The plan's 5 macOS runners can't keep up with every push, so locallibrary's macOS and Windows job (`other-os`) runs once for each merge queue group, which runs nothing else, or on a pull request with the `all-os` label. `locallibrary-cache.yml` runs it nightly on `main`, which is where its caches are saved.
Pull requests run on Linux. The plan's 5 macOS runners can't keep up with every push, so locallibrary's macOS and Windows job (`other-os`) runs once for each merge queue group, which runs nothing else but CodeQL, or on a pull request with the `all-os` label. `locallibrary-cache.yml` runs it nightly on `main`, which is where its caches are saved, when what they're keyed on changed or they were evicted. A push to `main`, whose commit is the merge queue group's, runs only what pull requests use from it: the website's and localtoolbox's coverage and sizes, which the reports compare with, the Rust and Docker caches when what they're keyed on changed, CodeQL, and the ruleset check when the ruleset file changed.

The "Protect Main" ruleset requires "CI passed", which reports either way, so a tool's checks never block a pull request that doesn't touch it. The ruleset is `.github/rulesets/protect-main.json`: an admin applies it with `tools/ruleset.sh apply` once the pull request that changes it merges, and the Repo checks workflow fails on `main` while GitHub's copy differs. A pull request that renames or drops a required check is the exception: GitHub won't merge it while the old name is required, so the admin applies the file from its branch first, and open pull requests then merge `main` to report the new name. It merges through a merge queue, which builds at most 2 groups of up to 5 pull requests at once, and requires CI passed and code scanning, which is `codeql.yml`'s CodeQL analysis of the workflows, the TypeScript and the Rust, leaving out the Rust tests (`.github/codeql/codeql-config.yml`).
The "Protect Main" ruleset requires "CI passed", which passes when it's skipped, so a tool's checks never block a pull request that doesn't touch it. The ruleset is `.github/rulesets/protect-main.json`: an admin applies it with `tools/ruleset.sh apply` once the pull request that changes it merges, and the Repo checks workflow fails on `main` while GitHub's copy differs. A pull request that renames or drops a required check is the exception: GitHub won't merge it while the old name is required, so the admin applies the file from its branch first, and open pull requests then merge `main` to report the new name. It merges through a merge queue, which builds at most 2 groups of up to 5 pull requests at once, and requires CI passed and code scanning, which is `codeql.yml`'s CodeQL analysis of the workflows, the TypeScript and the Rust, leaving out the Rust tests (`.github/codeql/codeql-config.yml`). `ci.yml` calls it with the workflows every time, so each commit has a result, and the other languages when their code changed.

Changes reach `main` only through a pull request, merged as a merge commit. Each one needs an approval from a code owner (`.github/CODEOWNERS`) given after its last push, and a new push dismisses earlier approvals, so an agent's pull request waits for a person to approve it.

Expand All @@ -80,13 +80,13 @@ Workflow conventions, which `workflow-lint.yml` checks with [actionlint](https:/
- actions pinned to a commit SHA with the version in a comment;
- `permissions: contents: read` unless a job needs more, and `persist-credentials: false` on checkout;
- Node from `.node-version` with `actions/setup-node`, pnpm from `packageManager` with `pnpm/action-setup`, and `pnpm install --frozen-lockfile`;
- a concurrency group per ref that cancels superseded runs, except in a workflow `ci.yml` calls.
- a concurrency group per ref that cancels superseded runs (but a push to `main`'s), except in a workflow `ci.yml` calls.

Slow suites (mutation testing, fuzzing, every browser, release builds) run weekly in `<tool>-scheduled.yml`, on a Monday cron plus `workflow_dispatch` so they can run on any branch. They have no `passed` job and no path filter.
Slow suites (mutation testing, fuzzing, every browser, release builds) run weekly in `<tool>-scheduled.yml`, on a Monday cron plus `workflow_dispatch` so they can run on any branch. They have no `passed` job. A `changes` job skips each suite on a scheduled run when nothing it tests changed since its last run that passed (`.github/actions/last-passed` with `events`); a manual run runs everything.

Repository-wide checks: `npm-advisories.yml` runs `pnpm audit --prod` over the lockfile on pull requests and weekly, `cargo deny` (in `locallibrary-ci.yml`, with `deny.toml`) checks the Rust dependencies' advisories, licences and sources, OpenSSF Scorecard (`scorecard.yml`) runs osv-scanner over every lockfile and skips what `osv-scanner.toml` lists (a lockfile in another directory needs its own, as `fuzz/` has), and Dependabot updates the npm lockfile, Cargo (`/` and `/fuzz`) and Actions monthly. npm advisories and Dependabot cover a new app without changes; `cargo deny` runs when `locallibrary-ci.yml`'s paths change, which a new Rust app adds to.
Repository-wide checks: `npm-advisories.yml` runs `pnpm audit --prod` over the lockfile on pull requests that change it and weekly, `cargo deny` (in `locallibrary-ci.yml`, with `deny.toml`) checks the Rust dependencies' advisories, licences and sources, OpenSSF Scorecard (`scorecard.yml`) runs osv-scanner over every lockfile weekly and skips what `osv-scanner.toml` lists (a lockfile in another directory needs its own, as `fuzz/` has), and Dependabot updates the npm lockfile, Cargo (`/` and `/fuzz`) and Actions monthly, in one pull request. npm advisories and Dependabot cover a new app without changes; `cargo deny` runs when `Cargo.lock`, a `Cargo.toml` or `deny.toml` changes, which a new Rust app's manifest is.

Labels: `pr-labels.yml` labels pull requests from `labeler.yml`, and `labels.yml` (the workflow) syncs `.github/labels.yml` to GitHub. Each tool has a label, and its areas are `"<tool>: <area>"` labels in a lighter shade of its colour.
Labels: `ci.yml`'s changes job labels pull requests from `labeler.yml` (`pr-labels.yml` labels those from forks, whose token can't), and `labels.yml` (the workflow) syncs `.github/labels.yml` to GitHub. Each tool has a label, and its areas are `"<tool>: <area>"` labels in a lighter shade of its colour.

Issues are planned in Linear, in the `localnetwork` workspace's Localnetwork team (`LOC`). Linear's GitHub integration syncs this repository's issues both ways with that team and links pull requests to them, so nothing in the repository runs for it. Its settings live in Linear, so they're listed here:

Expand Down
18 changes: 15 additions & 3 deletions .github/actions/last-passed/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ description: >-
last commit that passed, those files count until a run passes. Empty, with a warning, when none
of the workflow's last 100 runs is one, which leaves paths-filter on the commit before the push.
For a merge queue group, the commit it goes on top of, so the group checks what its pull requests
change. Needs `actions: read`.
change. With `events`, the last successful run of those events on the branch instead, with no
warning when there's none: a scheduled workflow decides for itself what to do without one. Needs
`actions: read`.

inputs:
events:
description: The events whose runs count, separated by spaces.
default: push

outputs:
sha:
Expand All @@ -24,6 +31,7 @@ runs:
WORKFLOW_REF: ${{ github.workflow_ref }}
BRANCH: ${{ github.ref_name }}
GROUP_BASE: ${{ github.event.merge_group.base_sha }}
EVENTS: ${{ inputs.events }}
run: |
if [ -n "$GROUP_BASE" ]; then
echo "sha=$GROUP_BASE" >> "$GITHUB_OUTPUT"
Expand All @@ -37,10 +45,14 @@ runs:
# go through GitHub's search index, which answered 11 of 40 identical queries with no runs
# on 2026-10-22, while the unfiltered list, newest first, found the run every time.
sha=$(gh api "repos/$GH_REPO/actions/workflows/$workflow/runs?per_page=100" |
jq -r --arg branch "$BRANCH" '[.workflow_runs[]
| select(.head_branch == $branch and .event == "push" and .conclusion == "success")]
jq -r --arg branch "$BRANCH" --arg events "$EVENTS" '[.workflow_runs[]
| select(.head_branch == $branch and (.event | IN($events | split(" ")[])) and .conclusion == "success")]
| .[0].head_sha // empty')
echo "sha=$sha" >> "$GITHUB_OUTPUT"
if [ "$EVENTS" != push ]; then
echo "Last $EVENTS run that passed on $BRANCH: ${sha:-none in the last 100 runs}"
exit 0
fi
if [ -z "$sha" ]; then
echo "::warning title=No passed run::None of $workflow's last 100 runs passed on a push to $BRANCH, so this push is diffed against the commit before it"
fi
Expand Down
Loading
Loading