Skip to content
This text was generated by an LLM (how cgwalters uses LLMs). cgwalters has not reviewed it yet.

Workflow changes at pull request ingress

workflow-ingress.yml runs on pull_request_target and merge_group. It has only contents: read, no secrets or OIDC permission, and never executes PR code. The checker and the SHA-256-verified actionlint 1.7.12 and zizmor 1.30.1 pins come from github.sha, which for this event is the tip of the PR's base branch, not its head. The workflow definition comes from the base branch too, or from the ref the ruleset names when a ruleset runs it.

The job reports as vet on every PR, so a ruleset can require that status context. It runs when a PR is opened, synchronized, reopened or edited; required by a ruleset, GitHub ignores those types. Retargeting a PR to another base does not run it again, so close and reopen a retargeted PR, or push to it.

The checker reads changed workflow/action files at the PR head as Git blobs into an isolated temporary directory. It rejects files over 1 MiB, and any changed path under .github that is a symlink or submodule. Zizmor runs offline, without repository configuration, including its default audits; actionlint checks workflows with shellcheck/pyflakes disabled. Any finding in a changed file fails, even if it existed before this PR. Deleted and unchanged files are not audited by these two analyzers. No job digest or finding-identity grandfathering is involved.

The same job builds agentic-job with cargo build --locked --release only from the base checkout (the 30-minute limit allows a cold build). It runs workflows check on the complete head workflow set, even if no workflow changed, extracted as Git blobs into an empty temporary root. Symlinks and submodules are refused, including directory replacements; the set is bounded to 500 files of 1 MiB each. Only the base checkout's .github/agentic-job/workflows.toml supplies exemptions. Missing exempt files fail: land the exemption removal first before deleting or renaming a file. This stops proposals from exempting themselves, not malicious behavior in an already exempt file. Permissions stay contents: read, with no secrets and no proposed code execution.

An inline # zizmor: ignore[...] comment can silence unrelated findings, so added or changed ignores in the diff against the merge-base fail, with one exception: exactly # zizmor: ignore[self-repository] on a step's uses: line whose literal path begins with ./, or a job's uses: line naming ./.github/workflows/<name>.yml. The workflow filename must start with an ASCII letter, digit, underscore or hyphen, contain only those characters and dots, and contain no ... Neither form permits remote references; both run code from this repository that still needs human review. The checker accepts block-style steps with - uses: or - name:/- id: followed by uses:, with a plain single-line name/id, an unquoted or single-/double-quoted local path and an optional reason after the directive. The exception requires LF line endings and YAML quoted scalars closed on their line. Quotes within plain scalar values and indented |/> block bodies do not open YAML quoting. Structural refusals report the source line and reason when an ignore is refused. Other spellings and placements fail closed. Ignores already on the base branch keep working. Every other new or changed exemption is refused, including additional audit names on a local step. It goes on the line zizmor reports; a comment elsewhere does nothing. Actionlint has no inline-ignore syntax: fix real findings instead. PR configuration files cannot disable either analyzer here. The base branch's .github/actionlint.yaml (CI's shared workflow check reads a pull request's own copy) adds the GitHub-hosted ubuntu-26.04 label missing from actionlint's built-in list; it does not disable label checks. It also filters the pinned analyzer's unknown job.workflow_repository/job.workflow_sha fields in policy, check and apply, and jobs.notify.result in policy. Only the secret-free secure-host test workflow may use deprecated log commands: its hostile probes and positive control verify that sandbox log escaping stops runner-command injection. These message- and file-specific filters also cover the isolated Git blobs; other expression and deprecated-command errors still fail.

This check catches patterns the analyzers know to be bad, and new exemptions from them. It does not stop a hostile PR. In particular it does not judge new permissions, secrets or id-token: write on triggers the analyzers consider safe, such as push, and an ignore already on the base keeps covering its line whatever changes below it: a workflow whose pull_request_target trigger is exempted can be turned into one that runs PR code. Nor does it prove scripts safe or analyze transitive action/reusable-workflow behavior. Human or reviewer approval before merge does all of that. Analyzer parser vulnerabilities remain a risk: the runner has a runtime token and the base branch's cache scope, even though its repository token is read-only.

An administrator must enforce the trusted workflow with a ruleset; a required status name alone can be imitated by PR-owned workflows. Installation needs maintainer review because the old default branch cannot run a new definition. Events produced with GITHUB_TOKEN do not trigger workflows; token-created PRs need an app/PAT-triggered event. Private-repository unauthenticated fetches are not supported. Hosted event delivery and ruleset enforcement need verification on GitHub, not just local tests.

Merge queue

A queue merges a group commit, not the head the PR run vetted, so vet also runs on merge_group and checks base_sha..head_sha with the checker and analyzer pins checked out at base_sha, never the queued tree. That workflow definition is the queued one, though: a PR that edits this workflow, or adds another job reporting vet, can pass the group's check. The PR-time run from the base branch still audits such an edit; the human approval required below is the gate for it.

Enqueueing runs the proposed workflows and code with repository write and OIDC permissions before landing, even if the group later fails. Therefore the queue must require an approving human review of the current head before admission. The sandbox does not protect credentials held by workflow steps outside it. Future auto-merge must not bypass human approval of the current head for any apply-opened PR. Edits to .github/** or action.yml/action.yaml are examples, not the entire boundary: binaries and host scripts built from the queued tree also execute outside the sandbox. Require at least one approval, approval of the most recent push, and dismissal of stale approvals on push. Allow no bypass actors unless they run and verify the full E2E suite before merging. Do not enable the queue until this approval requirement and broker OIDC trust have been verified. Queue setting changes also require rerunning open PRs whose green CI skipped E2E, as described in the rollout guide.