mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 05:25:14 +09:00
The unit and controller test job has no module cache and runs on an ephemeral runner, so every run fetches every module from proxy.golang.org, and one transient stream reset fails the job. It now restores the module cache keyed on every go.sum in the tree and prefetches the modules of every tracked go.mod through a bounded retry, hack/go-mod-download.sh. The cache is written from main by the build cache warmer under the same key, because a cache saved by a PR run is visible to that PR only, while one saved on the default branch is readable from every PR. Every job that runs Go on the runner sets GOPROXY=https://proxy.golang.org|direct so any proxy error falls back to the origin. The release jobs get only that, not a cache. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la>
164 lines
7.1 KiB
YAML
164 lines
7.1 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 }}
|
|
# The pipe falls back to direct on any proxy error (hack/common-envs.mk).
|
|
GOPROXY: "https://proxy.golang.org|direct"
|
|
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
|