diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ca95c2fe2f..486400a035 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -35,10 +35,67 @@ jobs: instance: ${{ steps.validate.outputs.instance }} has_bazel: ${{ steps.validate.outputs.has_bazel }} steps: + - id: resolve + name: Resolve live pull request merge ref + if: ${{ github.event_name == 'pull_request_target' }} + env: + D2B_PR_NUMBER: ${{ github.event.pull_request.number || '' }} + D2B_BASE_REF: ${{ github.base_ref }} + D2B_REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + + fail() { + echo "::error::$1" >&2 + exit 76 + } + + is_sha() { + [[ "$1" =~ ^[0-9a-f]{40}$ ]] + } + + [ "$D2B_REPOSITORY" = "vicondoa/d2b" ] || + fail "the trusted build is restricted to vicondoa/d2b" + case "$D2B_BASE_REF" in + main|v3) ;; + *) fail "pull request target is not main or v3" ;; + esac + [ -n "$D2B_PR_NUMBER" ] && [[ "$D2B_PR_NUMBER" =~ ^[0-9]+$ ]] || + fail "pull request number is invalid" + + advertised_refs="$( + git ls-remote --exit-code "https://github.com/$D2B_REPOSITORY.git" \ + "refs/heads/$D2B_BASE_REF" \ + "refs/pull/$D2B_PR_NUMBER/head" \ + "refs/pull/$D2B_PR_NUMBER/merge" + )" || fail "live pull request refs are unavailable" + + advertised_sha() { + local ref="$1" line remote_sha remote_ref extra result="" count=0 + while IFS= read -r line; do + read -r remote_sha remote_ref extra <<<"$line" + if [ "$remote_ref" = "$ref" ]; then + count=$((count + 1)) + is_sha "$remote_sha" && [ -z "$extra" ] || + fail "live pull request ref is malformed: $ref" + result="$remote_sha" + fi + done <<<"$advertised_refs" + [ "$count" -eq 1 ] || + fail "live pull request ref is unavailable: $ref" + printf '%s\n' "$result" + } + + base_sha="$(advertised_sha "refs/heads/$D2B_BASE_REF")" + head_sha="$(advertised_sha "refs/pull/$D2B_PR_NUMBER/head")" + merge_sha="$(advertised_sha "refs/pull/$D2B_PR_NUMBER/merge")" + printf 'base_sha=%s\n' "$base_sha" >>"$GITHUB_OUTPUT" + printf 'head_sha=%s\n' "$head_sha" >>"$GITHUB_OUTPUT" + printf 'merge_sha=%s\n' "$merge_sha" >>"$GITHUB_OUTPUT" - name: Checkout immutable tested revision uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: - ref: ${{ github.event_name == 'push' && github.sha || github.event.pull_request.merge_commit_sha }} + ref: ${{ github.event_name == 'push' && github.sha || steps.resolve.outputs.merge_sha }} path: source fetch-depth: 0 persist-credentials: false @@ -51,7 +108,9 @@ jobs: D2B_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} D2B_BASE_SHA: ${{ github.event.pull_request.base.sha || github.sha }} D2B_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - D2B_MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha || github.sha }} + D2B_RESOLVED_BASE_SHA: ${{ steps.resolve.outputs.base_sha || '' }} + D2B_RESOLVED_HEAD_SHA: ${{ steps.resolve.outputs.head_sha || '' }} + D2B_RESOLVED_MERGE_SHA: ${{ steps.resolve.outputs.merge_sha || '' }} D2B_PR_NUMBER: ${{ github.event.pull_request.number || '' }} D2B_BRANCH: ${{ github.head_ref || github.ref_name }} D2B_RUN_ID: ${{ github.run_id }} @@ -112,8 +171,29 @@ jobs: fail "pull request number is invalid" is_sha "$D2B_BASE_SHA" || fail "pull request base SHA is invalid" is_sha "$D2B_HEAD_SHA" || fail "pull request head SHA is invalid" - is_sha "$D2B_MERGE_SHA" || fail "pull request merge SHA is invalid" - tested_sha="$D2B_MERGE_SHA" + tested_sha="$(git -C source rev-parse HEAD)" + is_sha "$tested_sha" || fail "live pull request merge ref did not resolve to a commit" + is_sha "$D2B_RESOLVED_BASE_SHA" || + fail "resolved pull request base SHA is invalid" + is_sha "$D2B_RESOLVED_HEAD_SHA" || + fail "resolved pull request head SHA is invalid" + is_sha "$D2B_RESOLVED_MERGE_SHA" || + fail "resolved pull request merge SHA is invalid" + [ "$tested_sha" = "$D2B_RESOLVED_MERGE_SHA" ] || + fail "tested checkout does not match the resolved pull request merge ref" + [ "$D2B_RESOLVED_HEAD_SHA" = "$D2B_HEAD_SHA" ] || + fail "pull request head changed during workflow dispatch" + read -r merge_parent_base merge_parent_head merge_parent_extra <<< \ + "$(git -C source show -s --format='%P' "$tested_sha")" + is_sha "$merge_parent_base" && is_sha "$merge_parent_head" && + [ -z "$merge_parent_extra" ] || + fail "live pull request merge ref must have exactly two parents" + D2B_BASE_SHA="$D2B_RESOLVED_BASE_SHA" + D2B_HEAD_SHA="$D2B_RESOLVED_HEAD_SHA" + [ "$merge_parent_base" = "$D2B_BASE_SHA" ] && + [ "$merge_parent_head" = "$D2B_HEAD_SHA" ] || + fail "live pull request merge ref does not bind the current base and head" + D2B_MERGE_SHA="$tested_sha" instance="d2b/pr/$D2B_PR_NUMBER/$D2B_HEAD_SHA/linux-x86_64/rules_rs/worker-v1/minimal/lock-v1" elif [ "$D2B_EVENT_NAME" = "push" ]; then [ "$D2B_CALLER_WORKFLOW_REF" = "vicondoa/d2b/.github/workflows/build.yaml@refs/heads/main" ] || @@ -123,10 +203,15 @@ jobs: fail "trusted build pushes are restricted to main" [ -z "$D2B_BASE_REF" ] || fail "push events must not carry a base ref" [ -z "$D2B_PR_NUMBER" ] || fail "push events must not carry PR metadata" - is_sha "$D2B_MERGE_SHA" || fail "pushed SHA is invalid" - D2B_BASE_SHA="$D2B_MERGE_SHA" - D2B_HEAD_SHA="$D2B_MERGE_SHA" - tested_sha="$D2B_MERGE_SHA" + is_sha "$D2B_CALLER_WORKFLOW_SHA" || + fail "pushed workflow SHA is invalid" + tested_sha="$(git -C source rev-parse HEAD)" + is_sha "$tested_sha" || fail "pushed revision is invalid" + [ "$tested_sha" = "$D2B_HEAD_SHA" ] || + fail "tested checkout does not match the pushed revision" + D2B_BASE_SHA="$tested_sha" + D2B_HEAD_SHA="$tested_sha" + D2B_MERGE_SHA="$tested_sha" instance="d2b/trusted-seed/main/$tested_sha/linux-x86_64/rules_rs/worker-v1/minimal/lock-v1" else fail "only pull_request_target and push events are accepted" @@ -183,6 +268,11 @@ jobs: D2B_BAZEL_PROFILE: local D2B_BAZEL_UNTRUSTED: "1" D2B_BAZEL_REQUIRE_REMOTE: "0" + # The build job also runs the nested local flake shards. Keep both + # layers serial on the hosted runner so Nix evaluation and Rust + # compilation cannot exhaust its memory before the gate reports. + D2B_CHECK_JOBS: "1" + D2B_FLAKE_JOBS: "1" steps: - name: Checkout immutable trusted controls uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 diff --git a/CHANGELOG.md b/CHANGELOG.md index f150a29b06..adc8b6c5a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,12 @@ deprecations ship one minor release before removal. identifiers whenever GitHub provides them. - Prepared the pinned Rust toolchain before parallel local Layer-1 gates so concurrent Rust and proof jobs do not race during rustup installation. +- Serialized the main push workflow's nested local Layer-1 fan-out on hosted + runners so concurrent Nix evaluation and Rust compilation cannot exhaust the + runner before the aggregate check reports. +- Resolved pull-request tests from the live synthetic merge ref and verified + its immutable base and head parents instead of trusting a stale event merge + SHA. ## [1.4.1] - 2026-07-12 diff --git a/packages/xtask/tests/policy_ci.rs b/packages/xtask/tests/policy_ci.rs index 0c2200060a..c6983b8d6c 100644 --- a/packages/xtask/tests/policy_ci.rs +++ b/packages/xtask/tests/policy_ci.rs @@ -256,6 +256,20 @@ fn main_controlled_buildbuddy_workflows_preserve_trust_contract() { .find("\n remote:\n") .expect("build workflow must define a remote job"); let local_job = &build[local_job_start..local_job_end]; + let local_env_start = local_job + .find("\n env:\n") + .expect("local job must define a job-level environment"); + let local_env_end = local_env_start + + local_job[local_env_start..] + .find("\n steps:\n") + .expect("local job environment must end before its steps"); + let local_env = &local_job[local_env_start..local_env_end]; + let local_env_lines = local_env.lines().collect::>(); + assert!( + local_env_lines.contains(&" D2B_CHECK_JOBS: \"1\"") + && local_env_lines.contains(&" D2B_FLAKE_JOBS: \"1\""), + "main hosted local gate must export serialized Layer-1 bounds at job scope" + ); let rust_bootstrap = local_job .find(" - name: Prepare pinned Rust toolchain for parallel Layer-1 gates") .expect("local Layer-1 gate must prepare the shared Rust toolchain"); @@ -278,10 +292,32 @@ fn main_controlled_buildbuddy_workflows_preserve_trust_contract() { "credential-bearing remote execution must be restricted to trusted main pushes" ); assert!( - build.contains("refs/heads/$D2B_DEFAULT_BRANCH") + build.contains("name: Resolve live pull request merge ref") + && build.contains("if: ${{ github.event_name == 'pull_request_target' }}") + && build.contains("ref: ${{ github.event_name == 'push' && github.sha || steps.resolve.outputs.merge_sha }}") + && build.contains("git ls-remote --exit-code \"https://github.com/$D2B_REPOSITORY.git\"") + && build.contains("git -C source show -s --format='%P' \"$tested_sha\"") + && build.contains("refs/heads/$D2B_BASE_REF") + && build.contains("refs/pull/$D2B_PR_NUMBER/head") + && build.contains("D2B_RESOLVED_BASE_SHA") + && build.contains("D2B_RESOLVED_HEAD_SHA") + && build.contains("D2B_RESOLVED_MERGE_SHA") + && build.contains("base_sha=\"$(advertised_sha") + && build.contains("head_sha=\"$(advertised_sha") + && build.contains("merge_sha=\"$(advertised_sha") + && !build.contains("printf 'base_sha=%s\\n' \"$(advertised_sha") + && build.contains("[ \"$tested_sha\" = \"$D2B_RESOLVED_MERGE_SHA\" ]") + && build.contains("[ \"$D2B_RESOLVED_HEAD_SHA\" = \"$D2B_HEAD_SHA\" ]") + && build.contains("[ \"$merge_parent_base\" = \"$D2B_BASE_SHA\" ]") + && build.contains("[ \"$merge_parent_head\" = \"$D2B_HEAD_SHA\" ]") + && build.contains("live pull request refs are unavailable") + && build.contains("live pull request ref is malformed") + && build.contains("live pull request merge ref does not bind the current base and head") + && !build.contains("github.event.pull_request.merge_commit_sha") + && build.contains("refs/heads/$D2B_DEFAULT_BRANCH") && build.contains("secret = os.read(9, 4096)") && build.contains("9<&0"), - "PR ref validation and credential bootstrap must match GitHub's default-branch semantics" + "PR merge-ref validation and credential bootstrap must match GitHub's default-branch semantics" ); assert!( build.contains("redact") && build.contains("^warning:"),