Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/scripts/test-merge-candidate-workflows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ assert_job_needs "$ABI_STAGING_MERGE_GATE" \
validate-current-evidence abi-staging-exact-head-structure
grep -Fq 'contents: read' <<<"$protected_structure_job" ||
fail "candidate structural ABI job must remain read-only"
protected_final_job=$(job_block \
"$ABI_STAGING_MERGE_GATE" validate-current-evidence)
grep -Fq 'statuses: write' <<<"$protected_final_job" ||
fail "protected ABI gate must own only exact commit-status publication"
grep -Fq 'contents: read' <<<"$protected_final_job" ||
fail "protected ABI gate must not own branch-write authority"
grep -Fq 'env -u GH_TOKEN -u GITHUB_TOKEN -u ACTIONS_RUNTIME_TOKEN' \
<<<"$protected_structure_step" ||
fail "candidate structural ABI code must not receive workflow credentials"
Expand All @@ -307,6 +313,18 @@ do
grep -Fq -- "$exact_gate_contract" <<<"$protected_provenance_step" ||
fail "protected ABI gate lacks exact provenance contract: $exact_gate_contract"
done
for exact_status_contract in \
'if [[ $mode != enforce ]]' \
'observe mode never publishes merge-gate authority' \
'"/repos/$GITHUB_REPOSITORY/statuses/$PR_HEAD_SHA"' \
'-f state=success' \
'-f context=merge-gate' \
"-f description='Exact staged bottles and product evidence succeeded.'" \
'-f target_url="$details_url"'
do
grep -Fq -- "$exact_status_contract" <<<"$protected_final_step" ||
fail "protected ABI gate lacks exact status contract: $exact_status_contract"
done
for exact_projection_contract in \
'check-projection project' \
'cmp -s' \
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/abi-staging-merge-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ jobs:
actions: read
checks: read
contents: read
statuses: write
env:
PR_HEAD_REF: ${{ needs.capture-current-subject.outputs.exact-ref }}
PR_HEAD_SHA: ${{ needs.capture-current-subject.outputs.exact-head }}
Expand Down Expand Up @@ -741,4 +742,15 @@ jobs:
staging_problem "pull-request head or merge-preparation label changed"
exit $?
fi
if [[ $mode != enforce ]]; then
echo "::notice::observe mode never publishes merge-gate authority"
exit 0
fi
gh api -X POST \
-H 'Accept: application/vnd.github+json' \
"/repos/$GITHUB_REPOSITORY/statuses/$PR_HEAD_SHA" \
-f state=success \
-f context=merge-gate \
-f description='Exact staged bottles and product evidence succeeded.' \
-f target_url="$details_url"
echo "Current exact-head Kandelo PR Check succeeded: $expected_external_id"
2 changes: 1 addition & 1 deletion abi/staging/request-policy.generated.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion abi/staging/required-check-activation.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
schema = 1
kind = "kandelo-abi-staging-required-check-activation"
mode = "observe"
mode = "enforce"
76 changes: 72 additions & 4 deletions scripts/check-abi-staging-pr-check-workflow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ def check_merge_gate(workflow)
check(structure.fetch("permissions") == {"contents" => "read"},
"exact-head structure must remain read-only")
check(validate.fetch("permissions") == {
"actions" => "read", "checks" => "read", "contents" => "read"
"actions" => "read", "checks" => "read", "contents" => "read",
"statuses" => "write"
}, "protected evidence validation permissions changed")
check(Array(structure.fetch("needs")) == ["capture-current-subject"],
"structural job dependency changed")
Expand Down Expand Up @@ -396,9 +397,36 @@ def check_merge_gate(workflow)
final_source.include?("cmp -s") &&
final_source.include?("published_conclusion == \"success\"") &&
final_source.include?("computed_conclusion == \"success\"") &&
final_source.include?("if [[ $mode != enforce ]]") &&
final_source.include?(
"::notice::observe mode never publishes merge-gate authority"
) &&
final_source.include?(
'"/repos/$GITHUB_REPOSITORY/statuses/$PR_HEAD_SHA"'
) &&
final_source.include?("-f state=success") &&
final_source.include?("-f context=merge-gate") &&
final_source.include?(
"-f description='Exact staged bottles and product evidence succeeded.'"
) &&
final_source.include?('-f target_url="$details_url"') &&
final_source.include?("staging_problem") &&
!final_source.include?("SYNTHETIC_MERGE_SHA"),
"final gate does not reproject exact protected success")
"final gate does not reproject and publish exact protected success")
check_revalidation = final_source.index(
'"/repos/$GITHUB_REPOSITORY/check-runs/$check_id"'
)
pull_revalidation = final_source.index(
'"/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER"'
)
enforce_guard = final_source.index("if [[ $mode != enforce ]]")
status_write = final_source.index(
'"/repos/$GITHUB_REPOSITORY/statuses/$PR_HEAD_SHA"'
)
check(check_revalidation && pull_revalidation && enforce_guard && status_write &&
check_revalidation < pull_revalidation &&
pull_revalidation < enforce_guard && enforce_guard < status_write,
"merge-gate authority is not published after exact current revalidation")

validate.fetch("steps").each do |step|
if step.fetch("uses", "").start_with?("./")
Expand Down Expand Up @@ -527,8 +555,11 @@ def rejected_mutation(workflow, label)
"PR-controlled trigger" => lambda { |copy|
copy["on"] = {"pull_request" => {"types" => ["labeled"]}}
},
"write-capable gate" => lambda { |copy|
copy.dig("jobs", "validate-current-evidence", "permissions")["checks"] = "write"
"missing status authority" => lambda { |copy|
copy.dig("jobs", "validate-current-evidence", "permissions").delete("statuses")
},
"branch-write authority" => lambda { |copy|
copy.dig("jobs", "validate-current-evidence", "permissions")["contents"] = "write"
},
"synthetic structural head" => lambda { |copy|
step = copy.dig("jobs", "abi-staging-exact-head-structure", "steps").find do |item|
Expand Down Expand Up @@ -608,6 +639,43 @@ def rejected_mutation(workflow, label)
end
step["if"] = "${{ success() }}"
},
"status write before revalidation" => lambda { |copy|
step = copy.dig("jobs", "validate-current-evidence", "steps").find do |item|
item["name"] == "Reproject and validate protected Check provenance"
end
source = step.fetch("run")
block_start = source.index("if [[ $mode != enforce ]]")
block_end = source.index('-f target_url="$details_url"', block_start)
raise "status publication block is absent" unless block_start && block_end
block_end = source.index("\n", block_end) || source.length
block = source[block_start...block_end]
step["run"] = block + "\n" + source.sub(block, "")
},
"status write in observe mode" => lambda { |copy|
step = copy.dig("jobs", "validate-current-evidence", "steps").find do |item|
item["name"] == "Reproject and validate protected Check provenance"
end
step["run"] = step.fetch("run").sub(
"if [[ $mode != enforce ]]", "if [[ $mode == enforce ]]"
)
},
"wrong status context" => lambda { |copy|
step = copy.dig("jobs", "validate-current-evidence", "steps").find do |item|
item["name"] == "Reproject and validate protected Check provenance"
end
step["run"] = step.fetch("run").sub(
"-f context=merge-gate", "-f context=other-gate"
)
},
"status on protected SHA" => lambda { |copy|
step = copy.dig("jobs", "validate-current-evidence", "steps").find do |item|
item["name"] == "Reproject and validate protected Check provenance"
end
step["run"] = step.fetch("run").sub(
'"/repos/$GITHUB_REPOSITORY/statuses/$PR_HEAD_SHA"',
'"/repos/$GITHUB_REPOSITORY/statuses/$PROTECTED_SHA"'
)
},
"swallowed provenance validation" => lambda { |copy|
step = copy.dig("jobs", "validate-current-evidence", "steps").find do |item|
item["name"] == "Validate current request and locate protected Check provenance"
Expand Down
11 changes: 11 additions & 0 deletions tools/xtask/src/abi_staging/check_projection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2134,6 +2134,17 @@ mod tests {
assert!(required_check_activation_mode(root.path()).is_err());
}

#[test]
fn checked_in_required_check_activation_enforces_current_evidence() {
let activation_path =
crate::repo_root().join("abi/staging/required-check-activation.toml");
let bytes = read_bounded_regular_file(&activation_path, MAX_ACTIVATION_BYTES).unwrap();
assert_eq!(
parse_required_check_activation(&activation_path, &bytes).unwrap(),
RequiredCheckActivationV1::Enforce,
);
}

#[test]
fn public_records_must_use_the_exact_candidate_namespace() {
let mut fixture = input();
Expand Down
Loading