Bump actions/checkout from 6 to 7 in the all-actions group #111
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ["v*"] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| test: | |
| name: Test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: stable | |
| cache: true | |
| - name: Build | |
| run: go build ./... | |
| - name: gofmt | |
| run: | | |
| out="$(gofmt -l .)" | |
| if [ -n "$out" ]; then | |
| echo "gofmt needed on:" && echo "$out" && exit 1 | |
| fi | |
| - name: Test (with race) | |
| run: go test -race -count=1 -timeout 120s ./... | |
| - name: Fuzz (short) | |
| run: | | |
| go test -fuzz=^FuzzParsePattern$ -fuzztime=10s ./internal/pattern/ | |
| go test -fuzz=^FuzzReflectSchema$ -fuzztime=10s -parallel 1 ./internal/schema/ | |
| - name: Vet | |
| run: go vet ./... | |
| # The supported floor is the go directive in go.mod. This job | |
| # computes every stable patch release >= that floor from go.dev and | |
| # fans the full suite out over all of them, so every version we | |
| # advertise compatibility with is actually tested — including | |
| # patches released after this file was written. | |
| versions: | |
| name: Supported Go versions | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.list.outputs.matrix }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - id: list | |
| name: Compute versions from the go.mod floor | |
| run: | | |
| floor="$(awk '/^go /{print $2}' go.mod)" | |
| curl -fsSL 'https://go.dev/dl/?mode=json&include=all' -o releases.json | |
| python3 - "$floor" <<'EOF' > versions.json | |
| import json, sys | |
| floor = tuple(int(x) for x in sys.argv[1].split(".")) | |
| out = [] | |
| for rel in json.load(open("releases.json")): | |
| if not rel.get("stable"): | |
| continue | |
| v = rel["version"].removeprefix("go") | |
| parts = v.split(".") | |
| if len(parts) == 2: | |
| parts.append("0") | |
| try: | |
| t = tuple(int(x) for x in parts) | |
| except ValueError: | |
| continue | |
| if t >= floor: | |
| out.append((t, v)) | |
| out.sort() | |
| print(json.dumps([v for _, v in out])) | |
| EOF | |
| echo "Supported versions: $(cat versions.json)" | |
| echo "matrix=$(cat versions.json)" >> "$GITHUB_OUTPUT" | |
| compat: | |
| name: Go ${{ matrix.go }} | |
| needs: versions | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| go: ${{ fromJSON(needs.versions.outputs.matrix) }} | |
| env: | |
| # Force the exact toolchain under test; if go.mod ever demands | |
| # a newer one, this fails loudly instead of silently upgrading. | |
| GOTOOLCHAIN: local | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ matrix.go }} | |
| cache: true | |
| - name: Toolchain | |
| run: go version | |
| - name: Build | |
| run: go build ./... | |
| - name: Vet | |
| run: go vet ./... | |
| - name: Test (with race) | |
| run: go test -race -count=1 -timeout 120s ./... | |
| - name: YAML roundtrip module | |
| working-directory: internal/spec/yaml/roundtrip_test | |
| run: | | |
| go vet ./... | |
| go test -race -count=1 -timeout 60s ./... | |
| # The generated documents are validated with external validators: | |
| # openapi-spec-validator for 3.0.4/3.1.2 and the official OpenAPI | |
| # 3.2 JSON Schema for 3.2.0 — backing the documentation's claim | |
| # that all three outputs are externally validated in CI. | |
| validate: | |
| name: Spec validation | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: stable | |
| cache: true | |
| - name: Generate specs for all three versions | |
| run: | | |
| mkdir -p /tmp/specgen && cd /tmp/specgen | |
| cat > go.mod <<'EOF' | |
| module specgen | |
| go 1.24.0 | |
| require github.com/FumingPower3925/stdocs v0.0.0 | |
| replace github.com/FumingPower3925/stdocs => ${{ github.workspace }} | |
| EOF | |
| cat > main.go <<'EOF' | |
| package main | |
| import ( | |
| "net/http" | |
| "os" | |
| "github.com/FumingPower3925/stdocs" | |
| ) | |
| type CreateTask struct { | |
| Title string `json:"title" doc:"Short title" minLength:"1" maxLength:"200" pattern:"^.+$"` | |
| Priority int `json:"priority" minimum:"1" maximum:"5" default:"3" example:"2"` | |
| Ratio float64 `json:"ratio" exclusiveMinimum:"0" exclusiveMaximum:"1.5"` | |
| Status *string `json:"status" enum:"pending,active,done"` | |
| Tags []string `json:"tags" minItems:"1" maxItems:"10" uniqueItems:"true"` | |
| Email string `json:"email" format:"email"` | |
| } | |
| type Task struct{ ID string `json:"id"` } | |
| type APIError struct{ Message string `json:"message"` } | |
| type ListParams struct { | |
| Cursor string `query:"cursor" doc:"Opaque cursor"` | |
| Limit int `query:"limit" default:"20" minimum:"1" maximum:"100"` | |
| } | |
| // GenTask is CreateTask without exclusive bounds: Go client | |
| // generators (ogen, oapi-codegen) reject the numeric 2020-12 | |
| // exclusiveMinimum/exclusiveMaximum form that 3.1 documents | |
| // correctly emit, so the generator corpus omits them (Lint | |
| // documents the same limitation to users). Window pairs | |
| // nullability with a default — the anyOf form ogen accepts | |
| // since v1.17.0 — so the pin proves it stays consumable. | |
| type GenTask struct { | |
| Title string `json:"title" doc:"Short title" minLength:"1" maxLength:"200" pattern:"^.+$"` | |
| Priority int `json:"priority" minimum:"1" maximum:"5" default:"3" example:"2"` | |
| Status *string `json:"status" enum:"pending,active,done"` | |
| Tags []string `json:"tags" minItems:"1" maxItems:"10" uniqueItems:"true"` | |
| Window *int `json:"window" default:"30"` | |
| } | |
| func build(v stdocs.SpecVersion, body any) []byte { | |
| mux := stdocs.New( | |
| stdocs.WithTitle("Validation API"), | |
| stdocs.WithVersion(v), | |
| stdocs.WithBearerAuth("bearerAuth", "JWT"), | |
| stdocs.WithDefaultResponse(500, APIError{}), | |
| ) | |
| mux.HandleFunc("GET /tasks", func(w http.ResponseWriter, r *http.Request) {}, | |
| stdocs.WithParams(ListParams{})) | |
| mux.HandleFunc("POST /tasks", func(w http.ResponseWriter, r *http.Request) {}, | |
| stdocs.WithBody(body), | |
| stdocs.WithResponse(201, Task{}), | |
| stdocs.WithSecurity("bearerAuth")) | |
| raw, err := mux.JSON() | |
| if err != nil { | |
| panic(err) | |
| } | |
| return raw | |
| } | |
| func main() { | |
| for _, v := range []stdocs.SpecVersion{stdocs.OpenAPI30, stdocs.OpenAPI31, stdocs.OpenAPI32} { | |
| if err := os.WriteFile("/tmp/spec-"+string(v)+".json", build(v, CreateTask{}), 0o644); err != nil { | |
| panic(err) | |
| } | |
| } | |
| if err := os.WriteFile("/tmp/spec-generators.json", build(stdocs.OpenAPI31, GenTask{}), 0o644); err != nil { | |
| panic(err) | |
| } | |
| } | |
| EOF | |
| go mod tidy && go run . | |
| ls -la /tmp/spec-*.json | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.x" | |
| - name: Validate 3.0 and 3.1 with openapi-spec-validator | |
| run: | | |
| pip install --quiet 'openapi-spec-validator~=0.7.1' | |
| python -m openapi_spec_validator /tmp/spec-3.0.4.json | |
| python -m openapi_spec_validator /tmp/spec-3.1.2.json | |
| - name: Consumability — generate typed clients with ogen | |
| run: | | |
| mkdir -p /tmp/ogencheck && cd /tmp/ogencheck | |
| # The full-corpus 3.0.4 document (generators accept the | |
| # draft-4 boolean exclusive bounds) and the generator-corpus | |
| # 3.1 document (anyOf nullables; no exclusive bounds, which | |
| # generators reject in their numeric 2020-12 form). | |
| go run github.com/ogen-go/ogen/cmd/ogen@v1.20.3 --target ./out30 --clean /tmp/spec-3.0.4.json | |
| test -s out30/oas_client_gen.go | |
| go run github.com/ogen-go/ogen/cmd/ogen@v1.20.3 --target ./out31 --clean /tmp/spec-generators.json | |
| test -s out31/oas_client_gen.go | |
| echo "ogen generated typed clients from both documents" | |
| - name: Style-lint the 3.0.4 document with Spectral | |
| run: | | |
| cat > /tmp/.spectral.yaml <<'EOF' | |
| extends: ["spectral:oas"] | |
| # Spectral's JSONPath engine crashes evaluating enum rules | |
| # over arrays containing a null member, which 3.0 nullable | |
| # enums legally require (upstream bug) — the two enum rules | |
| # stay off until that is fixed. Spectral is run on the 3.0.4 | |
| # document only: v6 misvalidates 3.1 keywords against the | |
| # 3.0 schema and does not know 3.2; the official validators | |
| # above cover those versions structurally. | |
| rules: | |
| typed-enum: off | |
| duplicated-entry-in-enum: off | |
| EOF | |
| npx -y @stoplight/spectral-cli@6.15.0 lint --ruleset /tmp/.spectral.yaml --fail-severity=error /tmp/spec-3.0.4.json | |
| - name: Consumability — type-check the generated TypeScript | |
| run: | | |
| # The committed golden is generation-equal by test; here the | |
| # pinned compiler proves it is valid strict TypeScript and | |
| # that real payloads assign to the generated types. | |
| mkdir -p /tmp/tscheck | |
| cp tsgen/testdata/corpus.ts /tmp/tscheck/corpus.ts | |
| cat > /tmp/tscheck/harness.ts <<'EOF' | |
| import type { CorpusTask, components, operations, webhooks } from "./corpus"; | |
| const task: CorpusTask = { | |
| id: "t1", | |
| title: "Fix the gate", | |
| status: "pending", | |
| level: 2, | |
| priority: 3, | |
| ratio: 0.5, | |
| due_at: null, | |
| note: null, | |
| count: "12", | |
| labels: { a: 1 }, | |
| tags: ["one"], | |
| links: [{ kind: "k" }, null], | |
| parent: null, | |
| }; | |
| const created: operations["post_tasks"]["responses"][201] = task; | |
| const page: operations["get_tasks"]["responses"][200] = { items: [task, created] }; | |
| const query: NonNullable<operations["get_tasks"]["parameters"]["query"]> = { cursor: "c", limit: 10 }; | |
| const path: operations["get_tasks_by_id"]["parameters"]["path"] = { id: "t1" }; | |
| const gone: operations["delete_tasks_by_id"]["responses"][204] = undefined; | |
| const csv: operations["get_export"]["responses"][200] = "a,b\n1,2\n"; | |
| const event: webhooks["task-event"]["requestBody"] = { kind: "created" }; | |
| const err: components["CorpusError"] = { code: 1, message: "m" }; | |
| export { created, page, query, path, gone, csv, event, err }; | |
| EOF | |
| TS_PIN="$(node -e "console.log(require('./package.json').devDependencies.typescript)")" | |
| npx -y -p "typescript@${TS_PIN}" tsc --strict --noEmit /tmp/tscheck/harness.ts | |
| echo "generated TypeScript type-checks under tsc ${TS_PIN} --strict" | |
| - name: Validate 3.2 against the official OpenAPI 3.2 JSON Schema | |
| run: | | |
| pip install --quiet 'jsonschema[format]~=4.23' 'requests~=2.32' | |
| python - <<'EOF' | |
| import json, jsonschema, requests | |
| resp = requests.get( | |
| "https://spec.openapis.org/oas/3.2/schema/2025-09-17", | |
| headers={"Accept": "application/schema+json"}, | |
| timeout=30, | |
| ) | |
| resp.raise_for_status() | |
| schema = resp.json() | |
| assert schema.get("$id", "").startswith("https://spec.openapis.org/oas/3.2/schema"), schema.get("$id") | |
| doc = json.load(open("/tmp/spec-3.2.0.json")) | |
| jsonschema.validate(doc, schema) | |
| print("3.2.0 valid against", schema.get("$id", "official schema")) | |
| EOF | |
| # Headless rendering smoke tests for all nine bundled UIs. Needs a | |
| # browser and (for the CDN variants) the network, so it runs on | |
| # manual dispatch rather than gating every push; the same command | |
| # runs locally with any Chrome/Chromium. | |
| ui-smoke: | |
| name: UI smoke | |
| if: ${{ github.event_name == 'workflow_dispatch' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: stable | |
| cache: true | |
| - name: Render all UIs headlessly | |
| env: | |
| CHROME_BIN: /usr/bin/google-chrome | |
| run: go test -tags uismoke -run TestUISmoke -v -timeout 15m . | |
| # Single fan-in check for branch protection: required status checks | |
| # are matched by name, and the compat matrix's job names change with | |
| # every Go patch release — so protection requires only this job, | |
| # which fails unless every job it needs succeeded. | |
| ci-ok: | |
| name: CI OK | |
| runs-on: ubuntu-latest | |
| needs: [test, versions, compat, lint, coverage, validate] | |
| if: always() | |
| steps: | |
| - name: All required jobs succeeded | |
| run: | | |
| results='${{ join(needs.*.result, ' ') }}' | |
| echo "job results: $results" | |
| for r in $results; do | |
| if [ "$r" != "success" ]; then | |
| echo "a required job finished with: $r" && exit 1 | |
| fi | |
| done | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: stable | |
| cache: true | |
| - name: golangci-lint | |
| uses: golangci/golangci-lint-action@v9 | |
| with: | |
| version: v2.12.2 | |
| # The YAML round-trip module is a separate go module, so the | |
| # root invocation above does not reach it; lint it on its own. | |
| - name: golangci-lint (round-trip module) | |
| uses: golangci/golangci-lint-action@v9 | |
| with: | |
| version: v2.12.2 | |
| working-directory: internal/spec/yaml/roundtrip_test | |
| coverage: | |
| name: Coverage | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: stable | |
| cache: true | |
| - name: Generate coverage | |
| run: go test -coverprofile=coverage.out -covermode=atomic ./... | |
| - name: Enforce the coverage floor | |
| run: | | |
| total="$(go tool cover -func=coverage.out | awk '/^total:/{sub("%","",$3); print $3}')" | |
| floor=80.0 | |
| echo "total statement coverage: ${total}% (floor: ${floor}%)" | |
| awk -v t="$total" -v f="$floor" 'BEGIN { exit (t+0 >= f+0) ? 0 : 1 }' || { | |
| echo "coverage ${total}% fell below the ${floor}% floor"; exit 1; } | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| files: ./coverage.out | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| fail_ci_if_error: false |