From b238bab9e2d749470564fc460eb686d2b850e246 Mon Sep 17 00:00:00 2001 From: joshlf's Agent Date: Tue, 25 Aug 2026 01:30:38 +0000 Subject: [PATCH] [ci] Shadow typed plans in Actions Run the checked CI planner in parallel with Docker image construction and publish both compact matrices as job outputs. Upload the complete plan as an exact JSON artifact so reviewers can inspect every inclusion and exclusion. Keep the existing handwritten build and Miri matrices authoritative in this commit. The required-check aggregate waits for planning, so validation fails without changing executed test coverage if live repository inputs or workflow job inventory drift from the reviewed policy. Tests: ./ci/check_actions.sh Tests: ./ci/check_job_dependencies.sh Tests: CARGO_NET_OFFLINE=true cargo +1.93.1 test --manifest-path tools/Cargo.toml -p zc --lib gherrit-pr-id: Gx6vwydb7e2ijorcpxmqt3axxviika37l --- .../actions/upload-file-artifact/action.yml | 23 +++-- .github/workflows/ci.yml | 87 ++++++++++++++++--- ci/workflow-jobs.tsv | 1 + 3 files changed, 92 insertions(+), 19 deletions(-) diff --git a/.github/actions/upload-file-artifact/action.yml b/.github/actions/upload-file-artifact/action.yml index 8e98d18359..2741acda04 100644 --- a/.github/actions/upload-file-artifact/action.yml +++ b/.github/actions/upload-file-artifact/action.yml @@ -1,5 +1,5 @@ name: Upload file artifact -description: Publish one already-compressed file for jobs in this workflow run +description: Publish one exact file for jobs in this workflow run inputs: name: @@ -8,6 +8,10 @@ inputs: path: description: Exact path of the nonempty file to publish required: true + retention-days: + description: Days to retain the artifact, from 1 through 90 + required: false + default: "1" outputs: artifact-id: @@ -25,6 +29,7 @@ runs: env: ARTIFACT_NAME: ${{ inputs.name }} ARTIFACT_PATH: ${{ inputs.path }} + RETENTION_DAYS: ${{ inputs.retention-days }} run: | set -euo pipefail @@ -45,15 +50,19 @@ runs: echo "Artifact is missing or empty: $ARTIFACT_PATH" >&2 exit 1 fi + if [[ ! "$RETENTION_DAYS" =~ ^([1-9]|[1-8][0-9]|90)$ ]]; then + echo "Artifact retention must be an integer from 1 through 90: $RETENTION_DAYS" >&2 + exit 1 + fi artifact_size=$(stat --format=%s "$ARTIFACT_PATH") echo "Uploading $ARTIFACT_NAME ($artifact_size bytes)" | tee -a "$GITHUB_STEP_SUMMARY" - # Both current callers produce tar archives whose contents are already - # compressed (Docker's image layers use gzip; Anneal uses zstd). Version 7's - # direct-file mode avoids a redundant ZIP on upload and extraction on every - # consumer. Keep this coordinated with download-artifact v8 in - # `../download-artifact-with-retry/action.yml`, which understands direct + # The large tar callers are already compressed (Docker's image layers use + # gzip; Anneal uses zstd), while the CI plan caller needs its exact JSON + # bytes preserved for review. Version 7's direct-file mode handles both + # without a redundant ZIP. Keep this coordinated with download-artifact v8 + # in `../download-artifact-with-retry/action.yml`, which understands direct # artifacts and verifies their service-provided digest. - name: Upload artifact id: upload @@ -62,7 +71,7 @@ runs: name: ${{ inputs.name }} path: ${{ inputs.path }} if-no-files-found: error - retention-days: 1 + retention-days: ${{ inputs.retention-days }} archive: false # Artifacts are immutable. Delete an artifact with the same validated # name first so "Re-run all jobs" can republish it under a new ID. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cee70f0857..ffb5fcb4f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,54 @@ env: CARGO_ZEROCOPY_AUTO_INSTALL_TOOLCHAIN: 1 jobs: + # Build the typed plan in parallel with the Docker image. This job is + # deliberately unprivileged: it may select ordinary test work, but runner, + # action, permission, and command authority remain in this workflow. + # + # The first rollout only publishes the matrices and review artifact. The + # existing handwritten matrices remain authoritative until later commits + # consume these outputs after shadow validation on real workflow runs. + plan_ci: + name: Plan ordinary CI work + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + build_matrix: ${{ steps.plan.outputs.build_matrix }} + miri_matrix: ${{ steps.plan.outputs.miri_matrix }} + defaults: + run: + working-directory: zerocopy + env: + # upload-file-artifact requires its name to equal the path basename. + # Keep this one value coordinated with the planner invocation and upload + # below rather than repeating a load-bearing artifact name. + CI_PLAN_ARTIFACT: ci-plan.json + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Validate inputs and project the plan + id: plan + env: + EVENT_NAME: ${{ github.event_name }} + run: | + set -euo pipefail + ./cargo.sh ci github-plan \ + --event "$EVENT_NAME" \ + --github-output "$GITHUB_OUTPUT" \ + --artifact "$RUNNER_TEMP/$CI_PLAN_ARTIFACT" + + - name: Upload detailed plan for review + uses: ./.github/actions/upload-file-artifact + with: + name: ${{ env.CI_PLAN_ARTIFACT }} + path: ${{ runner.temp }}/${{ env.CI_PLAN_ARTIFACT }} + # This JSON is small and intended for PR review, unlike the large + # one-day fan-out archives used only by jobs in the same run. + retention-days: 14 + build_test: runs-on: ubuntu-latest # We pre-compute a Docker image containing most dependencies. Each job in @@ -1058,25 +1106,40 @@ jobs: # success in branch protection, so this aggregation must fail closed. if: ${{ always() }} runs-on: ubuntu-latest - needs: [build_test, miri, codegen, coverage, kani, check_be_aarch64, check_avr_atmega, check_fmt, check_tools, check_actions, check_readme, check_versions, check_msrv_is_minimal, check_stale_stderr, check-all-toolchains-tested, check-job-dependencies, check-todo, run-git-hooks, zizmor, build_docker_env] + needs: [build_test, miri, codegen, coverage, kani, check_be_aarch64, check_avr_atmega, check_fmt, check_tools, check_actions, check_readme, check_versions, check_msrv_is_minimal, check_stale_stderr, check-all-toolchains-tested, check-job-dependencies, check-todo, run-git-hooks, zizmor, build_docker_env, plan_ci] steps: - name: Reject workflow cancellation if: ${{ cancelled() }} run: exit 1 + # GitHub can omit a job output at promotion time if its secret scanner + # produces a false positive. Keep this comparison in expression space + # so the large JSON never enters a process environment; a PR's empty + # Miri plan is still the nonempty JSON value {"include":[]}. + - name: Require published planner outputs + if: ${{ needs.plan_ci.result == 'success' && (needs.plan_ci.outputs.build_matrix == '' || needs.plan_ci.outputs.miri_matrix == '') }} + run: exit 1 + - name: Require every dependency to succeed env: EVENT_NAME: ${{ github.event_name }} - NEEDS_JSON: ${{ toJSON(needs) }} + # Do not serialize the entire `needs` object here: job outputs may + # legitimately approach GitHub's configured output limit, while a + # Linux process has a much smaller per-environment-value limit. + # Results stay small regardless of matrix JSON size. Keep the + # separate Miri result so the one intentional PR skip is still + # identified rather than accepting an arbitrary skipped job. + RESULTS_JSON: ${{ toJSON(needs.*.result) }} + MIRI_RESULT: ${{ needs.miri.result }} run: | set -euo pipefail - jq -e --arg event "$EVENT_NAME" ' - type == "object" and length > 0 and - all( - to_entries[]; - if .key == "miri" and $event == "pull_request" - then .value.result == "skipped" - else .value.result == "success" - end - ) - ' <<< "$NEEDS_JSON" + jq -e --arg event "$EVENT_NAME" --arg miri "$MIRI_RESULT" ' + type == "array" and length > 0 and + if $event == "pull_request" + then $miri == "skipped" and + ([.[] | select(. == "skipped")] | length) == 1 and + all(.[]; . == "success" or . == "skipped") + else $miri == "success" and + all(.[]; . == "success") + end + ' <<< "$RESULTS_JSON" diff --git a/ci/workflow-jobs.tsv b/ci/workflow-jobs.tsv index e7a016bde1..8b9111bf38 100644 --- a/ci/workflow-jobs.tsv +++ b/ci/workflow-jobs.tsv @@ -45,6 +45,7 @@ workflow job role .github/workflows/ci.yml coverage static-ci .github/workflows/ci.yml kani static-ci .github/workflows/ci.yml miri planned +.github/workflows/ci.yml plan_ci static-ci .github/workflows/ci.yml run-git-hooks static-ci .github/workflows/ci.yml zizmor security .github/workflows/dependency-review.yml dependency-review security