From 1c9ba6b352b76531b60ff3bb7e432685478bc718 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Wed, 14 Oct 2026 20:23:04 -0400 Subject: [PATCH 1/4] CORE-6123: add run_infra_ci 1.0.0 actionlint and zizmor have no npm installer, so repos either skip them or install them ad hoc. This workflow runs each as its own job through tasks the repo defines in a committed mise.toml, so CI and engineers run the same pinned tools with the same command. Tasks and jobs name what is checked, not the tool: lint:gha / "Lint GHA" (actionlint with shellcheck) and scan:gha / "Scan GHA" (zizmor), plus an aggregate infra-ci task that lists both. CI appends annotation flags after --; zizmor runs its online audits with github.token. mise 2026.9.17 is installed through jdx/mise-action 5.0.0 pinned by SHA, with the binary sha256 pinned; with a mise.lock present the action installs --locked. Verified locally: mise lock --platform linux-x64,macos-arm64 records checksums for both; Renovate's mise manager regenerates mise.lock; the aqua backend carries GitHub attestations for actionlint and zizmor, while shellcheck has no registry checksum, so mise.lock pins it; no actionlint release knows ubuntu-26.04, so the README documents the self-hosted-runner label config. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/CHANGELOGS/run_infra_ci.md | 15 ++ .github/workflows/READMES/run_infra_ci.md | 158 ++++++++++++++++++ .github/workflows/run_infra_ci.yml | 163 +++++++++++++++++++ 3 files changed, 336 insertions(+) create mode 100644 .github/workflows/CHANGELOGS/run_infra_ci.md create mode 100644 .github/workflows/READMES/run_infra_ci.md create mode 100644 .github/workflows/run_infra_ci.yml diff --git a/.github/workflows/CHANGELOGS/run_infra_ci.md b/.github/workflows/CHANGELOGS/run_infra_ci.md new file mode 100644 index 0000000..b16fc8a --- /dev/null +++ b/.github/workflows/CHANGELOGS/run_infra_ci.md @@ -0,0 +1,15 @@ +# Run Infra CI Workflow Changelog + +All notable changes to the **run_infra_ci** reusable workflow are documented in this file. + +## 1.0.0 + +### Added + +- First release of the `run_infra_ci` reusable workflow, with an optional `commit-identifier` input. +- Jobs `lint-gha` ("Lint GHA", actionlint with shellcheck) and `scan-gha` ("Scan GHA", zizmor), each + running the consuming repository's `lint:gha` or `scan:gha` mise task with GitHub annotation output. +- A contract check that fails with a README link when `mise.toml` or the job's task is missing. +- mise 2026.9.17 installed through `jdx/mise-action` 5.0.0, with the binary checksum pinned. +- zizmor online audits using the job's `github.token`. +- Job summaries with the result, the suppression count, and the local command. diff --git a/.github/workflows/READMES/run_infra_ci.md b/.github/workflows/READMES/run_infra_ci.md new file mode 100644 index 0000000..8f1303e --- /dev/null +++ b/.github/workflows/READMES/run_infra_ci.md @@ -0,0 +1,158 @@ +# Run Infra CI + +This reusable workflow runs infrastructure and repository-configuration checks whose tools have no npm +installer. This release checks GitHub Actions workflows and actions with +[actionlint](https://github.com/rhysd/actionlint) (including shellcheck for `run` scripts) and +[zizmor](https://docs.zizmor.sh/). Each tool runs in its own job, through tasks the consuming +repository defines in a committed `mise.toml`, so engineers run the same command locally. + +## Usage + +```yaml +jobs: + infra-ci: + name: Infra CI + needs: ci-context + if: ${{ needs.ci-context.outputs.should-run-ci == 'true' }} + uses: OpenSesame/core-github-actions/.github/workflows/run_infra_ci.yml@ # workflows/run_infra_ci/1.0.0 + permissions: + contents: read # Required to check out the workflows to lint and audit. + with: + commit-identifier: ${{ needs.ci-context.outputs.commit-identifier }} +``` + +The checks are reported as `Infra CI / Lint GHA` and `Infra CI / Scan GHA`. Add `infra-ci` to the +caller's `ci-result` needs. The workflow has no per-tool switches; new tools arrive as new required +tasks in a major release. + +## Contract: `mise.toml` + +The consuming repository commits a `mise.toml` at its root with these tasks. Task names describe +what is checked, not the tool, and later tools follow the same `action:subject` pattern, such as +`lint:markdown` or `scan:terraform`: + +| Task | Runs | +| ---------- | --------------------------------------------- | +| `lint:gha` | actionlint, with shellcheck for `run` scripts | +| `scan:gha` | zizmor on `.github` | +| `infra-ci` | both tasks | + +Copy this template, keeping the tasks as written: + +```toml +[tools] +"aqua:rhysd/actionlint" = "1.7.12" +"aqua:koalaman/shellcheck" = "0.11.0" +"aqua:zizmorcore/zizmor" = "1.29.0" + +[tasks."lint:gha"] +description = "Lint GitHub Actions workflows" +run = "actionlint" + +[tasks."scan:gha"] +description = "Audit GitHub Actions workflows for security issues" +run = "zizmor .github" + +[tasks.infra-ci] +description = "Run every infra-ci check" +depends = ["lint:gha", "scan:gha"] +``` + +Then generate the lockfile for both developer Macs and the CI runners, and commit it: + +```shell +mise lock --platform linux-x64,macos-arm64 +``` + +Rules: + +1. `[tools]` holds infra-ci tools only. Node.js and npm stay on `.nvmrc` and `actions/setup-node`. +2. Every tool names its backend explicitly, using a backend that verifies downloads. The `aqua:` + backend checks each download against the registry's checksums, and actionlint and zizmor also + carry GitHub artifact attestations. The aqua registry has no checksum for shellcheck, so the + committed `mise.lock` is what pins its download. +3. Each required task is a single command with no arguments of its own beyond its target. The + workflow appends GitHub annotation flags (`mise run -- `), and engineers + run the task bare. +4. `mise.lock` covers `linux-x64` and `macos-arm64`. When a lockfile is present, `jdx/mise-action` + installs with `--locked`, so CI downloads must match the recorded URLs and checksums. Renovate's + mise manager regenerates `mise.lock` when it bumps a tool, and `mise lock` keeps the platforms + already in the file. + +If `mise.toml` or a job's task is missing, that job fails with a link to this README. + +npm repositories may add one pointer script per tool, for example +`"lint:gha": "mise run lint:gha"` and `"scan:gha": "mise run scan:gha"`. Keep `lint` and `scan` +themselves as explicit commands, not `lint:*` or `scan:*` globs, so `app-ci` steps such as +`npm run lint` don't need mise. + +## Suppressing findings + +Fix findings, including existing ones found when a repository adopts infra-ci. Suppress a finding only +when it can't be fixed: a false positive, or a pattern the code needs on purpose. Each tool keeps its +suppressions in its own config file at a fixed location: + +| Tool | File | +| ---------- | ------------------------------------------------------------------------------ | +| actionlint | `.github/actionlint.yaml` (`paths..ignore` patterns) | +| zizmor | `.github/zizmor.yml` (`rules..ignore`), or inline `# zizmor: ignore[]` | + +Rules: + +1. Suppress a specific finding (a file and rule, or a file, line and rule). Never disable a rule + for the whole repository. +2. Every entry has a comment explaining why the finding can't be fixed. Suppressions are permanent, + so they have no follow-up ticket. +3. New findings always fail. +4. Prefer inline zizmor comments to line-number ignores, which break when the file changes. + +Each job summary reports how many suppressions its config file holds. + +`actionlint.yaml` also carries configuration that is not a suppression. No actionlint release yet +knows the `ubuntu-26.04` runner label, so declare it until one does: + +```yaml +self-hosted-runner: + labels: + - ubuntu-26.04 +``` + +## Jobs + +Each job checks out `commit-identifier` without persisting credentials, verifies the contract, +installs mise 2026.9.17 with `jdx/mise-action` (version and binary checksum pinned), installs the +tools from `mise.toml`, runs its task, and writes a job summary with the result, the suppression +count, and the local command. + +| Job | Command in CI | Local command | +| ---------- | ----------------------------------------------------- | ------------------- | +| `lint-gha` | `mise run lint:gha -- -format ''` | `mise run lint:gha` | +| `scan-gha` | `mise run scan:gha -- --format=github` | `mise run scan:gha` | + +zizmor runs with `GH_TOKEN` set to the job's `github.token`, which enables its online audits, such +as known-vulnerable actions and impostor commits. Locally it runs offline unless `GH_TOKEN` is set. + +## Inputs + +| Input | Type | Required | Default | Description | +| ------------------- | ------ | -------- | --------------------- | ---------------------------------------- | +| `commit-identifier` | string | No | Triggering ref or SHA | Commit SHA, tag, or branch to check out. | + +## Permissions + +| Permission | Reason | +| ---------- | -------------------------------------------------- | +| `contents` | `read`: check out the workflows to lint and audit. | + +The workflow jobs run on Ubuntu 26.04. + +## Contribution + +- Update the workflow, README, and changelog together. +- Create a PR and set a version label following the [versioning instructions](../../../VERSIONING.md). + +## References + +- [actionlint configuration](https://github.com/rhysd/actionlint/blob/main/docs/config.md) +- [zizmor configuration](https://docs.zizmor.sh/configuration/) +- [mise tasks](https://mise.jdx.dev/tasks/) and [mise.lock](https://mise.jdx.dev/dev-tools/mise-lock.html) diff --git a/.github/workflows/run_infra_ci.yml b/.github/workflows/run_infra_ci.yml new file mode 100644 index 0000000..e97fd1a --- /dev/null +++ b/.github/workflows/run_infra_ci.yml @@ -0,0 +1,163 @@ +name: Infra CI + +on: + workflow_call: + inputs: + commit-identifier: + description: Commit SHA or ref to check; defaults to the ref that triggered the workflow + type: string + required: false + default: '' + +permissions: {} + +defaults: + run: + shell: bash + +env: + CONTRACT_DOCS: https://github.com/OpenSesame/core-github-actions/blob/main/.github/workflows/READMES/run_infra_ci.md + +jobs: + lint-gha: + name: Lint GHA + runs-on: ubuntu-26.04 + permissions: + contents: read # Required to check out the workflows to lint. + + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.commit-identifier }} + persist-credentials: false + + - name: Check for mise.toml + run: | + if [[ ! -f mise.toml ]]; then + echo "::error title=Infra CI contract::mise.toml is missing. See $CONTRACT_DOCS" + exit 1 + fi + + - name: Set up mise + uses: jdx/mise-action@9149ea85001c7435d5a66bb127d6a1b6227cb0a5 # v5.0.0 + with: + version: 2026.9.17 + sha256: 63049bc35fb9065e8dc35ac8b25fdae53e9bd6f1885a843aedeba398e046a1ee + + - name: Check for the lint:gha task + run: | + if ! mise tasks info lint:gha >/dev/null 2>&1; then + echo "::error title=Infra CI contract::mise.toml has no lint:gha task. See $CONTRACT_DOCS" + exit 1 + fi + + - name: Run actionlint + id: run + run: | + status=0 + # shellcheck disable=SC2016 + mise run lint:gha -- \ + -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}},title=actionlint {{$err.Kind}}::{{$err.Message}}\n{{end}}' \ + || status=$? + echo "status=$status" >> "$GITHUB_OUTPUT" + exit "$status" + + - name: Write job summary + if: ${{ !cancelled() && steps.run.outcome != 'skipped' }} + env: + STATUS: ${{ steps.run.outputs.status }} + run: | + config_file=.github/actionlint.yaml + if [[ ! -f "$config_file" ]]; then + entries=0 + elif command -v yq >/dev/null 2>&1; then + entries=$(yq '[.paths[]?.ignore[]?] | length' "$config_file") + else + entries="unknown (yq not installed)" + fi + + { + echo "## Lint GHA (actionlint)" + echo "" + if [[ "$STATUS" == "0" ]]; then + echo "✅ No findings." + else + echo "❌ Findings reported as annotations on this run." + fi + echo "" + echo "- **Suppressions** (\`$config_file\` \`paths.*.ignore\`): $entries" + echo "- **Run locally**: \`mise run lint:gha\`" + } >> "$GITHUB_STEP_SUMMARY" + + scan-gha: + name: Scan GHA + runs-on: ubuntu-26.04 + permissions: + contents: read # Required to check out the workflows to audit. + + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.commit-identifier }} + persist-credentials: false + + - name: Check for mise.toml + run: | + if [[ ! -f mise.toml ]]; then + echo "::error title=Infra CI contract::mise.toml is missing. See $CONTRACT_DOCS" + exit 1 + fi + + - name: Set up mise + uses: jdx/mise-action@9149ea85001c7435d5a66bb127d6a1b6227cb0a5 # v5.0.0 + with: + version: 2026.9.17 + sha256: 63049bc35fb9065e8dc35ac8b25fdae53e9bd6f1885a843aedeba398e046a1ee + + - name: Check for the scan:gha task + run: | + if ! mise tasks info scan:gha >/dev/null 2>&1; then + echo "::error title=Infra CI contract::mise.toml has no scan:gha task. See $CONTRACT_DOCS" + exit 1 + fi + + - name: Run zizmor + id: run + env: + GH_TOKEN: ${{ github.token }} + run: | + status=0 + mise run scan:gha -- --format=github || status=$? + echo "status=$status" >> "$GITHUB_OUTPUT" + exit "$status" + + - name: Write job summary + if: ${{ !cancelled() && steps.run.outcome != 'skipped' }} + env: + STATUS: ${{ steps.run.outputs.status }} + run: | + config_file=.github/zizmor.yml + if [[ ! -f "$config_file" ]]; then + entries=0 + elif command -v yq >/dev/null 2>&1; then + entries=$(yq '[.rules[]?.ignore[]?] | length' "$config_file") + else + entries="unknown (yq not installed)" + fi + inline=$(grep -rhoE 'zizmor: ?ignore\[' .github 2>/dev/null | wc -l | tr -d ' ') + + { + echo "## Scan GHA (zizmor)" + echo "" + if [[ "$STATUS" == "0" ]]; then + echo "✅ No findings." + else + echo "❌ Findings reported as annotations on this run." + fi + echo "" + echo "- **Suppressions** (\`$config_file\` \`rules.*.ignore\`): $entries" + echo "- **Inline ignores** (\`# zizmor: ignore[...]\` under \`.github\`): $inline" + echo "- **Run locally**: \`mise run scan:gha\` (offline unless \`GH_TOKEN\` is set)" + } >> "$GITHUB_STEP_SUMMARY" From bb9129fb151e11100239477f18741d94a496ab75 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Wed, 14 Oct 2026 20:37:13 -0400 Subject: [PATCH 2/4] CORE-6123: fix findings in the internal version-label workflows run_infra_ci will lint core-github-actions itself, so the internal workflows need to pass actionlint, shellcheck and zizmor. Set Version Tags ran `const message = ...` in bash steps, a JavaScript line that fails with "const: command not found". Every untracked-label merge and every closed-without-merge PR failed the workflow (9 of the last 28 runs, including 2026-10-14). Those steps now assign a shell variable. Permissions move to the jobs: only set-version-tags gets contents: write, and its checkout persists credentials explicitly because it pushes the tags. The unused pull-requests: read is dropped. Validate PR Version Labels passed step outputs into run scripts through ${{ env.* }} interpolation; the scripts now read the environment variables directly and write each summary through one grouped redirect. Its checkout no longer persists credentials, and permissions are set on the job with reasons. Job names, and so required check names, are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../internal_on_merge_tag_versions.yml | 15 ++-- ...ernal_on_pr_validate_component_version.yml | 69 ++++++++++--------- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/.github/workflows/internal_on_merge_tag_versions.yml b/.github/workflows/internal_on_merge_tag_versions.yml index 5b3a58d..d824b62 100644 --- a/.github/workflows/internal_on_merge_tag_versions.yml +++ b/.github/workflows/internal_on_merge_tag_versions.yml @@ -4,27 +4,30 @@ on: pull_request: types: [closed] -permissions: - contents: write - pull-requests: read +permissions: {} jobs: no-merge: if: github.event.pull_request.merged == false runs-on: ubuntu-26.04 + permissions: {} steps: - name: No Merge Detected run: | - const message = "Pull request was closed without merge. No version tags will be created." + message="Pull request was closed without merge. No version tags will be created." echo "ℹ️ $message" >> "$GITHUB_STEP_SUMMARY" echo "::notice title=No Merge::$message" set-version-tags: if: github.event.pull_request.merged == true runs-on: ubuntu-26.04 + permissions: + contents: write # Required to push the version tags. steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: true # Required for git push of the version tags. - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -60,7 +63,7 @@ jobs: - name: Untracked Version Check if: ${{ steps.get_tags.outputs.hasUntracked == 'true' }} run: | - const message = "Untracked version label detected. Skipping tag creation." + message="Untracked version label detected. Skipping tag creation." echo "ℹ️ $message" >> "$GITHUB_STEP_SUMMARY" echo "::notice title=Untracked Version::$message" @@ -69,7 +72,7 @@ jobs: env: COMPONENT_TAGS: ${{ steps.get_tags.outputs.componentTags }} run: | - IFS=',' read -r -a tagsArray <<< "${{ env.COMPONENT_TAGS }}" + IFS=',' read -r -a tagsArray <<< "$COMPONENT_TAGS" for tag in "${tagsArray[@]}"; do git tag "$tag" git push origin "$tag" diff --git a/.github/workflows/internal_on_pr_validate_component_version.yml b/.github/workflows/internal_on_pr_validate_component_version.yml index fde82de..a9ce3d1 100644 --- a/.github/workflows/internal_on_pr_validate_component_version.yml +++ b/.github/workflows/internal_on_pr_validate_component_version.yml @@ -11,18 +11,21 @@ on: - unlabeled - reopened -permissions: - contents: read - pull-requests: write +permissions: {} jobs: validate-version-labels: name: Validate PR Version Labels runs-on: ubuntu-26.04 + permissions: + contents: read # Required to check out the changelogs and validation scripts. + pull-requests: write # Required to upsert the version summary comment. steps: - name: Checkout repo uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Get all labels from PR id: get_labels @@ -66,34 +69,36 @@ jobs: INVALID_COMPONENTS: ${{ steps.validate.outputs.invalidComponents }} MISSING_CHANGELOGS: ${{ steps.validate.outputs.missingChangelogs }} run: | - echo "## Validation Outcome" >> "$GITHUB_STEP_SUMMARY" - - if [ "${{ env.NO_LABELS }}" == "true" ]; then - echo "❌ No labels found on the PR. Add at least one version label." >> "$GITHUB_STEP_SUMMARY" - fi + { + echo "## Validation Outcome" - if [ "${{ env.IS_VALID }}" == "false" ]; then - echo "❌ Version label validation failed." >> "$GITHUB_STEP_SUMMARY" - if [ -n "${{ env.VALIDATION_MESSAGE }}" ]; then - echo "${{ env.VALIDATION_MESSAGE }}" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - fi - if [ -n "${{ env.INVALID_VERSION_LABELS }}" ]; then - echo "**Invalid version labels**: ${{ env.INVALID_VERSION_LABELS }}" >> "$GITHUB_STEP_SUMMARY" + if [ "$NO_LABELS" == "true" ]; then + echo "❌ No labels found on the PR. Add at least one version label." fi - if [ -n "${{ env.INVALID_COMPONENTS }}" ]; then - echo "**Nonexistent/Invalid components**: ${{ env.INVALID_COMPONENTS }}" >> "$GITHUB_STEP_SUMMARY" + + if [ "$IS_VALID" == "false" ]; then + echo "❌ Version label validation failed." + if [ -n "$VALIDATION_MESSAGE" ]; then + echo "$VALIDATION_MESSAGE" + echo "" + fi + if [ -n "$INVALID_VERSION_LABELS" ]; then + echo "**Invalid version labels**: $INVALID_VERSION_LABELS" + fi + if [ -n "$INVALID_COMPONENTS" ]; then + echo "**Nonexistent/Invalid components**: $INVALID_COMPONENTS" + fi + if [ -n "$MISSING_CHANGELOGS" ]; then + echo "**Components missing changelogs/entries**: $MISSING_CHANGELOGS" + fi + else + echo "✅ Version label validation passed" fi - if [ -n "${{ env.MISSING_CHANGELOGS }}" ]; then - echo "**Components missing changelogs/entries**: ${{ env.MISSING_CHANGELOGS }}" >> "$GITHUB_STEP_SUMMARY" + echo -e "\n**Untracked Version**: $HAS_UNTRACKED_VERSION" + if [ -n "$COMPONENT_VERSION_LABELS" ]; then + echo "**Component Versions**: $COMPONENT_VERSION_LABELS" fi - else - echo "✅ Version label validation passed" >> "$GITHUB_STEP_SUMMARY" - fi - echo -e "\n**Untracked Version**: ${{ env.HAS_UNTRACKED_VERSION }}" >> "$GITHUB_STEP_SUMMARY" - if [ -n "${{ env.COMPONENT_VERSION_LABELS }}" ]; then - echo "**Component Versions**: ${{ env.COMPONENT_VERSION_LABELS }}" >> "$GITHUB_STEP_SUMMARY" - fi + } >> "$GITHUB_STEP_SUMMARY" - name: Write Tags Job Summary id: tags_summary @@ -107,7 +112,7 @@ jobs: summary+="- No Tags will be created on main" else summary+="The following tags will be created on main after merge\n\n" - echo $VALID_COMPONENTS + echo "$VALID_COMPONENTS" IFS=',' read -ra components <<< "$VALID_COMPONENTS" for component_version in "${components[@]}"; do summary+="🏷️ \`$component_version\`\n" @@ -120,9 +125,11 @@ jobs: # Replace any literal \n with real newlines for correct PR comment formatting summary_with_eol="${summary//\\n/$'\n'}" - echo "summary<> "$GITHUB_OUTPUT" - echo "$summary_with_eol" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" + { + echo "summary<> "$GITHUB_OUTPUT" - name: Write Tags to PR Comment uses: ./.github/actions/upsert-pr-comment From afab33320fe8a35210c456d3f0d84d0d1a91328d Mon Sep 17 00:00:00 2001 From: ssvoss Date: Mon, 19 Oct 2026 21:35:12 -0400 Subject: [PATCH 3/4] CORE-6123: run run_infra_ci on core-github-actions core-github-actions now lints and audits its own workflows with the infra-ci contract it ships: mise.toml and mise.lock pin actionlint, shellcheck and zizmor, .github/actionlint.yaml declares the ubuntu-26.04 label, and internal CI calls run_infra_ci by local path as the infra-ci job, which ci-result now needs. npm scripts follow the standard's names: lint:js, lint:gha, format:prettier, scan:semgrep and scan:gha, with lint-fix:js and format-fix:prettier. lint, format, scan and ci are npm-run-all --continue-on-error aggregates (npm-run-all2 9.0.3), so one failing check no longer hides the rest. Internal CI's app-ci calls lint:js and format:prettier directly and never needs mise. The pre-commit hook runs the infra checks through ci; they take well under a second. The run_infra_ci README is cut to setup steps, and its npm guidance now matches the aggregate convention. Co-Authored-By: Claude Opus 5.5 --- .github/actionlint.yaml | 4 + .github/workflows/READMES/run_infra_ci.md | 160 ++++++---------------- .github/workflows/internal_on_push_ci.yml | 16 ++- AGENTS.md | 1 + README.md | 3 + mise.lock | 52 +++++++ mise.toml | 16 +++ package-lock.json | 136 ++++++++++++++++++ package.json | 20 ++- 9 files changed, 278 insertions(+), 130 deletions(-) create mode 100644 .github/actionlint.yaml create mode 100644 mise.lock create mode 100644 mise.toml diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..78fc41b --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,4 @@ +self-hosted-runner: + # No actionlint release knows the ubuntu-26.04 GitHub-hosted runner label yet. + labels: + - ubuntu-26.04 diff --git a/.github/workflows/READMES/run_infra_ci.md b/.github/workflows/READMES/run_infra_ci.md index 8f1303e..6bb7938 100644 --- a/.github/workflows/READMES/run_infra_ci.md +++ b/.github/workflows/READMES/run_infra_ci.md @@ -1,10 +1,8 @@ # Run Infra CI -This reusable workflow runs infrastructure and repository-configuration checks whose tools have no npm -installer. This release checks GitHub Actions workflows and actions with -[actionlint](https://github.com/rhysd/actionlint) (including shellcheck for `run` scripts) and -[zizmor](https://docs.zizmor.sh/). Each tool runs in its own job, through tasks the consuming -repository defines in a committed `mise.toml`, so engineers run the same command locally. +Lints GitHub Actions workflows with [actionlint](https://github.com/rhysd/actionlint) (and shellcheck) +and audits them with [zizmor](https://docs.zizmor.sh/), through tasks in the repo's `mise.toml`, so +CI and engineers run the same pinned tools. ## Usage @@ -21,138 +19,58 @@ jobs: commit-identifier: ${{ needs.ci-context.outputs.commit-identifier }} ``` -The checks are reported as `Infra CI / Lint GHA` and `Infra CI / Scan GHA`. Add `infra-ci` to the -caller's `ci-result` needs. The workflow has no per-tool switches; new tools arrive as new required -tasks in a major release. +Checks are reported as `Infra CI / Lint GHA` and `Infra CI / Scan GHA`. Add `infra-ci` to `ci-result`'s +needs. -## Contract: `mise.toml` +## Setup -The consuming repository commits a `mise.toml` at its root with these tasks. Task names describe -what is checked, not the tool, and later tools follow the same `action:subject` pattern, such as -`lint:markdown` or `scan:terraform`: +1. Commit this `mise.toml` at the repo root (infra tools only; Node stays on `.nvmrc`): -| Task | Runs | -| ---------- | --------------------------------------------- | -| `lint:gha` | actionlint, with shellcheck for `run` scripts | -| `scan:gha` | zizmor on `.github` | -| `infra-ci` | both tasks | + ```toml + [tools] + "aqua:rhysd/actionlint" = "1.7.12" + "aqua:koalaman/shellcheck" = "0.11.0" + "aqua:zizmorcore/zizmor" = "1.29.0" -Copy this template, keeping the tasks as written: + [tasks."lint:gha"] + run = "actionlint" -```toml -[tools] -"aqua:rhysd/actionlint" = "1.7.12" -"aqua:koalaman/shellcheck" = "0.11.0" -"aqua:zizmorcore/zizmor" = "1.29.0" + [tasks."scan:gha"] + run = "zizmor .github" -[tasks."lint:gha"] -description = "Lint GitHub Actions workflows" -run = "actionlint" + [tasks.infra-ci] + depends = ["lint:gha", "scan:gha"] + ``` -[tasks."scan:gha"] -description = "Audit GitHub Actions workflows for security issues" -run = "zizmor .github" +2. Commit a lockfile for Macs and CI runners: `mise lock --platform linux-x64,macos-arm64`. +3. Declare the runner label actionlint doesn't know yet in `.github/actionlint.yaml`: -[tasks.infra-ci] -description = "Run every infra-ci check" -depends = ["lint:gha", "scan:gha"] -``` - -Then generate the lockfile for both developer Macs and the CI runners, and commit it: - -```shell -mise lock --platform linux-x64,macos-arm64 -``` - -Rules: - -1. `[tools]` holds infra-ci tools only. Node.js and npm stay on `.nvmrc` and `actions/setup-node`. -2. Every tool names its backend explicitly, using a backend that verifies downloads. The `aqua:` - backend checks each download against the registry's checksums, and actionlint and zizmor also - carry GitHub artifact attestations. The aqua registry has no checksum for shellcheck, so the - committed `mise.lock` is what pins its download. -3. Each required task is a single command with no arguments of its own beyond its target. The - workflow appends GitHub annotation flags (`mise run -- `), and engineers - run the task bare. -4. `mise.lock` covers `linux-x64` and `macos-arm64`. When a lockfile is present, `jdx/mise-action` - installs with `--locked`, so CI downloads must match the recorded URLs and checksums. Renovate's - mise manager regenerates `mise.lock` when it bumps a tool, and `mise lock` keeps the platforms - already in the file. - -If `mise.toml` or a job's task is missing, that job fails with a link to this README. - -npm repositories may add one pointer script per tool, for example -`"lint:gha": "mise run lint:gha"` and `"scan:gha": "mise run scan:gha"`. Keep `lint` and `scan` -themselves as explicit commands, not `lint:*` or `scan:*` globs, so `app-ci` steps such as -`npm run lint` don't need mise. - -## Suppressing findings - -Fix findings, including existing ones found when a repository adopts infra-ci. Suppress a finding only -when it can't be fixed: a false positive, or a pattern the code needs on purpose. Each tool keeps its -suppressions in its own config file at a fixed location: - -| Tool | File | -| ---------- | ------------------------------------------------------------------------------ | -| actionlint | `.github/actionlint.yaml` (`paths..ignore` patterns) | -| zizmor | `.github/zizmor.yml` (`rules..ignore`), or inline `# zizmor: ignore[]` | + ```yaml + self-hosted-runner: + labels: + - ubuntu-26.04 + ``` -Rules: +4. npm repos: add `"lint:gha": "mise run lint:gha"` and `"scan:gha": "mise run scan:gha"`. CI calls + specific scripts, never the `lint`/`scan` aggregates, so CI jobs don't need mise. -1. Suppress a specific finding (a file and rule, or a file, line and rule). Never disable a rule - for the whole repository. -2. Every entry has a comment explaining why the finding can't be fixed. Suppressions are permanent, - so they have no follow-up ticket. -3. New findings always fail. -4. Prefer inline zizmor comments to line-number ignores, which break when the file changes. +Run locally with `mise run infra-ci`. zizmor runs offline unless `GH_TOKEN` is set; CI runs it online. -Each job summary reports how many suppressions its config file holds. +## Findings -`actionlint.yaml` also carries configuration that is not a suppression. No actionlint release yet -knows the `ubuntu-26.04` runner label, so declare it until one does: +Fix findings. Suppress only false positives or intentional patterns, one specific finding at a time, +each with a comment explaining why: -```yaml -self-hosted-runner: - labels: - - ubuntu-26.04 -``` - -## Jobs - -Each job checks out `commit-identifier` without persisting credentials, verifies the contract, -installs mise 2026.9.17 with `jdx/mise-action` (version and binary checksum pinned), installs the -tools from `mise.toml`, runs its task, and writes a job summary with the result, the suppression -count, and the local command. - -| Job | Command in CI | Local command | -| ---------- | ----------------------------------------------------- | ------------------- | -| `lint-gha` | `mise run lint:gha -- -format ''` | `mise run lint:gha` | -| `scan-gha` | `mise run scan:gha -- --format=github` | `mise run scan:gha` | - -zizmor runs with `GH_TOKEN` set to the job's `github.token`, which enables its online audits, such -as known-vulnerable actions and impostor commits. Locally it runs offline unless `GH_TOKEN` is set. +- actionlint: `.github/actionlint.yaml` `paths..ignore` +- zizmor: inline `# zizmor: ignore[] `, or `.github/zizmor.yml` `rules..ignore` ## Inputs -| Input | Type | Required | Default | Description | -| ------------------- | ------ | -------- | --------------------- | ---------------------------------------- | -| `commit-identifier` | string | No | Triggering ref or SHA | Commit SHA, tag, or branch to check out. | - -## Permissions - -| Permission | Reason | -| ---------- | -------------------------------------------------- | -| `contents` | `read`: check out the workflows to lint and audit. | - -The workflow jobs run on Ubuntu 26.04. +| Input | Default | Description | +| ------------------- | --------------------- | ----------------------------------- | +| `commit-identifier` | Triggering ref or SHA | Commit SHA, tag, or branch to check | ## Contribution -- Update the workflow, README, and changelog together. -- Create a PR and set a version label following the [versioning instructions](../../../VERSIONING.md). - -## References - -- [actionlint configuration](https://github.com/rhysd/actionlint/blob/main/docs/config.md) -- [zizmor configuration](https://docs.zizmor.sh/configuration/) -- [mise tasks](https://mise.jdx.dev/tasks/) and [mise.lock](https://mise.jdx.dev/dev-tools/mise-lock.html) +Update the workflow, README, and changelog together, and set a version label per +[VERSIONING.md](../../../VERSIONING.md). diff --git a/.github/workflows/internal_on_push_ci.yml b/.github/workflows/internal_on_push_ci.yml index edd88a3..912c413 100644 --- a/.github/workflows/internal_on_push_ci.yml +++ b/.github/workflows/internal_on_push_ci.yml @@ -64,11 +64,21 @@ jobs: - name: Lint if: ${{ !cancelled() && steps.install.outcome == 'success' }} - run: npm run lint + run: npm run lint:js - name: Check formatting if: ${{ !cancelled() && steps.install.outcome == 'success' }} - run: npm run format + run: npm run format:prettier + + infra-ci: + name: Infra CI + needs: ci-context + if: ${{ needs.ci-context.outputs.should-run-ci == 'true' }} + uses: ./.github/workflows/run_infra_ci.yml + permissions: + contents: read # Required to check out the workflows to lint and audit. + with: + commit-identifier: ${{ needs.ci-context.outputs.commit-identifier }} semgrep: name: Semgrep @@ -87,7 +97,7 @@ jobs: ci-result: name: CI Result - needs: [ci-context, app-ci, semgrep] + needs: [ci-context, app-ci, infra-ci, semgrep] if: ${{ !cancelled() }} runs-on: ubuntu-26.04 permissions: {} diff --git a/AGENTS.md b/AGENTS.md index b160423..b550362 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ the repository map, consumer guidance, and migration context. - Use Node.js 24.15.0 or newer in the Node.js 24 release line: `nvm use` - Install the pinned npm version: `npm install --global npm@12.1.0` - Install dependencies: `npm ci` +- Install the GitHub Actions lint and scan tools (needs [mise](https://mise.jdx.dev/)): `mise install` - Run all non-mutating checks: `npm run ci` - Run tests in watch mode: `npm run watch` - Fix lint issues: `npm run lint-fix` diff --git a/README.md b/README.md index bab8185..dc8e83f 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ There are recommended vscode extensions and settings included in the project. To get started: - install semgrep globally `brew install semgrep` +- install [mise](https://mise.jdx.dev/) (`brew install mise`), then run `mise install` for actionlint, + shellcheck and zizmor, pinned in `mise.toml`. `npm run lint:gha` and `npm run scan:gha` run them, and + `npm run ci` and the pre-commit hook include both. - use Node.js 24.15.0 or newer in the Node.js 24 release line (`nvm use`) - install the pinned npm version `npm install --global npm@12.1.0` - install project dependencies `npm ci` diff --git a/mise.lock b/mise.lock new file mode 100644 index 0000000..14a52a2 --- /dev/null +++ b/mise.lock @@ -0,0 +1,52 @@ +# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html + +lockfile_version = 3 + +[[tools."aqua:koalaman/shellcheck"]] +version = "0.11.0" +backend = "aqua:koalaman/shellcheck" +specifiers = ["0.11.0"] + +[tools."aqua:koalaman/shellcheck"."platforms.linux-x64"] +checksum = "sha256:8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198" +url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056942" + +[tools."aqua:koalaman/shellcheck"."platforms.macos-arm64"] +checksum = "sha256:56affdd8de5527894dca6dc3d7e0a99a873b0f004d7aabc30ae407d3f48b0a79" +url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.darwin.aarch64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056932" + +[[tools."aqua:rhysd/actionlint"]] +version = "1.7.12" +backend = "aqua:rhysd/actionlint" +specifiers = ["1.7.12"] + +[tools."aqua:rhysd/actionlint"."platforms.linux-x64"] +checksum = "sha256:8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924896" +provenance = "github-attestations" + +[tools."aqua:rhysd/actionlint"."platforms.macos-arm64"] +checksum = "sha256:aba9ced2dee8d27fecca3dc7feb1a7f9a52caefa1eb46f3271ea66b6e0e6953f" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924893" +provenance = "github-attestations" + +[[tools."aqua:zizmorcore/zizmor"]] +version = "1.29.0" +backend = "aqua:zizmorcore/zizmor" +specifiers = ["1.29.0"] + +[tools."aqua:zizmorcore/zizmor"."platforms.linux-x64"] +checksum = "sha256:dd96df044a6e8538d5f423790f453bdd03d49e5b2bcc38214acc41a2f1297839" +url = "https://github.com/zizmorcore/zizmor/releases/download/v1.29.0/zizmor-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/zizmorcore/zizmor/releases/assets/498263145" +provenance = "github-attestations" + +[tools."aqua:zizmorcore/zizmor"."platforms.macos-arm64"] +checksum = "sha256:720322fade9e83a9c7953944c438f2ba942636b86b96a8f0e6b15ce94c8a6b6f" +url = "https://github.com/zizmorcore/zizmor/releases/download/v1.29.0/zizmor-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/zizmorcore/zizmor/releases/assets/498263141" +provenance = "github-attestations" diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..c5b7465 --- /dev/null +++ b/mise.toml @@ -0,0 +1,16 @@ +[tools] +"aqua:rhysd/actionlint" = "1.7.12" +"aqua:koalaman/shellcheck" = "0.11.0" +"aqua:zizmorcore/zizmor" = "1.29.0" + +[tasks."lint:gha"] +description = "Lint GitHub Actions workflows" +run = "actionlint" + +[tasks."scan:gha"] +description = "Audit GitHub Actions workflows for security issues" +run = "zizmor .github" + +[tasks.infra-ci] +description = "Run every infra-ci check" +depends = ["lint:gha", "scan:gha"] diff --git a/package-lock.json b/package-lock.json index cb02310..f20f79b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "globals": "17.13.0", "husky": "9.1.7", "jest": "30.5.2", + "npm-run-all2": "9.0.3", "prettier": "3.9.9", "semgrep": "0.0.1" }, @@ -4837,6 +4838,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5572,6 +5582,82 @@ "node": ">=0.10.0" } }, + "node_modules/npm-normalize-package-bin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-6.0.0.tgz", + "integrity": "sha512-tdt4aFn9QamlhdN3HV2D2ccpBwO5/fyjjbXUxYA6uBjyekMZcZvDq0aSj9t5Jo+tih6AYFnt/cuIRn9013e0Uw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-run-all2": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-9.0.3.tgz", + "integrity": "sha512-BQAEdU1PtYc48qYRdghW2BVTQT3VqWCoFQmO87NlM1h1PYwMCKQpUWaNyB20V26caNzFsXQDfyOdznLlHCih6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^7.0.0", + "cross-spawn": "^7.0.6", + "memorystream": "^0.3.1", + "picomatch": "^4.0.2", + "pidtree": "^1.0.0", + "read-package-json-fast": "^6.0.0", + "shell-quote": "^1.8.4", + "which": "^7.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0", + "npm": ">= 10" + } + }, + "node_modules/npm-run-all2/node_modules/ansi-styles": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-7.0.0.tgz", + "integrity": "sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm-run-all2/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/npm-run-all2/node_modules/which": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-7.0.0.tgz", + "integrity": "sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -5754,6 +5840,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-1.0.0.tgz", + "integrity": "sha512-avfAvjB9Dd0wdj3rjJX//yS+G79OO0KrS5pJHFJENjYGX6N4SMgEDBBI/yFy0lloOYSaC6XQxzpOAMPfSYFV/Q==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -5935,6 +6034,30 @@ "dev": true, "license": "MIT" }, + "node_modules/read-package-json-fast": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-6.0.0.tgz", + "integrity": "sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^6.0.0", + "npm-normalize-package-bin": "^6.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-6.0.0.tgz", + "integrity": "sha512-2/8adwnK1/+Fdjyts4r6wSpfANWw8zdNhU9U/Llk59c6O+DjSisPWPykwoL8gZmocP9Dy64S7oie2g+Mia123A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6008,6 +6131,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", diff --git a/package.json b/package.json index 7279345..5f38764 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "globals": "17.13.0", "husky": "9.1.7", "jest": "30.5.2", + "npm-run-all2": "9.0.3", "prettier": "3.9.9", "semgrep": "0.0.1" }, @@ -31,11 +32,18 @@ "test": "jest", "watch": "jest --watch", "audit": "npm audit --audit-level=high", - "lint": "eslint *.js --ext .js,.json", - "lint-fix": "eslint *.js --ext .js,.json --fix", - "format": "prettier --check './*.js' './*.mjs' './*.json' './*.md' 'scripts/**/*.js' '.github/actions/**/*.*'", - "format-fix": "prettier --write './*.js' './*.mjs' './*.json' './*.md' 'scripts/**/*.js' '.github/actions/**/*.*'", - "scan": "semgrep --config=p/ci --config=p/security-audit --config=p/javascript ./*.js ./*.mjs ./*.json scripts/ .github/actions/", - "ci": "npm run audit && npm run test && npm run lint && npm run format && npm run scan" + "lint": "npm-run-all --continue-on-error lint:*", + "lint:js": "eslint *.js --ext .js,.json", + "lint:gha": "mise run lint:gha", + "lint-fix": "npm-run-all --continue-on-error lint-fix:*", + "lint-fix:js": "eslint *.js --ext .js,.json --fix", + "format": "npm-run-all --continue-on-error format:*", + "format:prettier": "prettier --check './*.js' './*.mjs' './*.json' './*.md' 'scripts/**/*.js' '.github/actions/**/*.*'", + "format-fix": "npm-run-all --continue-on-error format-fix:*", + "format-fix:prettier": "prettier --write './*.js' './*.mjs' './*.json' './*.md' 'scripts/**/*.js' '.github/actions/**/*.*'", + "scan": "npm-run-all --continue-on-error scan:*", + "scan:semgrep": "semgrep --config=p/ci --config=p/security-audit --config=p/javascript ./*.js ./*.mjs ./*.json scripts/ .github/actions/", + "scan:gha": "mise run scan:gha", + "ci": "npm-run-all --continue-on-error audit test lint format scan" } } From b221ab05028ed6027e4e189ed434a71a77cfdaca Mon Sep 17 00:00:00 2001 From: ssvoss Date: Mon, 19 Oct 2026 21:53:58 -0400 Subject: [PATCH 4/4] CORE-6123: run zizmor offline locally and in CI zizmor's online audits (impostor-commit, known-vulnerable-actions, ref-confusion, stale-action-refs) list tags and commits of every pinned action. The job's github.token can only read its own repo, so the audit errored on OpenSesame/gha-oidc-access, a private action used by every Terraform and deploy repo. scan:gha now runs zizmor --offline, matching the ai-tooling repository standard, and the workflow passes no token. Task names stay lint:gha and scan:gha. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/CHANGELOGS/run_infra_ci.md | 2 +- .github/workflows/READMES/run_infra_ci.md | 5 +++-- .github/workflows/run_infra_ci.yml | 4 +--- mise.toml | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/CHANGELOGS/run_infra_ci.md b/.github/workflows/CHANGELOGS/run_infra_ci.md index b16fc8a..89eec3d 100644 --- a/.github/workflows/CHANGELOGS/run_infra_ci.md +++ b/.github/workflows/CHANGELOGS/run_infra_ci.md @@ -11,5 +11,5 @@ All notable changes to the **run_infra_ci** reusable workflow are documented in running the consuming repository's `lint:gha` or `scan:gha` mise task with GitHub annotation output. - A contract check that fails with a README link when `mise.toml` or the job's task is missing. - mise 2026.9.17 installed through `jdx/mise-action` 5.0.0, with the binary checksum pinned. -- zizmor online audits using the job's `github.token`. +- zizmor runs offline (`scan:gha` uses `--offline`), so no token is needed. - Job summaries with the result, the suppression count, and the local command. diff --git a/.github/workflows/READMES/run_infra_ci.md b/.github/workflows/READMES/run_infra_ci.md index 6bb7938..4edb88e 100644 --- a/.github/workflows/READMES/run_infra_ci.md +++ b/.github/workflows/READMES/run_infra_ci.md @@ -36,7 +36,7 @@ needs. run = "actionlint" [tasks."scan:gha"] - run = "zizmor .github" + run = "zizmor --offline .github" [tasks.infra-ci] depends = ["lint:gha", "scan:gha"] @@ -54,7 +54,8 @@ needs. 4. npm repos: add `"lint:gha": "mise run lint:gha"` and `"scan:gha": "mise run scan:gha"`. CI calls specific scripts, never the `lint`/`scan` aggregates, so CI jobs don't need mise. -Run locally with `mise run infra-ci`. zizmor runs offline unless `GH_TOKEN` is set; CI runs it online. +Run locally with `mise run infra-ci`. zizmor runs `--offline` locally and in CI: its online audits need a +token that can read the org's private action repos. ## Findings diff --git a/.github/workflows/run_infra_ci.yml b/.github/workflows/run_infra_ci.yml index e97fd1a..d3d5c73 100644 --- a/.github/workflows/run_infra_ci.yml +++ b/.github/workflows/run_infra_ci.yml @@ -125,8 +125,6 @@ jobs: - name: Run zizmor id: run - env: - GH_TOKEN: ${{ github.token }} run: | status=0 mise run scan:gha -- --format=github || status=$? @@ -159,5 +157,5 @@ jobs: echo "" echo "- **Suppressions** (\`$config_file\` \`rules.*.ignore\`): $entries" echo "- **Inline ignores** (\`# zizmor: ignore[...]\` under \`.github\`): $inline" - echo "- **Run locally**: \`mise run scan:gha\` (offline unless \`GH_TOKEN\` is set)" + echo "- **Run locally**: \`mise run scan:gha\` " } >> "$GITHUB_STEP_SUMMARY" diff --git a/mise.toml b/mise.toml index c5b7465..5a1e3d6 100644 --- a/mise.toml +++ b/mise.toml @@ -9,7 +9,7 @@ run = "actionlint" [tasks."scan:gha"] description = "Audit GitHub Actions workflows for security issues" -run = "zizmor .github" +run = "zizmor --offline .github" [tasks.infra-ci] description = "Run every infra-ci check"