From 9c6ac5213e9578bcf567cfed02c8f8280f9f0acc Mon Sep 17 00:00:00 2001 From: Taylor Blau Date: Mon, 7 Sep 2026 15:59:03 -0500 Subject: [PATCH] codex: preserve release guards for reusable workflow calls The reusable release workflow accepts a source input for build-only calls. The controller still compares its trigger and job conditions with the push-only form, so an admitted topic cannot pass output verification. Check the callable workflow's publication, version, and release conditions before presenting its push-only form to the existing comparison. Retain the raw workflow for credential checks and keep the existing publication body, version dependency, and branch checks. Extend the release upgrade regression to cover callable workflows on both generated lanes. Reject weakened call conditions and extra release triggers, and verify that rejected candidates move no refs. Signed-off-by: Taylor Blau --- .github/workflows/codex-branch.sh | 53 +++++++++++-- t/t9905-codex-branch.sh | 123 +++++++++++++++++++++++++----- 2 files changed, 149 insertions(+), 27 deletions(-) diff --git a/.github/workflows/codex-branch.sh b/.github/workflows/codex-branch.sh index cac14bfddf60eb..ed698a5bdabc54 100755 --- a/.github/workflows/codex-branch.sh +++ b/.github/workflows/codex-branch.sh @@ -852,12 +852,53 @@ automation_workflow_matches () { "$tmp_dir/actual-automation.yml" } -extract_release_trigger () { +# Compare the push-only projection of a reusable workflow. Calls may skip +# provenance checks to build another source, but must never publish it. +read_release_workflow () ( head_oid=$1 output=$2 make_tmp_dir git show "$head_oid:.github/workflows/codex-release.yml" \ - >"$tmp_dir/codex-release.yml" 2>/dev/null || return 1 + >"$output" 2>/dev/null || return 1 + grep -F -x ' workflow_call:' "$output" >/dev/null || return 0 + while IFS="$tab" read -r job condition + do + extract_release_job "$output" "$job" "$tmp_dir/call-job" || + return 1 + actual=$(sed -n '/^ if:/p' "$tmp_dir/call-job") + test "$actual" = " if: $condition" || return 1 + done <<-'EOF' + publication inputs.source_sha == '' && github.event.deleted == false + version ${{ !cancelled() && (inputs.source_sha != '' || needs.publication.outputs.published == 'true') }} + release inputs.source_sha == '' + EOF + awk ' + $0 == "on:" { in_trigger = 1; skip = 0 } + in_trigger && /^ [^[:space:]]/ { + skip = ($0 == " workflow_call:") + } + in_trigger && /^[^[:space:]]/ && $0 != "on:" { + in_trigger = skip = 0 + } + skip { next } + $0 ~ /^ [^[:space:]]/ { job = $0 } + /^ if:/ && job == " publication:" { + $0 = " if: github.event.deleted == false" + } + /^ if:/ && job == " version:" { + $0 = " if: needs.publication.outputs.published == \047true\047" + } + { print } + ' "$output" >"$output.normalized" && + mv "$output.normalized" "$output" +) + +extract_release_trigger () { + head_oid=$1 + output=$2 + make_tmp_dir + read_release_workflow "$head_oid" "$tmp_dir/codex-release.yml" || + return 1 test "$(grep -c '^on:$' "$tmp_dir/codex-release.yml")" = 1 || return 1 awk ' $0 == "on:" && !found { @@ -1005,17 +1046,17 @@ release_publication_controls_preserved () ( make_tmp_dir old_workflow=$tmp_dir/published-release.yml new_workflow=$tmp_dir/candidate-release.yml - if ! git show "$published:.github/workflows/codex-release.yml" \ - >"$old_workflow" 2>/dev/null + if ! git cat-file -e \ + "$published:.github/workflows/codex-release.yml" 2>/dev/null then return 0 fi + read_release_workflow "$published" "$old_workflow" || return 1 if ! grep -F -x ' publication:' "$old_workflow" >/dev/null then return 0 fi - git show "$candidate:.github/workflows/codex-release.yml" \ - >"$new_workflow" 2>/dev/null || return 1 + read_release_workflow "$candidate" "$new_workflow" || return 1 extract_release_job "$old_workflow" publication \ "$tmp_dir/published-publication" || return 1 extract_release_job "$new_workflow" publication \ diff --git a/t/t9905-codex-branch.sh b/t/t9905-codex-branch.sh index 8b9ba6d9dc5081..ecdba502b458b7 100755 --- a/t/t9905-codex-branch.sh +++ b/t/t9905-codex-branch.sh @@ -286,6 +286,38 @@ write_dual_guarded_release_workflow () { EOF } +write_callable_release_workflow () { + write_dual_guarded_release_workflow "$1" && + awk ' + $0 == "on:" { + print + print " workflow_call:" + print " inputs:" + print " source_sha:" + print " required: true" + print " type: string" + next + } + $0 == " if: github.event.deleted == false" { + $0 = " if: inputs.source_sha == \047\047 && github.event.deleted == false" + } + $0 == " if: needs.publication.outputs.published == \047true\047" { + $0 = " if: ${{ !cancelled() && (inputs.source_sha != \047\047 || needs.publication.outputs.published == \047true\047) }}" + } + { print } + END { + print "" + print " release:" + print " if: inputs.source_sha == \047\047" + print " needs: version" + print " runs-on: ubuntu-24.04" + print " steps:" + print " - run: echo publish" + } + ' "$1" >"$1.callable" && + mv "$1.callable" "$1" +} + install_bootstrap_pin_guard_gh () { directory=$1 && mkdir -p "$directory" && @@ -2733,7 +2765,7 @@ test_expect_success 'published release provenance gates cannot be removed' ' done ' -test_expect_success 'release publication guard has one exact dual-lane upgrade' ' +test_expect_success 'release publication guard permits reviewed workflow upgrades' ' git init --bare release-upgrade.git && test_create_repo release-upgrade-source && ( @@ -2761,6 +2793,43 @@ test_expect_success 'release publication guard has one exact dual-lane upgrade' git add .github/workflows/codex-release.yml && git commit -m "publish dual release guard" && + git switch -c call-valid dual-valid && + write_callable_release_workflow \ + .github/workflows/codex-release.yml && + git add .github/workflows/codex-release.yml && + git commit -m "allow build-only workflow calls" && + for job in publication version release + do + git switch -c "call-bad-$job" call-valid && + awk -v target=" $job:" '\'' + /^ [^[:space:]]/ { job = $0 } + /^ if:/ && job == target { + $0 = " if: always()" + } + { print } + '\'' .github/workflows/codex-release.yml >release.tmp && + mv release.tmp .github/workflows/codex-release.yml && + git add .github/workflows/codex-release.yml && + git commit -m "drop $job call guard" || return 1 + done && + git switch -c call-extra-trigger call-valid && + awk '\'' + { print } + $0 == "on:" { print " workflow_dispatch:" } + '\'' .github/workflows/codex-release.yml >release.tmp && + mv release.tmp .github/workflows/codex-release.yml && + git add .github/workflows/codex-release.yml && + git commit -m "add an unreviewed release trigger" && + + git switch -c call-duplicate-trigger call-valid && + awk '\'' + $0 == " push:" { print "on:" } + { print } + '\'' .github/workflows/codex-release.yml >release.tmp && + mv release.tmp .github/workflows/codex-release.yml && + git add .github/workflows/codex-release.yml && + git commit -m "duplicate the release trigger mapping" && + git switch -c dual-old-guard "$legacy" && awk '\'' { print } @@ -2787,31 +2856,43 @@ test_expect_success 'release publication guard has one exact dual-lane upgrade' git push origin master meta codex codex-unstable \ dual-valid:aa/codex/release \ - dual-valid dual-old-guard dual-no-delete dual-wrong-key + dual-valid dual-old-guard dual-no-delete dual-wrong-key \ + call-valid call-bad-publication call-bad-version \ + call-bad-release call-extra-trigger call-duplicate-trigger ) && git clone release-upgrade.git release-upgrade-runner && ( cd release-upgrade-runner && fetch_all && - snapshot_refs ../release-upgrade.git >before && - sh "$codex_branch" rewrite --remote origin \ - --base master --codex codex \ - --result result --updates updates \ - --inputs inputs --failure failure && - candidate=$(cat result) && - unstable=$(updated_tip codex-unstable updates) && - write_dual_guarded_release_workflow expected-release.yml && - git show "$candidate:.github/workflows/codex-release.yml" \ - >actual-release.yml && - test_cmp expected-release.yml actual-release.yml && - stable_release=$(git rev-parse \ - "$candidate:.github/workflows/codex-release.yml") && - unstable_release=$(git rev-parse \ - "$unstable:.github/workflows/codex-release.yml") && - test "$stable_release" = "$unstable_release" && - snapshot_refs ../release-upgrade.git >after && - test_cmp before after && - for variant in dual-old-guard dual-no-delete dual-wrong-key + for variant in dual-valid call-valid + do + oid=$(git rev-parse "origin/$variant") && + git --git-dir=../release-upgrade.git update-ref \ + refs/heads/aa/codex/release "$oid" && + fetch_all && + snapshot_refs ../release-upgrade.git >before && + sh "$codex_branch" rewrite --remote origin \ + --base master --codex codex \ + --result result --updates updates \ + --inputs inputs --failure failure && + candidate=$(cat result) && + unstable=$(updated_tip codex-unstable updates) && + git show "$oid:.github/workflows/codex-release.yml" \ + >expected-release.yml && + git show "$candidate:.github/workflows/codex-release.yml" \ + >actual-release.yml && + test_cmp expected-release.yml actual-release.yml && + stable_release=$(git rev-parse \ + "$candidate:.github/workflows/codex-release.yml") && + unstable_release=$(git rev-parse \ + "$unstable:.github/workflows/codex-release.yml") && + test "$stable_release" = "$unstable_release" && + snapshot_refs ../release-upgrade.git >after && + test_cmp before after || return 1 + done && + for variant in dual-old-guard dual-no-delete dual-wrong-key \ + call-bad-publication call-bad-version call-bad-release \ + call-extra-trigger call-duplicate-trigger do oid=$(git rev-parse "origin/$variant") && git --git-dir=../release-upgrade.git update-ref \