audit-ci · git:20261031.fa3de16 · 2026-10-31 · sha256 801f6a476350f2dc
audit-ci git:20261031.fa3de16A
Immutable. This exact content never changes and is served at /api/v1/blob/801f6a476350f2dc.
--- name: audit-ci description: "Audit and change Nino's GitHub Actions (ci.yml, runtimes.yml, unity.yml, fuzz.yml, codeql.yml, release.yml, report.yml, ab-bench.yml and the test branch's A/B workflows) - as fast and parallel as possible, green on main and the test branch, every action pinned to a SHA, path filters that run what a change touches, one Unity license seat, secrets only through Trusted Publishing or repository secrets. Use when changing a workflow, when CI is slow or red, or before a release. Triggers: .github/workflows, .github/actions, actionlint, CI, runner, matrix, shards, cache, path filter, failed run, slow CI, Unity license, dependabot, 工作流, 持续集成, CI 失败, CI 慢, 并行." metadata: internal: true --- # CI audit The owner reads every failure email: CI stays green, and a red run is fixed at once. ## Rules - **Fast and parallel**: jobs that do not depend on each other run side by side; long suites are sharded (the unit tests' three shards per configuration, `.github/scripts/test_shards.py` checks they ran everything; `report.yml`'s ten shards per architecture); builds are cached where the cache is safe; each job installs only what it needs. - **Path filters** run a workflow only for the paths its jobs read (ci.yml lists them with a YAML anchor shared by push and pull_request); a new folder a job reads is added to its filter. A job whose inputs are committed files that another workflow checks runs on those files, not on what produces them: runtimes.yml runs on `runtimes/` and `conformance/corpus/` only, because ci.yml's integrations job checks that the corpus and nino-gen's generated files are what .NET writes today (its 16 jobs had run for every Nino.Core change). Inside a workflow, jobs of different areas are gated by `.github/scripts/changed_paths.py` (a `changes` job diffing against the push's `before` or the pull request's base; everything runs when that is unknown, on a schedule and when the workflow changes): runtimes.yml per runtime (Miri only for the crate's library code), codeql.yml per language (every language weekly). - **Green**: never push a change whose workflow was not run locally in its parts (`verify`); `actionlint` (1.7.12 or newer: 1.7.7 and 1.7.8 reject the YAML anchors of the path filters with "must be sequence node but got alias node"; `go install github.com/rhysd/actionlint/cmd/actionlint@latest`) with `.github/actionlint.yaml` after any workflow edit; a step that pipes a command keeps its failure (`bash -eo pipefail` by default). Flaky is not a root cause: find it. A step that downloads has a bound: Ubuntu packages go through `.github/actions/apt-install` (a time limit per try, three tries, the .deb files cached), never a bare `apt-get`. - **Pinned**: every action by commit SHA with its version in a comment (`uses: actions/checkout@<sha> # v7.0.1`); Dependabot updates both, grouped. - **Least privilege**: `permissions: contents: read` unless a job writes; no ref taken from an input runs code in a cache scope that is not its own (cache poisoning); publishing goes through Trusted Publishing (NuGet, crates.io, npm), never a stored key in a file. - **Unity**: `unity.yml` runs one Unity version at a time (`max-parallel: 1`) and is never cancelled while running, so CI holds at most one license seat; `report.yml`'s Unity section the same. `unity.yml` also builds a WebGL player per version after the tests (`webgl` job, `max-parallel: 1`) and runs it in headless Chrome (playwright-core with the runner's Chrome, `.github/scripts/webgl_run.mjs`, which must exit as soon as the player reports done: a pending `setTimeout` kept Node, and with it the run that holds the `unity-license` group, for 40 more minutes). An agent session's GitHub integration cannot `workflow_dispatch` (403 "Resource not accessible by integration", also after `add_repo` with push access): to run unity.yml on an experiment, push a branch whose own unity.yml adds it to `on.push.branches`, check first that no Unity run is queued (a newer pending run in the `unity-license` group replaces an older pending one), and name the branch in the report: the session cannot delete it either (`git push --delete` returned 403). Pin `cliVersion` on every game-ci action that runs the game-ci CLI (unity-test-runner 4.4+, unity-builder 6+; all to one release): its default `latest` asks the GitHub API on every run, and a 403 from the rate limit failed a job. Dependabot bumps the action, never that input: on a game-ci major, read its `action.yml` at the new SHA for inputs it ignores (unity-builder 6.0.0's `unityVersion` reaches the CLI as `--engineVersion`) and keep the editor chosen by `customImage`. A custom `buildMethod` under unity-builder 6 prints the CLI's build summary (a `# Build results #` banner, `Errors: 0`, then a `Size:` line, as `WebGLBenchBuild` does): the CLI parses the log for it and failed the WebGL job of a build Unity reported as succeeded (4.8.2 never looked). - **Releases and reports**: `release.yml` and `report.yml` run only on the owner's action or a release; `report.yml` runs only the sections the release's tree has. - **Concurrency budget**: every matrix that can grow with a request or a file has `max-parallel` (`runtime-ab.yml` 10, `dotnet-ab.yml` 8, `report.yml`'s bench 12, `ab-bench.yml` and ci.yml's pull-request benchmark 6 (shards.json), `unity.yml` 1), and request caps keep a run's job count bounded (8 runtime lines, 3 .NET lines). A push to main should start well under the account's 40 concurrent jobs: short checks of one runner share a job, pushes fuzz with one libFuzzer runner (three at night), and a job runs only for the area that changed (Path filters). How workers share the budget: `benchmark`, "Spend CI as a budget". - **A/B workflows** (`runtime-ab.yml`, `dotnet-ab.yml`) live on the test branch and read `.github/ab-request.txt` (`benchmark`); `ab-bench.yml` on main compares a ref with the previous release or its base. ## Look for - Jobs that wait on others without needing their output; steps repeated across jobs that a composite action (`.github/actions/setup-dotnet`) or a script would share; caches that never hit; matrix entries that test the same thing twice; jobs that run on paths they do not read. - The slowest job of each workflow (`gh run view --json jobs` or the API): what dominates it, and whether it can be sharded or cached. - Unpinned actions, missing `permissions`, `pull_request_target` or inputs flowing into `run:` unquoted. - The code standard (`audit-code`: DRY, decoupled, clear) applies to these files, and redundancy in them (duplicated logic, dead code, two ways to do one job) is part of this audit; the sweep across layers is `audit-redundancy`. ## Proof `actionlint`, the job's commands run locally, then the pushed run watched to the end; for a speed change, the before and after durations of the same workflow.