@@ -9,8 +9,153 @@ permissions:
99 contents : write
1010
1111jobs :
12+ release-guard :
13+ runs-on : ubuntu-latest
14+ steps :
15+ - uses : actions/checkout@v4
16+ with :
17+ fetch-depth : 0
18+
19+ - name : Release guard checks
20+ env :
21+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
22+ REPO_NAME : ${{ github.event.repository.name }}
23+ REPO_FULL : ${{ github.repository }}
24+ TAG_NAME : ${{ github.ref_name }}
25+ run : |
26+ set -euo pipefail
27+
28+ fail() {
29+ echo "::error::$1"
30+ exit 1
31+ }
32+
33+ warn() {
34+ echo "::warning::$1"
35+ }
36+
37+ api_get() {
38+ local endpoint="$1"
39+ curl -fsSL \
40+ -H "Authorization: Bearer $GITHUB_TOKEN" \
41+ -H "Accept: application/vnd.github+json" \
42+ "https://api.github.com/repos/${REPO_FULL}${endpoint}"
43+ }
44+
45+ api_get_best_effort() {
46+ local endpoint="$1"
47+ local out_file="$2"
48+ local code
49+ code=$(curl -sS -o "$out_file" -w "%{http_code}" \
50+ -H "Authorization: Bearer $GITHUB_TOKEN" \
51+ -H "Accept: application/vnd.github+json" \
52+ "https://api.github.com/repos/${REPO_FULL}${endpoint}")
53+ echo "$code"
54+ }
55+
56+ expected_checks_for_repo() {
57+ case "$REPO_NAME" in
58+ repo-template-base)
59+ cat <<'EOF'
60+ core-validation
61+ pr-title
62+ EOF
63+ ;;
64+ repo-template-python)
65+ cat <<'EOF'
66+ core-gated (3.10)
67+ core-gated (3.11)
68+ core-gated (3.12)
69+ pr-title
70+ EOF
71+ ;;
72+ repo-template-flutter)
73+ cat <<'EOF'
74+ core-gated
75+ pr-title
76+ EOF
77+ ;;
78+ repo-template-cpp-cmake)
79+ cat <<'EOF'
80+ build
81+ pr-title
82+ EOF
83+ ;;
84+ repo-template-cpp-family)
85+ cat <<'EOF'
86+ smoke
87+ pr-title
88+ EOF
89+ ;;
90+ *)
91+ cat <<'EOF'
92+ pr-title
93+ EOF
94+ ;;
95+ esac
96+ }
97+
98+ check_branch_protection() {
99+ local branch="$1"
100+ local expected
101+ expected="$(expected_checks_for_repo)"
102+
103+ local prot_file sig_file
104+ prot_file="$(mktemp)"
105+ sig_file="$(mktemp)"
106+ trap 'rm -f "$prot_file" "$sig_file"' EXIT
107+
108+ local prot_code
109+ prot_code="$(api_get_best_effort "/branches/${branch}/protection" "$prot_file")"
110+ if [ "$prot_code" != "200" ]; then
111+ warn "Best-effort protection check skipped for ${branch} : HTTP ${prot_code}"
112+ return 0
113+ fi
114+
115+ jq -e '.required_status_checks != null' "$prot_file" >/dev/null || fail "Missing required status checks protection on ${branch}"
116+ jq -e '.required_pull_request_reviews != null' "$prot_file" >/dev/null || fail "Missing required PR reviews protection on ${branch}"
117+ jq -e '.allow_force_pushes.enabled == false' "$prot_file" >/dev/null || fail "Force-push must be blocked on ${branch}"
118+ jq -e '.allow_deletions.enabled == false' "$prot_file" >/dev/null || fail "Branch deletion must be blocked on ${branch}"
119+
120+ local contexts
121+ contexts="$(jq -r '.required_status_checks.contexts[]?' "$prot_file")"
122+ while IFS= read -r check; do
123+ [ -n "$check" ] || continue
124+ echo "$contexts" | grep -Fx "$check" >/dev/null || fail "Required check '${check}' missing on ${branch}"
125+ done <<< "$expected"
126+
127+ local sig_code
128+ sig_code="$(api_get_best_effort "/branches/${branch}/protection/required_signatures" "$sig_file")"
129+ if [ "$sig_code" != "200" ]; then
130+ warn "Best-effort signature check skipped for ${branch} : HTTP ${sig_code}"
131+ return 0
132+ fi
133+ jq -e '.enabled == true' "$sig_file" >/dev/null || fail "Signed commits must be required on ${branch}"
134+
135+ echo "Protection check passed for ${branch}"
136+ }
137+
138+ echo "Checking working tree cleanliness"
139+ [ -z "$(git status --porcelain)" ] || fail "Repository is not clean at release time"
140+
141+ echo "Checking tag signature verification for ${TAG_NAME}"
142+ ref_json="$(api_get "/git/ref/tags/${TAG_NAME}")"
143+ ref_type="$(echo "$ref_json" | jq -r '.object.type')"
144+ ref_sha="$(echo "$ref_json" | jq -r '.object.sha')"
145+
146+ [ "$ref_type" = "tag" ] || fail "Tag ${TAG_NAME} is lightweight; signed annotated tags are required"
147+
148+ tag_json="$(api_get "/git/tags/${ref_sha}")"
149+ verified="$(echo "$tag_json" | jq -r '.verification.verified')"
150+ reason="$(echo "$tag_json" | jq -r '.verification.reason')"
151+ [ "$verified" = "true" ] || fail "Tag ${TAG_NAME} is not verified (reason : ${reason})"
152+
153+ echo "Checking required branch protections (best-effort via API)"
154+ check_branch_protection master
155+
12156 draft-release :
13157 runs-on : ubuntu-latest
158+ needs : release-guard
14159 steps :
15160 - name : Create or update draft release
16161 uses : ncipollo/release-action@v1
0 commit comments