Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ jobs:
fi
echo "OK: no version-file changes outside a Release Please PR."
- run: make openapi-generated-check
- run: make release-workflow-check
- run: make lint
- run: make test
- run: go build ./...
Expand Down
42 changes: 28 additions & 14 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ on:
- main
workflow_dispatch:

# Serialize release runs so overlapping pushes don't race the release PR/tag.
# Serialize all release runs so manual and push events cannot race.
concurrency:
group: release-please-${{ github.ref }}
group: release-please
cancel-in-progress: false

# Least privilege for the default GITHUB_TOKEN: the release work runs on the
# GitHub App token minted below, so the default token needs no write scope.
# The default GITHUB_TOKEN reads PR checks. Release writes use the scoped
# GitHub App token minted below.
permissions:
checks: read
contents: read
pull-requests: read

jobs:
release-please:
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand Down Expand Up @@ -55,6 +58,7 @@ jobs:
# included so a workflow fix can recover a stalled release.
- name: Merge non-major release PRs
env:
CHECKS_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_REPO: ${{ github.repository }}
run: |
Expand All @@ -67,10 +71,7 @@ jobs:
exit 0
fi
current_major="${current_version%%.*}"
if ! gh api "repos/${GH_REPO}/branches/main/protection/required_status_checks" --jq '.contexts[]' | grep -Fxq 'check / check'; then
echo "Required release check 'check / check' is not configured; merging nothing."
exit 1
fi
expected_checks='["Analyze (actions)","Analyze (go)","Analyze (javascript-typescript)","Analyze (python)","Analyze (ruby)","CodeQL","check / check","check / localmode-e2e","license/cla"]'
gh pr list --state open --label 'autorelease: pending' --json number --jq '.[].number' | while read -r number; do
metadata="$(gh pr view "$number" --json author,headRefName,headRefOid,title)"
author="$(jq -r '.author.login' <<< "$metadata")"
Expand All @@ -94,22 +95,35 @@ jobs:
echo "Waiting for required checks on release PR #${number} (${current_version} -> ${new_version})."
checks_ready=false
for attempt in {1..30}; do
if checks_probe="$(gh pr checks "$number" --required --json name 2>&1)"; then
checks_ready=true
break
if checks_probe="$(GH_TOKEN="$CHECKS_TOKEN" gh pr checks "$number" --required --json name 2>&1)"; then
checks_status=0
else
checks_status=$?
fi
if ! grep -Eq 'no (required )?checks reported' <<< "$checks_probe"; then
if [ "$checks_status" -eq 0 ] || [ "$checks_status" -eq 8 ]; then
if jq -e --argjson expected "$expected_checks" '$expected - (map(.name)) | length == 0' <<< "$checks_probe" >/dev/null; then
checks_ready=true
break
fi
elif ! grep -Eq 'no (required )?checks reported' <<< "$checks_probe"; then
printf '%s\n' "$checks_probe" >&2
exit 1
fi
echo "Required checks are not registered yet (attempt ${attempt}/30)."
echo "Required checks are not all registered yet (attempt ${attempt}/30)."
sleep 10
done
if [ "$checks_ready" != true ]; then
echo "Required checks did not register for release PR #${number}." >&2
exit 1
fi
gh pr checks "$number" --required --watch --fail-fast
GH_TOKEN="$CHECKS_TOKEN" gh pr checks "$number" --required --watch --fail-fast
GH_TOKEN="$CHECKS_TOKEN" gh pr checks "$number" --watch --fail-fast
all_checks="$(GH_TOKEN="$CHECKS_TOKEN" gh pr checks "$number" --json bucket,name)"
if ! jq -e 'length > 0 and all(.[]; .bucket == "pass" or .bucket == "skipping")' <<< "$all_checks" >/dev/null; then
echo "Not all checks passed for release PR #${number}; merging nothing." >&2
printf '%s\n' "$all_checks" >&2
exit 1
fi
echo "Squash-merging release PR #${number} with the Volcano app ruleset bypass."
gh pr merge "$number" --admin --squash --match-head-commit "$head_oid"
done
44 changes: 42 additions & 2 deletions scripts/ci/check-release-workflow.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,60 @@
set -euo pipefail

workflow=".github/workflows/release-please.yml"
check_workflow=".github/workflows/check.yml"
expected_checks='["Analyze (actions)","Analyze (go)","Analyze (javascript-typescript)","Analyze (python)","Analyze (ruby)","CodeQL","check / check","check / localmode-e2e","license/cla"]'
grep -Fxq -- ' group: release-please' "$workflow" || {
echo "release workflow must serialize all refs in one concurrency group" >&2
exit 1
}

for required in \
" if: \${{ github.ref == 'refs/heads/main' }}" \
"gh pr list --state open --label 'autorelease: pending'" \
'required_status_checks' \
'checks: read' \
'pull-requests: read' \
"CHECKS_TOKEN: \${{ github.token }}" \
"head_oid=\"\$(jq -r '.headRefOid' <<< \"\$metadata\")\"" \
'for attempt in {1..30}' \
"checks_probe=\"\$(GH_TOKEN=\"\$CHECKS_TOKEN\" gh pr checks \"\$number\" --required --json name 2>&1)\"" \
"if [ \"\$checks_status\" -eq 0 ] || [ \"\$checks_status\" -eq 8 ]; then" \
'no (required )?checks reported' \
"gh pr checks \"\$number\" --required --watch --fail-fast" \
"expected_checks='$expected_checks'" \
"\$expected - (map(.name)) | length == 0" \
"GH_TOKEN=\"\$CHECKS_TOKEN\" gh pr checks \"\$number\" --required --watch --fail-fast" \
"GH_TOKEN=\"\$CHECKS_TOKEN\" gh pr checks \"\$number\" --watch --fail-fast" \
'all(.[]; .bucket == "pass" or .bucket == "skipping")' \
"gh pr merge \"\$number\" --admin --squash --match-head-commit \"\$head_oid\""; do
grep -Fq -- "$required" "$workflow" || {
echo "release workflow is missing: $required" >&2
exit 1
}
done

grep -Fq -- 'run: make release-workflow-check' "$check_workflow" || {
echo "CI does not run the release workflow check" >&2
exit 1
}

required_checks='[{"name":"Analyze (actions)"},{"name":"Analyze (go)"},{"name":"Analyze (javascript-typescript)"},{"name":"Analyze (python)"},{"name":"Analyze (ruby)"},{"name":"CodeQL"},{"name":"check / check"},{"name":"check / localmode-e2e"},{"name":"license/cla"}]'
jq -e --argjson expected "$expected_checks" '$expected - (map(.name)) | length == 0' <<< "$required_checks" >/dev/null
if jq -e --argjson expected "$expected_checks" '$expected - (map(.name)) | length == 0' <<< '[{"name":"check / check"}]' >/dev/null; then
echo "release check policy accepted an incomplete required-check set" >&2
exit 1
fi

check_success='length > 0 and all(.[]; .bucket == "pass" or .bucket == "skipping")'
jq -e "$check_success" <<< '[{"bucket":"pass"},{"bucket":"skipping"}]' >/dev/null
if jq -e "$check_success" <<< '[{"bucket":"pass"},{"bucket":"fail"}]' >/dev/null; then
echo "release check policy accepted a failed non-required check" >&2
exit 1
fi

if grep -Fq 'branches/main/protection' "$workflow"; then
echo "release workflow must not require administration access to read branch protection" >&2
exit 1
fi

if grep -Eq 'gh[[:space:]]+pr[[:space:]]+merge[[:space:]].*--auto([[:space:]]|$)' "$workflow"; then
echo "release workflow must use the app bypass instead of review-gated auto-merge" >&2
exit 1
Expand Down