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 \