Skip to content
Merged
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
145 changes: 145 additions & 0 deletions .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,153 @@ permissions:
contents: write

jobs:
release-guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Release guard checks
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO_NAME: ${{ github.event.repository.name }}
REPO_FULL: ${{ github.repository }}
TAG_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail

fail() {
echo "::error::$1"
exit 1
}

warn() {
echo "::warning::$1"
}

api_get() {
local endpoint="$1"
curl -fsSL \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${REPO_FULL}${endpoint}"
}

api_get_best_effort() {
local endpoint="$1"
local out_file="$2"
local code
code=$(curl -sS -o "$out_file" -w "%{http_code}" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${REPO_FULL}${endpoint}")
echo "$code"
}

expected_checks_for_repo() {
case "$REPO_NAME" in
repo-template-base)
cat <<'EOF'
core-validation
pr-title
EOF
;;
repo-template-python)
cat <<'EOF'
core-gated (3.10)
core-gated (3.11)
core-gated (3.12)
pr-title
EOF
;;
repo-template-flutter)
cat <<'EOF'
core-gated
pr-title
EOF
;;
repo-template-cpp-cmake)
cat <<'EOF'
build
pr-title
EOF
;;
repo-template-cpp-family)
cat <<'EOF'
smoke
pr-title
EOF
;;
*)
cat <<'EOF'
pr-title
EOF
;;
esac
}

check_branch_protection() {
local branch="$1"
local expected
expected="$(expected_checks_for_repo)"

local prot_file sig_file
prot_file="$(mktemp)"
sig_file="$(mktemp)"
trap 'rm -f "$prot_file" "$sig_file"' EXIT

local prot_code
prot_code="$(api_get_best_effort "/branches/${branch}/protection" "$prot_file")"
if [ "$prot_code" != "200" ]; then
warn "Best-effort protection check skipped for ${branch}: HTTP ${prot_code}"
return 0
fi

jq -e '.required_status_checks != null' "$prot_file" >/dev/null || fail "Missing required status checks protection on ${branch}"
jq -e '.required_pull_request_reviews != null' "$prot_file" >/dev/null || fail "Missing required PR reviews protection on ${branch}"
jq -e '.allow_force_pushes.enabled == false' "$prot_file" >/dev/null || fail "Force-push must be blocked on ${branch}"
jq -e '.allow_deletions.enabled == false' "$prot_file" >/dev/null || fail "Branch deletion must be blocked on ${branch}"

local contexts
contexts="$(jq -r '.required_status_checks.contexts[]?' "$prot_file")"
while IFS= read -r check; do
[ -n "$check" ] || continue
echo "$contexts" | grep -Fx "$check" >/dev/null || fail "Required check '${check}' missing on ${branch}"
done <<< "$expected"

local sig_code
sig_code="$(api_get_best_effort "/branches/${branch}/protection/required_signatures" "$sig_file")"
if [ "$sig_code" != "200" ]; then
warn "Best-effort signature check skipped for ${branch}: HTTP ${sig_code}"
return 0
fi
jq -e '.enabled == true' "$sig_file" >/dev/null || fail "Signed commits must be required on ${branch}"

echo "Protection check passed for ${branch}"
}

echo "Checking working tree cleanliness"
[ -z "$(git status --porcelain)" ] || fail "Repository is not clean at release time"

echo "Checking tag signature verification for ${TAG_NAME}"
ref_json="$(api_get "/git/ref/tags/${TAG_NAME}")"
ref_type="$(echo "$ref_json" | jq -r '.object.type')"
ref_sha="$(echo "$ref_json" | jq -r '.object.sha')"

[ "$ref_type" = "tag" ] || fail "Tag ${TAG_NAME} is lightweight; signed annotated tags are required"

tag_json="$(api_get "/git/tags/${ref_sha}")"
verified="$(echo "$tag_json" | jq -r '.verification.verified')"
reason="$(echo "$tag_json" | jq -r '.verification.reason')"
[ "$verified" = "true" ] || fail "Tag ${TAG_NAME} is not verified (reason: ${reason})"

echo "Checking required branch protections (best-effort via API)"
check_branch_protection master

draft-release:
runs-on: ubuntu-latest
needs: release-guard
steps:
- name: Create or update draft release
uses: ncipollo/release-action@v1
Expand Down