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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ jobs:
repository: Life-USTC/server
ref: ${{ steps.server-openapi-ref.outputs.ref }}
path: .openapi-server
# The pin is checked for reachability from server main, which needs
# real history: a shallow checkout cannot answer the question, and
# `openapi-contract verify-reachable` refuses to guess.
fetch-depth: 0

- name: Fetch server main for the pin reachability check
run: |
git -C .openapi-server fetch --no-tags --quiet origin \
+refs/heads/main:refs/remotes/origin/main
git -C .openapi-server rev-parse --verify refs/remotes/origin/main >/dev/null

- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
Expand Down
12 changes: 10 additions & 2 deletions .github/workflows/openapi-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ jobs:
repository: Life-USTC/server
ref: ${{ steps.server.outputs.sha }}
path: .openapi-server
# Needed so the sync refuses to pin a commit that never landed on
# server main (a pull-request head stays fetchable by SHA forever).
fetch-depth: 0

- name: Fetch server main for the pin reachability check
run: |
git -C .openapi-server fetch --no-tags --quiet origin \
+refs/heads/main:refs/remotes/origin/main
git -C .openapi-server rev-parse --verify refs/remotes/origin/main >/dev/null

- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
Expand All @@ -62,8 +71,7 @@ jobs:
- name: Sync contract and generated client
run: |
make sync-openapi OPENAPI_SERVER_DIR=.openapi-server SERVER_COMMIT=${{ steps.server.outputs.sha }}
./scripts/openapi-contract verify
cmp -s .openapi-server/public/openapi.generated.json api/openapi.json
make check-openapi-sync OPENAPI_SERVER_DIR=.openapi-server
make generate

- name: Build, test, and vet
Expand Down
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ OPENAPI_SERVER_DIR ?= ../server
SERVER_COMMIT ?=
LDFLAGS := -ldflags "-X github.com/Life-USTC/CLI/internal/cmd/root.version=$(VERSION)"

.PHONY: build clean test lint vet install generate sync-openapi check-openapi-provenance check-openapi-sync
.PHONY: build clean test test-scripts lint vet install generate sync-openapi check-openapi-provenance check-openapi-reachability check-openapi-sync

build: check-openapi-provenance generate
go build $(LDFLAGS) -o life-ustc ./cmd/life-ustc
Expand All @@ -12,9 +12,12 @@ clean:
rm -f life-ustc
rm -rf dist/

test:
test: test-scripts
go test -race ./...

test-scripts:
./scripts/openapi-contract.test.sh

lint:
golangci-lint run ./...

Expand All @@ -34,5 +37,8 @@ sync-openapi:
check-openapi-provenance:
./scripts/openapi-contract verify

check-openapi-reachability:
./scripts/openapi-contract verify-reachable "$(OPENAPI_SERVER_DIR)"

check-openapi-sync:
./scripts/openapi-contract verify-source "$(OPENAPI_SERVER_DIR)"
70 changes: 69 additions & 1 deletion scripts/openapi-contract
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ readonly spec="api/openapi.json"
readonly provenance="api/openapi.provenance.json"

usage() {
echo "usage: $0 {pinned-sha|verify|verify-source SERVER_DIR|sync SERVER_DIR SERVER_COMMIT}" >&2
echo "usage: $0 {pinned-sha|verify|verify-reachable SERVER_DIR|verify-source SERVER_DIR|sync SERVER_DIR SERVER_COMMIT}" >&2
exit 2
}

Expand Down Expand Up @@ -56,9 +56,72 @@ verify() {
}
}

# Resolves the ref that stands for the released history of $repository inside
# SERVER_DIR. The pin must be reachable from it, so a ref that is simply absent
# has to fail loudly: a reachability check that cannot be evaluated is worse
# than no check at all.
server_main_ref() {
local server_dir="$1" candidate
local candidates=("refs/remotes/origin/main" "refs/heads/main")

if [[ -n "${OPENAPI_SERVER_MAIN_REF:-}" ]]; then
candidates=("$OPENAPI_SERVER_MAIN_REF")
fi

for candidate in "${candidates[@]}"; do
if git -C "$server_dir" rev-parse --verify --quiet "${candidate}^{commit}" >/dev/null; then
printf '%s\n' "$candidate"
return 0
fi
done

echo "cannot check pin reachability: none of ${candidates[*]} exist in $server_dir" >&2
echo "fetch the release branch first, e.g. git -C $server_dir fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main" >&2
exit 1
}

# $repository squash-merges, so every released commit is on main and a
# pull-request head commit never is. A PR head stays fetchable by SHA forever,
# so pinning one produces a client generated from a branch that was thrown
# away. Pinning is only safe when the commit is reachable from main.
verify_reachable() {
local server_dir="$1"
local commit main_ref status=0

commit="$(pinned_sha)"

[[ -d "$server_dir/.git" || -f "$server_dir/.git" ]] || {
echo "cannot check pin reachability: $server_dir is not a git checkout of $repository" >&2
exit 1
}
[[ "$(git -C "$server_dir" rev-parse --is-shallow-repository)" == "false" ]] || {
echo "cannot check pin reachability: $server_dir is a shallow clone, so ancestry cannot be evaluated" >&2
echo "check out $repository with fetch-depth: 0 before verifying the pin" >&2
exit 1
}
git -C "$server_dir" rev-parse --verify --quiet "${commit}^{commit}" >/dev/null || {
echo "cannot check pin reachability: pinned commit $commit is not present in $server_dir" >&2
exit 1
}

main_ref="$(server_main_ref "$server_dir")"
git -C "$server_dir" merge-base --is-ancestor "$commit" "$main_ref" || status=$?

if [[ "$status" -eq 1 ]]; then
echo "pinned server commit $commit is NOT reachable from $main_ref of $repository" >&2
echo "$repository squash-merges, so this pin is a pull-request head or another commit that never landed on main; the generated client would be built from a dead branch" >&2
echo "re-run: make sync-openapi OPENAPI_SERVER_DIR=$server_dir SERVER_COMMIT=<a commit on main>" >&2
exit 1
elif [[ "$status" -ne 0 ]]; then
echo "cannot check pin reachability: git merge-base --is-ancestor $commit $main_ref failed with status $status" >&2
exit 1
fi
}

verify_source() {
local server_dir="$1"
verify
verify_reachable "$server_dir"

local commit head source_spec
commit="$(pinned_sha)"
Expand Down Expand Up @@ -100,6 +163,7 @@ sync() {
printf '{\n "repository": "%s",\n "commit": "%s",\n "sha256": "%s"\n}\n' \
"$repository" "$commit" "$hash" >"$provenance"
verify
verify_reachable "$server_dir"
}

case "${1:-}" in
Expand All @@ -111,6 +175,10 @@ case "${1:-}" in
[[ $# -eq 1 ]] || usage
verify
;;
verify-reachable)
[[ $# -eq 2 ]] || usage
verify_reachable "$2"
;;
verify-source)
[[ $# -eq 2 ]] || usage
verify_source "$2"
Expand Down
Loading
Loading