Skip to content

Commit 086ab64

Browse files
authored
Merge pull request #2 from tinmanworks/codex/release-guard-checklist
ci(release): add lightweight release guard checks
2 parents 3d354f9 + cefd37a commit 086ab64

File tree

1 file changed

+145
-0
lines changed

1 file changed

+145
-0
lines changed

.github/workflows/release-tag.yml

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,153 @@ permissions:
99
contents: write
1010

1111
jobs:
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

Comments
 (0)