mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 22:13:18 +09:00
162 lines
7.0 KiB
YAML
162 lines
7.0 KiB
YAML
name: API Review Gate
|
|
|
|
# Requires a review from a designated API owner (see API_OWNERS below) when a
|
|
# pull request makes a "sizeable" change to the Cozystack API surface: a new
|
|
# API group, a new resource, or a breaking change to an existing resource's
|
|
# schema. Detection is deterministic (cmd/api-gate); no LLM or network calls.
|
|
#
|
|
# The job runs on every PR (no path filter) so it can always report a status
|
|
# and therefore be marked as a required check in branch protection without
|
|
# leaving PRs that touch no API stuck in "pending". When nothing sizeable
|
|
# changed it passes immediately.
|
|
|
|
on:
|
|
pull_request:
|
|
types: [opened, synchronize, reopened]
|
|
# Re-evaluate when a review is submitted or dismissed so an approval by an
|
|
# API owner flips the check from failing to passing without a new push.
|
|
pull_request_review:
|
|
types: [submitted, dismissed]
|
|
|
|
concurrency:
|
|
group: api-review-gate-${{ github.event.pull_request.number }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
|
|
jobs:
|
|
api-review-gate:
|
|
name: Require API owner review for sizeable API changes
|
|
runs-on: ubuntu-22.04
|
|
env:
|
|
# Space-separated GitHub logins allowed to satisfy the gate. Edit here to
|
|
# change who can approve sizeable API changes.
|
|
API_OWNERS: "lllamnyp kvaps"
|
|
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
steps:
|
|
- name: Checkout head
|
|
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha }}
|
|
fetch-depth: 0
|
|
# Do not leave a usable token in the repo's git config: the PR-authored
|
|
# build and api-gate steps below must not have access to it. The token
|
|
# is scoped to the approval step alone, which is the only step that
|
|
# calls the GitHub API.
|
|
persist-credentials: false
|
|
|
|
- name: Set up Go
|
|
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
|
with:
|
|
go-version-file: go.mod
|
|
cache: true
|
|
|
|
- name: Materialize base checkout at the merge base
|
|
run: |
|
|
set -euo pipefail
|
|
# Diff against the PR's actual merge base, not the base branch tip:
|
|
# otherwise unrelated commits landing on the base branch while the PR
|
|
# is open would invent or hide API findings.
|
|
git fetch --no-tags origin "${BASE_REF}"
|
|
merge_base="$(git merge-base "${HEAD_SHA}" "origin/${BASE_REF}")"
|
|
echo "Merge base: ${merge_base}"
|
|
git worktree add --detach "${RUNNER_TEMP}/base" "${merge_base}"
|
|
|
|
- name: Build api-gate
|
|
run: |
|
|
set -euo pipefail
|
|
# Build the detector from a trusted checkout so a PR cannot neuter the
|
|
# gate by editing its own detector code — the tool analyzes the head as
|
|
# *data*, not as code. This selection stays inline in the workflow (an
|
|
# API-owner-protected file that is always present) rather than in a
|
|
# helper script: a helper would be read from the untrusted PR head,
|
|
# which both breaks PRs whose head predates the helper and lets a PR
|
|
# steer the selection.
|
|
#
|
|
# Precedence: prefer the merge-base checkout; when the merge base
|
|
# predates the commit that introduced the gate (cmd/api-gate absent
|
|
# there and in the equally old head) fall back to the base branch tip,
|
|
# which is equally trusted (protected branch, not PR-authored) and
|
|
# current — this unblocks PRs whose merge base is older than the gate
|
|
# without forcing a rebase. Bootstrap from head only while the tool
|
|
# exists nowhere on the base branch yet; in that window CODEOWNERS on
|
|
# cmd/api-gate, internal/apigate, and this workflow keeps tampering
|
|
# behind an API-owner review.
|
|
if [ -d "${RUNNER_TEMP}/base/cmd/api-gate" ]; then
|
|
build_dir="${RUNNER_TEMP}/base"
|
|
echo "Building api-gate from the trusted merge-base checkout."
|
|
elif git cat-file -e "origin/${BASE_REF}:cmd/api-gate" 2>/dev/null; then
|
|
git worktree add --detach "${RUNNER_TEMP}/base-tip" "origin/${BASE_REF}"
|
|
build_dir="${RUNNER_TEMP}/base-tip"
|
|
echo "Building api-gate from the ${BASE_REF} tip; the merge base predates the gate."
|
|
else
|
|
build_dir="${GITHUB_WORKSPACE}"
|
|
echo "::warning::api-gate not present on the base branch yet; building from head (bootstrap). CODEOWNERS enforces API-owner review on the gate's own files."
|
|
fi
|
|
( cd "${build_dir}" && go build -o "${RUNNER_TEMP}/api-gate" ./cmd/api-gate )
|
|
|
|
- name: Detect sizeable API changes
|
|
id: detect
|
|
run: |
|
|
set -uo pipefail
|
|
set +e
|
|
"${RUNNER_TEMP}/api-gate" \
|
|
--base "${RUNNER_TEMP}/base" \
|
|
--head "${GITHUB_WORKSPACE}" \
|
|
--format markdown > "${RUNNER_TEMP}/report.md"
|
|
code=$?
|
|
set -e
|
|
echo "code=${code}" >> "$GITHUB_OUTPUT"
|
|
cat "${RUNNER_TEMP}/report.md"
|
|
case "${code}" in
|
|
0) echo "sizeable=false" >> "$GITHUB_OUTPUT" ;;
|
|
2) echo "sizeable=true" >> "$GITHUB_OUTPUT" ;;
|
|
*) echo "::error::api-gate failed with exit code ${code}"; exit "${code}" ;;
|
|
esac
|
|
|
|
- name: Check for an API owner approval
|
|
if: steps.detect.outputs.sizeable == 'true'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
{
|
|
echo "## API Review Gate"
|
|
echo
|
|
cat "${RUNNER_TEMP}/report.md"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
# An APPROVED review by any API owner satisfies the gate, but only
|
|
# each reviewer's *latest* review *on the current head commit* counts:
|
|
# this drops approvals that a later commit invalidated, and approvals
|
|
# an owner subsequently superseded with CHANGES_REQUESTED. GitHub
|
|
# forbids self-approval, so a sizeable PR authored by an owner still
|
|
# needs the other owner.
|
|
approvals="$(
|
|
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate |
|
|
jq -rs --arg head "${HEAD_SHA}" '
|
|
add
|
|
| map(select(.commit_id == $head))
|
|
| sort_by(.submitted_at // "")
|
|
| group_by(.user.login)
|
|
| map(last)
|
|
| .[]
|
|
| select(.state == "APPROVED")
|
|
| .user.login
|
|
'
|
|
)"
|
|
|
|
for owner in ${API_OWNERS}; do
|
|
if grep -qxi "${owner}" <<<"${approvals}"; then
|
|
echo "Sizeable API change approved by @${owner}."
|
|
exit 0
|
|
fi
|
|
done
|
|
|
|
echo "::error::This PR makes a sizeable API change (new group, new resource, or breaking change) and needs an approving review from one of: ${API_OWNERS}. See the job summary for details."
|
|
exit 1
|