Skip to content

Commit 7de4caa

Browse files
committed
[ci] Make typed workflow bridges fail closed
Derive Miri job eligibility from the projected matrix instead of repeating the event policy in workflow YAML. Publish the same gate to the required-check aggregate so policy changes cannot select work that the workflow silently skips. Run the planner with an absolute privileged Bash, neutralized startup controls, and a command-local fixed PATH. Invoke an explicit child Bash so the cargo wrapper cannot select an ambient interpreter. Run typed cells with absolute Docker, a fixed /bin/bash entrypoint, privileged no-startup arguments, and an option terminator. Use absolute jq in the final gate. These bridges now fail if PATH, image startup state, or Bash startup state attempts to replace their commands. This changes transport and eligibility wiring, not selected coverage: pull requests still run 60 build and zero Miri cells. Full events still run 182 build cells and 64 Miri cells. Tests: offline zc tests Tests: ci/check_actions.sh Tests: cargo fmt --check Tests: git diff --check *Authored by an agent, posting via joshlf's account* gherrit-pr-id: Gg5onzlaf6nsoqoubbjg43tdiuxvvjrbc
1 parent 2a757e8 commit 7de4caa

3 files changed

Lines changed: 166 additions & 51 deletions

File tree

.github/workflows/ci.yml

Lines changed: 112 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,9 @@ name: Build & Tests
1515
- main
1616
pull_request:
1717
merge_group:
18-
# Useful for manually testing changes to this workflow. Because we check for
19-
# the merge queue by doing `github.event_name != 'pull_request'` rather than
20-
# `github.event_name == 'merge_group'`, triggering via workflow dispatch has
21-
# the effect of running all jobs, including those which would normally only be
22-
# run in the merge queue.
18+
# Useful for manually testing changes to this workflow. The typed planner
19+
# classifies this as a full event, so its plan enables all ordinary CI work,
20+
# including Miri.
2321
workflow_dispatch:
2422

2523
permissions:
@@ -61,8 +59,8 @@ jobs:
6159
# jobs consume only the checked selectors from each matrix cell and pass them
6260
# back to `cargo-zerocopy`, which reconstructs and executes the complete
6361
# command without interpreting matrix data as shell text. Keep the output
64-
# names coordinated with `tools/zc/src/github.rs` and the two `fromJSON`
65-
# expressions below.
62+
# names coordinated with `tools/zc/src/github.rs`, the two `fromJSON`
63+
# expressions below, and the Miri eligibility consumers below.
6664
plan_ci:
6765
name: Plan ordinary CI work
6866
runs-on: ubuntu-latest
@@ -71,9 +69,10 @@ jobs:
7169
outputs:
7270
build_matrix: ${{ steps.plan.outputs.build_matrix }}
7371
miri_matrix: ${{ steps.plan.outputs.miri_matrix }}
74-
defaults:
75-
run:
76-
working-directory: zerocopy
72+
# This gate is derived from the projected Miri matrix, not independently
73+
# from the event name. Keep it coordinated with `tools/zc/src/github.rs`,
74+
# the Miri job condition, and the required-check aggregation.
75+
miri_enabled: ${{ steps.plan.outputs.miri_enabled }}
7776
env:
7877
# upload-file-artifact requires its name to equal the path basename.
7978
# Keep this one value coordinated with the planner invocation and upload
@@ -86,11 +85,21 @@ jobs:
8685

8786
- name: Validate inputs and project the plan
8887
id: plan
88+
# The absolute custom shell removes Bash startup-control variables and
89+
# enables privileged mode so inherited startup files, shell options,
90+
# and exported functions cannot turn this step into a successful no-op.
91+
shell: /usr/bin/env -u BASH_ENV -u ENV -u SHELLOPTS -u BASHOPTS /bin/bash --noprofile --norc -p -euo pipefail -- {0}
92+
working-directory: zerocopy
8993
env:
9094
EVENT_NAME: ${{ github.event_name }}
95+
# The runner applies GITHUB_PATH after merging step env, so assign the
96+
# fixed path directly to the child command. Do not rely on cargo.sh's
97+
# /usr/bin/env bash shebang. Privileged mode also prevents an imported
98+
# function from replacing a builtin when this child Bash starts.
9199
run: |
92100
set -euo pipefail
93-
./cargo.sh ci github-plan \
101+
PATH=/home/runner/.cargo/bin:/usr/local/bin:/usr/bin:/bin \
102+
/bin/bash --noprofile --norc -p ./cargo.sh ci github-plan \
94103
--event "$EVENT_NAME" \
95104
--github-output "$GITHUB_OUTPUT" \
96105
--artifact "$RUNNER_TEMP/$CI_PLAN_ARTIFACT"
@@ -213,18 +222,48 @@ jobs:
213222
214223
# The matrix values are data, not shell fragments. `cargo-zerocopy`
215224
# validates these selectors against the plan for this event, reconstructs
216-
# the typed argv and environment, and executes the complete cell. Keep this
217-
# invocation coordinated with `tools/zc/src/cli.rs`; adding a command here
218-
# would bypass the checked execution model.
225+
# the typed argv and environment, and executes the complete cell. This step
226+
# deliberately does not use the job's generated Docker shell: auditing its
227+
# path would not prove that the generated wrapper still invokes Docker.
228+
# Instead, the complete bridge below is explicit. The absolute Docker path
229+
# relies on the hosted Ubuntu runner contract and prevents a PATH shim from
230+
# returning success without starting a container. The entrypoint override
231+
# and Bash startup options prevent image startup behavior or exported
232+
# functions from intercepting the typed executor argv. The option
233+
# terminator forces the inherited image value to be parsed as an image.
234+
# Keep this command and its options coordinated with `tools/zc/src/cli.rs`.
219235
- name: Execute checked build cell
236+
shell: /usr/bin/env -u BASH_ENV -u ENV -u SHELLOPTS -u BASHOPTS /bin/bash --noprofile --norc -p -euo pipefail -- {0}
237+
working-directory: zerocopy
220238
env:
221239
TOOLCHAIN: ${{ matrix.toolchain }}
222240
CRATE: ${{ matrix.crate }}
223241
FEATURE_PROFILE: ${{ matrix.feature_profile }}
224242
TARGET: ${{ matrix.target }}
225243
run: |
226244
set -euo pipefail
227-
./cargo.sh ci execute-build-cell \
245+
/usr/bin/docker run --rm \
246+
--workdir "$PWD" \
247+
-v /home/runner/work:/home/runner/work \
248+
-v /home/runner/.docker-cargo/registry:/root/.cargo/registry \
249+
-v /home/runner/.docker-cargo/git:/root/.cargo/git \
250+
-e GITHUB_ENV -e GITHUB_PATH -e GITHUB_STEP_SUMMARY -e GITHUB_OUTPUT -e GITHUB_WORKSPACE \
251+
-e CI -e GITHUB_ACTIONS -e GITHUB_ACTOR -e GITHUB_REPOSITORY -e GITHUB_SHA -e GITHUB_REF -e GITHUB_EVENT_NAME \
252+
-e TOOLCHAIN -e CRATE -e TARGET -e FEATURE_PROFILE \
253+
-e RUSTFLAGS -e RUSTDOCFLAGS -e MIRIFLAGS \
254+
-e CARGO_NET_RETRY -e RUSTUP_MAX_RETRIES \
255+
-e ZC_NIGHTLY_RUSTFLAGS -e ZC_NIGHTLY_MIRIFLAGS \
256+
-e ZC_SKIP_CARGO_SEMVER_CHECKS \
257+
-e GIT_CONFIG_COUNT=1 \
258+
-e GIT_CONFIG_KEY_0=safe.directory \
259+
-e "GIT_CONFIG_VALUE_0=*" \
260+
--entrypoint /bin/bash \
261+
-- \
262+
"$ZC_CI_IMAGE" \
263+
--noprofile \
264+
--norc \
265+
-p \
266+
./cargo.sh ci execute-build-cell \
228267
--event "$GITHUB_EVENT_NAME" \
229268
--package "$CRATE" \
230269
--toolchain "$TOOLCHAIN" \
@@ -319,24 +358,26 @@ jobs:
319358
matrix.target != 'wasm32-unknown-unknown' &&
320359
env.ZC_SKIP_CARGO_SEMVER_CHECKS != '1'
321360
322-
# Miri runs only on full events because it is much more expensive than the
323-
# ordinary build matrix. Each borrow model is its own matrix cell so the two
324-
# models and all eligible target/profile combinations can run concurrently.
361+
# Today's policy selects Miri only for full events because it is much more
362+
# expensive than the ordinary build matrix. Each borrow model is its own
363+
# matrix cell so the models and all eligible target/profile combinations can
364+
# run concurrently. The job itself follows the plan rather than repeating
365+
# that current policy.
325366
miri:
326-
if: github.event_name != 'pull_request'
367+
# The planner derives this output solely from whether it selected any Miri
368+
# cells. Do not duplicate today's PR/full-event policy here: future policy
369+
# changes must automatically enable or skip this job with the matrix they
370+
# produce.
371+
if: needs.plan_ci.outputs.miri_enabled == 'true'
327372
runs-on: ubuntu-latest
328373
needs: [build_docker_env, plan_ci]
329374
permissions:
330375
contents: read
331-
defaults:
332-
run:
333-
shell: /tmp/docker-shell.sh {0} # zizmor: ignore[misfeature] (CI intentionally routes Miri through the prebuilt Docker image)
334-
working-directory: zerocopy
335376
strategy:
336377
fail-fast: false
337-
# Pull requests skip this job before matrix expansion. Full events consume
338-
# exactly the Miri cells selected by the same checked plan used by the
339-
# executor below.
378+
# A planner-disabled job skips before matrix expansion. When enabled, it
379+
# consumes exactly the Miri cells selected by the same checked plan used
380+
# by the executor below.
340381
matrix: ${{ fromJSON(needs.plan_ci.outputs.miri_matrix) }}
341382

342383
name: Miri (${{ matrix.crate }} / ${{ matrix.feature_profile }} / ${{ matrix.miri_model }} / ${{ matrix.target }})
@@ -345,12 +386,18 @@ jobs:
345386
- *matrix_checkout
346387
- *download_ci_image
347388
- *load_ci_image
348-
- *create_docker_shell
349389

350390
# As with ordinary cells, the workflow passes only selectors. Model flags,
351391
# feature arguments, the Cargo configuration transaction, and target
352392
# workarounds belong to the typed executor rather than this shell adapter.
393+
# This explicit Docker bridge does not rely on the generated job shell
394+
# actually executing its input. Its absolute Docker path, entrypoint
395+
# override, Bash startup options, and option terminator provide the same
396+
# fail-closed boundary as the ordinary build bridge. Keep the separate
397+
# Miri-model selector coordinated with `tools/zc/src/cli.rs`.
353398
- name: Execute checked Miri cell
399+
shell: /usr/bin/env -u BASH_ENV -u ENV -u SHELLOPTS -u BASHOPTS /bin/bash --noprofile --norc -p -euo pipefail -- {0}
400+
working-directory: zerocopy
354401
env:
355402
TOOLCHAIN: ${{ matrix.toolchain }}
356403
CRATE: ${{ matrix.crate }}
@@ -359,7 +406,28 @@ jobs:
359406
MIRI_MODEL: ${{ matrix.miri_model }}
360407
run: |
361408
set -euo pipefail
362-
./cargo.sh ci execute-miri-cell \
409+
/usr/bin/docker run --rm \
410+
--workdir "$PWD" \
411+
-v /home/runner/work:/home/runner/work \
412+
-v /home/runner/.docker-cargo/registry:/root/.cargo/registry \
413+
-v /home/runner/.docker-cargo/git:/root/.cargo/git \
414+
-e GITHUB_ENV -e GITHUB_PATH -e GITHUB_STEP_SUMMARY -e GITHUB_OUTPUT -e GITHUB_WORKSPACE \
415+
-e CI -e GITHUB_ACTIONS -e GITHUB_ACTOR -e GITHUB_REPOSITORY -e GITHUB_SHA -e GITHUB_REF -e GITHUB_EVENT_NAME \
416+
-e TOOLCHAIN -e CRATE -e TARGET -e FEATURE_PROFILE -e MIRI_MODEL \
417+
-e RUSTFLAGS -e RUSTDOCFLAGS -e MIRIFLAGS \
418+
-e CARGO_NET_RETRY -e RUSTUP_MAX_RETRIES \
419+
-e ZC_NIGHTLY_RUSTFLAGS -e ZC_NIGHTLY_MIRIFLAGS \
420+
-e ZC_SKIP_CARGO_SEMVER_CHECKS \
421+
-e GIT_CONFIG_COUNT=1 \
422+
-e GIT_CONFIG_KEY_0=safe.directory \
423+
-e "GIT_CONFIG_VALUE_0=*" \
424+
--entrypoint /bin/bash \
425+
-- \
426+
"$ZC_CI_IMAGE" \
427+
--noprofile \
428+
--norc \
429+
-p \
430+
./cargo.sh ci execute-miri-cell \
363431
--event "$GITHUB_EVENT_NAME" \
364432
--package "$CRATE" \
365433
--toolchain "$TOOLCHAIN" \
@@ -768,32 +836,39 @@ jobs:
768836

769837
# GitHub can omit a job output at promotion time if its secret scanner
770838
# produces a false positive. Keep this comparison in expression space
771-
# so the large JSON never enters a process environment; a PR's empty
772-
# Miri plan is still the nonempty JSON value {"include":[]}.
839+
# so the large JSON never enters a process environment. A disabled Miri
840+
# plan is still the nonempty JSON value {"include":[]}; the boolean gate
841+
# must also be present and canonical before aggregation trusts it.
773842
- name: Require published planner outputs
774-
if: ${{ needs.plan_ci.result == 'success' && (needs.plan_ci.outputs.build_matrix == '' || needs.plan_ci.outputs.miri_matrix == '') }}
843+
if: ${{ needs.plan_ci.result == 'success' && (needs.plan_ci.outputs.build_matrix == '' || needs.plan_ci.outputs.miri_matrix == '' || (needs.plan_ci.outputs.miri_enabled != 'true' && needs.plan_ci.outputs.miri_enabled != 'false')) }}
844+
shell: /usr/bin/env -u BASH_ENV -u ENV -u SHELLOPTS -u BASHOPTS /bin/bash --noprofile --norc -p -euo pipefail -- {0}
775845
run: exit 1
776846

777847
- name: Require every dependency to succeed
848+
# Pin the absolute interpreter and remove Bash startup controls so a
849+
# job default, PATH shim, or exported function cannot turn this
850+
# required-check assertion into a successful no-op.
851+
shell: /usr/bin/env -u BASH_ENV -u ENV -u SHELLOPTS -u BASHOPTS /bin/bash --noprofile --norc -p -euo pipefail -- {0}
778852
env:
779-
EVENT_NAME: ${{ github.event_name }}
780853
# Do not serialize the entire `needs` object here: job outputs may
781854
# legitimately approach GitHub's configured output limit, while a
782855
# Linux process has a much smaller per-environment-value limit.
783856
# Results stay small regardless of matrix JSON size. Keep the
784-
# separate Miri result so the one intentional PR skip is still
785-
# identified rather than accepting an arbitrary skipped job.
857+
# separate Miri result so a planner-disabled Miri job is identified
858+
# rather than accepting an arbitrary skipped dependency.
786859
RESULTS_JSON: ${{ toJSON(needs.*.result) }}
860+
MIRI_ENABLED: ${{ needs.plan_ci.outputs.miri_enabled }}
787861
MIRI_RESULT: ${{ needs.miri.result }}
788862
run: |
789863
set -euo pipefail
790-
jq -e --arg event "$EVENT_NAME" --arg miri "$MIRI_RESULT" '
864+
/usr/bin/jq -e --arg enabled "$MIRI_ENABLED" --arg miri "$MIRI_RESULT" '
791865
type == "array" and length > 0 and
792-
if $event == "pull_request"
866+
if $enabled == "false"
793867
then $miri == "skipped" and
794868
([.[] | select(. == "skipped")] | length) == 1 and
795869
all(.[]; . == "success" or . == "skipped")
796-
else $miri == "success" and
870+
else $enabled == "true" and
871+
$miri == "success" and
797872
all(.[]; . == "success")
798873
end
799874
' <<< "$RESULTS_JSON"

tools/zc/src/cli.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,10 @@ mod tests {
644644
};
645645

646646
use super::{run, CliError, Command};
647-
use crate::execution::{BuildCellSelector, MiriCellSelector};
647+
use crate::{
648+
execution::{BuildCellSelector, MiriCellSelector},
649+
github::{BUILD_MATRIX_OUTPUT, MIRI_ENABLED_OUTPUT, MIRI_MATRIX_OUTPUT},
650+
};
648651

649652
fn strings(args: &[&str]) -> Vec<String> {
650653
args.iter().map(|arg| (*arg).to_owned()).collect()
@@ -896,7 +899,7 @@ mod tests {
896899
}
897900

898901
#[test]
899-
fn github_plan_publishes_both_outputs_from_one_checked_projection() {
902+
fn github_plan_publishes_all_outputs_from_one_checked_projection() {
900903
static NEXT: AtomicU64 = AtomicU64::new(0);
901904
let unique = NEXT.fetch_add(1, Ordering::Relaxed);
902905
let directory =
@@ -920,8 +923,9 @@ mod tests {
920923
.unwrap();
921924

922925
let job_outputs = fs::read_to_string(github_output).unwrap();
923-
assert!(job_outputs.starts_with("build_matrix={\"include\":["));
924-
assert!(job_outputs.ends_with("miri_matrix={\"include\":[]}\n"));
926+
assert!(job_outputs.starts_with(&format!("{BUILD_MATRIX_OUTPUT}={{\"include\":[")));
927+
assert!(job_outputs.contains(&format!("{MIRI_MATRIX_OUTPUT}={{\"include\":[]}}\n")));
928+
assert!(job_outputs.ends_with(&format!("{MIRI_ENABLED_OUTPUT}=false\n")));
925929
let artifact_json: serde_json::Value =
926930
serde_json::from_slice(&fs::read(artifact).unwrap()).unwrap();
927931
assert_eq!(artifact_json["event"], "pull_request");

0 commit comments

Comments
 (0)