CI #310
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, master] | |
| tags: ["v*.*.*"] | |
| pull_request: | |
| schedule: | |
| - cron: "17 20 * * *" | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build-test: | |
| name: build & test (go ${{ matrix.go }}) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| go: ["1.26", "stable"] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version: ${{ matrix.go }} | |
| check-latest: true | |
| cache: true | |
| - name: CI build/test/tidy gates | |
| run: make ci-fast | |
| - name: Generated project verification and service smoke matrix | |
| if: matrix.go == '1.26' | |
| run: make test-generated-matrix | |
| - name: Generated project runtime control-plane smoke | |
| if: matrix.go == '1.26' | |
| run: make generated-control-plane-smoke | |
| - name: Coverage gate | |
| if: matrix.go == '1.26' | |
| run: make cover-check | |
| - name: Upload coverage artifacts | |
| if: matrix.go == '1.26' && always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: coverage-go-${{ matrix.go }} | |
| path: | | |
| coverage.out | |
| coverage.txt | |
| if-no-files-found: ignore | |
| - name: Build CLI with ldflags | |
| run: | | |
| make build | |
| ./bin/gofly version | |
| lint: | |
| name: golangci-lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: golangci-lint | |
| uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a | |
| with: | |
| version: v2.12.2 | |
| platform-smoke: | |
| name: platform smoke (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [macos-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version: "stable" | |
| check-latest: true | |
| cache: true | |
| - name: Build CLI | |
| run: go build ./cmd/gofly | |
| - name: Smoke test core packages | |
| run: go test -count=1 ./app ./rest ./rpc ./gateway ./cache | |
| security: | |
| name: security (govulncheck + gosec) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: govulncheck — Go vulnerability scan | |
| run: make security | |
| supply-chain: | |
| name: supply-chain lint + OSV | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Install shellcheck | |
| run: sudo apt-get update && sudo apt-get install -y --no-install-recommends shellcheck | |
| - name: Workflow, shell, and OSV checks | |
| run: make supply-chain | |
| - name: Public API compatibility report | |
| env: | |
| API_BASE_REF: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || '' }} | |
| API_COMPAT_REPORT: ${{ runner.temp }}/api-compat-report.json | |
| API_COMPAT_REQUIRED: ${{ startsWith(github.ref, 'refs/tags/v') && 'true' || 'false' }} | |
| run: make api-compat | |
| - name: Public API compatibility summary | |
| if: always() | |
| run: | | |
| if [ -s "${{ runner.temp }}/api-compat-report.json" ]; then | |
| python3 - "${{ runner.temp }}/api-compat-report.json" >> "$GITHUB_STEP_SUMMARY" <<'PY' | |
| import json | |
| import sys | |
| from pathlib import Path | |
| data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) | |
| print("## Public API compatibility") | |
| print(f"- status: {data['status']}") | |
| print(f"- base_ref: {data['base_ref']}") | |
| print(f"- reason: {data['reason']}") | |
| PY | |
| fi | |
| - name: Upload public API compatibility report | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: api-compat-report | |
| path: ${{ runner.temp }}/api-compat-report.json | |
| if-no-files-found: warn | |
| codeql: | |
| name: CodeQL security analysis | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - name: Initialize CodeQL | |
| uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 | |
| with: | |
| languages: go | |
| config-file: ./.github/codeql/codeql-config.yml | |
| - name: Autobuild | |
| uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 | |
| - name: Perform CodeQL Analysis | |
| uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 | |
| dependency-review: | |
| name: dependency review | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - name: Review dependency changes | |
| if: github.event_name == 'pull_request' | |
| uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 | |
| with: | |
| fail-on-severity: high | |
| - name: Dependency review skip summary | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| { | |
| echo "## Dependency review" | |
| echo "Dependency Review is a pull-request-only gate; no dependency diff is available on this event." | |
| echo "The job remains successful so tag-release dependency graphs are not blocked by a skipped prerequisite." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| dependency-upgrade-validation: | |
| name: dependency upgrade validation | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Detect dependency manifest changes | |
| id: dependency_changes | |
| run: | | |
| if [ "${{ github.event_name }}" != "pull_request" ]; then | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| git fetch --no-tags --prune --depth=1 origin "+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}" | |
| changed_files="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD")" | |
| printf '%s\n' "$changed_files" | |
| if printf '%s\n' "$changed_files" | grep -E '(^|/)go\.(mod|sum)$'; then | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Dependency upgrade gates | |
| if: steps.dependency_changes.outputs.changed == 'true' | |
| run: make dependency-upgrade-check DEPENDENCY_UPGRADE_RUN_INTEGRATION=false | |
| - name: Dependency upgrade integration delegation summary | |
| if: steps.dependency_changes.outputs.changed == 'true' | |
| run: | | |
| { | |
| echo "## Dependency upgrade validation" | |
| echo "Dependency manifests changed; module verification and govulncheck ran here." | |
| echo "Docker-backed integration coverage is delegated to the required integration matrix to avoid duplicate service startup cost." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Dependency upgrade skip summary | |
| if: steps.dependency_changes.outputs.changed != 'true' | |
| run: | | |
| { | |
| echo "## Dependency upgrade validation" | |
| echo "No go.mod/go.sum changes detected; dependency upgrade gates skipped." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| branch-protection-audit: | |
| name: branch protection required-check audit | |
| if: github.repository == 'gofly/gofly' && (github.event_name == 'schedule' || (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch))) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Audit default-branch required status checks | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPOSITORY: ${{ github.repository }} | |
| TARGET_BRANCH: ${{ github.event.repository.default_branch || 'main' }} | |
| run: | | |
| gh api "repos/${REPOSITORY}/branches/${TARGET_BRANCH}/protection/required_status_checks" > required-status-checks.json | |
| python3 - required-status-checks.json <<'PY' | |
| import json | |
| import sys | |
| from pathlib import Path | |
| expected = { | |
| "build & test (go 1.26)", | |
| "build & test (go stable)", | |
| "golangci-lint", | |
| "platform smoke (macos-latest)", | |
| "platform smoke (windows-latest)", | |
| "security (govulncheck + gosec)", | |
| "supply-chain lint + OSV", | |
| "CodeQL security analysis", | |
| "dependency review", | |
| "dependency upgrade validation", | |
| "gateway profile contract", | |
| "branch protection required-check audit", | |
| "contract / api+rpc (check + breaking)", | |
| "governance gates", | |
| "bench + fuzz smoke", | |
| "integration tests (storage-mysql-postgres)", | |
| "integration tests (config-consul-nacos-etcd)", | |
| "integration tests (mq-brokers)", | |
| "integration tests (gateway-transcode)", | |
| "cloud-native live render", | |
| "docker build + trivy", | |
| "OSSF Scorecard", | |
| } | |
| data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) | |
| checks = data.get("checks") or [] | |
| actual = {item.get("context", "") for item in checks if item.get("context")} | |
| actual.update(data.get("contexts") or []) | |
| missing = sorted(expected - actual) | |
| extra = sorted(actual - expected) | |
| summary = Path(__import__("os").environ["GITHUB_STEP_SUMMARY"]) | |
| with summary.open("a", encoding="utf-8") as fh: | |
| print("## Branch protection required-check audit", file=fh) | |
| print(f"- expected checks: {len(expected)}", file=fh) | |
| print(f"- configured checks: {len(actual)}", file=fh) | |
| if missing: | |
| print("- missing required checks:", file=fh) | |
| for item in missing: | |
| print(f" - `{item}`", file=fh) | |
| if extra: | |
| print("- extra configured checks:", file=fh) | |
| for item in extra: | |
| print(f" - `{item}`", file=fh) | |
| if not missing and not extra: | |
| print("- status: branch protection matches the documented required-check set", file=fh) | |
| if missing: | |
| raise SystemExit("branch protection required-check drift detected") | |
| PY | |
| gateway-profile-contract: | |
| name: gateway profile contract | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Gateway profile contract gate | |
| run: | | |
| go test -count=1 -shuffle=on ./cmd/gofly/internal/command -run 'Test(GatewayProfileValidateCommandJSON|GatewayProfileValidateCommandBreakingAndUsage|GatewayAggregationValidateCommandJSON|GatewayAggregationSARIFRuleTaxonomyContract|ReleaseGatewayProfileContractCheck|ReleaseGatewayAggregationContractCheck|ReleaseGeneratedRPCMuxRetrySmokeCheck|ExecuteAIManifestJSONEnvelope|ExecuteAIManifestAliasAndText)' | |
| go test -count=1 -shuffle=on ./cmd/gofly/internal/command -run 'TestAINewGeneratedProjectVerificationMatrix' | |
| go test -count=1 -shuffle=on ./cmd/gofly/internal/generator -run 'Test(ProjectTemplate|GenerateGateway)' | |
| make aiflow-profile-gate-check | |
| make required-checks-drift-check | |
| - name: Generated RPC mux mTLS success release evidence | |
| run: | | |
| go run ./cmd/gofly release check --json --evidence generated-rpc-mux-retry-smoke > generated-rpc-mux-retry-smoke.json | |
| sh bin/scripts/check-generated-rpc-mux-mtls-evidence.sh generated-rpc-mux-retry-smoke.json | |
| - name: Gateway aggregation diff summary | |
| run: | | |
| tmp="$(mktemp -d)" | |
| go run ./cmd/gofly gen gateway edge --module example.com/edge --dir "$tmp/edge" | |
| go run ./cmd/gofly gateway aggregation validate \ | |
| --openapi-base "$tmp/edge/etc/edge-openapi-base.json" \ | |
| --openapi-candidate "$tmp/edge/etc/edge-openapi-breaking.json" \ | |
| --route home \ | |
| --format sarif > gateway-aggregation-breaking.sarif | |
| go run ./cmd/gofly gateway aggregation validate \ | |
| --openapi-base "$tmp/edge/etc/edge-openapi-base.json" \ | |
| --openapi-candidate "$tmp/edge/etc/edge-openapi-invalid.json" \ | |
| --route home \ | |
| --format sarif > gateway-aggregation-invalid.sarif | |
| python3 - gateway-aggregation-breaking.sarif gateway-aggregation-invalid.sarif gateway-aggregation.sarif <<'PY' | |
| import json | |
| import sys | |
| from pathlib import Path | |
| merged = None | |
| rules = {} | |
| results = [] | |
| for raw in sys.argv[1:3]: | |
| data = json.loads(Path(raw).read_text(encoding="utf-8")) | |
| if merged is None: | |
| merged = data | |
| run = (data.get("runs") or [{}])[0] | |
| driver = (run.get("tool") or {}).get("driver") or {} | |
| for rule in driver.get("rules") or []: | |
| rules[rule.get("id", "")] = rule | |
| results.extend(run.get("results") or []) | |
| if merged is None: | |
| raise SystemExit("no SARIF inputs") | |
| run = merged["runs"][0] | |
| run["tool"]["driver"]["rules"] = [rules[key] for key in sorted(rules) if key] | |
| run["results"] = results | |
| Path(sys.argv[3]).write_text(json.dumps(merged, indent=2, sort_keys=True) + "\n", encoding="utf-8") | |
| PY | |
| { | |
| echo "## Gateway aggregation contract" | |
| echo | |
| echo "### Compatible fixture" | |
| echo | |
| go run ./cmd/gofly gateway aggregation validate \ | |
| --openapi-base "$tmp/edge/etc/edge-openapi-base.json" \ | |
| --openapi-candidate "$tmp/edge/etc/edge-openapi-candidate.json" \ | |
| --route home \ | |
| --format markdown | |
| echo | |
| echo "### Intentionally breaking fixture" | |
| echo | |
| go run ./cmd/gofly gateway aggregation validate \ | |
| --openapi-base "$tmp/edge/etc/edge-openapi-base.json" \ | |
| --openapi-candidate "$tmp/edge/etc/edge-openapi-breaking.json" \ | |
| --route home \ | |
| --format markdown | |
| echo | |
| echo "### Invalid request-shaping fixture" | |
| echo | |
| if invalid_output="$(go run ./cmd/gofly gateway aggregation validate \ | |
| --openapi-base "$tmp/edge/etc/edge-openapi-base.json" \ | |
| --openapi-candidate "$tmp/edge/etc/edge-openapi-invalid.json" \ | |
| --route home \ | |
| --format markdown 2>&1)"; then | |
| echo "Expected invalid fixture to fail, but it passed." | |
| exit 1 | |
| else | |
| echo '```text' | |
| printf '%s\n' "$invalid_output" | |
| echo '```' | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload gateway aggregation SARIF artifact | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: gateway-aggregation-sarif | |
| path: gateway-aggregation.sarif | |
| if-no-files-found: error | |
| - name: Upload gateway aggregation SARIF to Code Scanning | |
| if: vars.GOFLY_UPLOAD_AGGREGATION_SARIF == 'true' | |
| uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 | |
| with: | |
| sarif_file: gateway-aggregation.sarif | |
| contract-check: | |
| name: contract / api+rpc (check + breaking) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Build gofly CLI | |
| run: go build -o /tmp/gofly ./cmd/gofly | |
| - name: Create demo contracts | |
| run: | | |
| mkdir -p /tmp/contract | |
| cat > /tmp/contract/v1.api <<'EOF' | |
| type User { | |
| ID int | |
| Name string | |
| } | |
| service UserService { | |
| @handler getUser | |
| GET /users/{id} (User) returns (User) | |
| } | |
| EOF | |
| cp /tmp/contract/v1.api /tmp/contract/v1-same.api | |
| cat > /tmp/contract/v2-break.api <<'EOF' | |
| type User { | |
| ID int | |
| } | |
| service UserService { | |
| @handler getUser | |
| POST /users/{id} (User) returns (User) | |
| } | |
| EOF | |
| cat > /tmp/contract/demo.proto <<'EOF' | |
| syntax = "proto3"; | |
| package demo; | |
| message User { | |
| int64 id = 1; | |
| string name = 2; | |
| } | |
| service UserService { | |
| rpc GetUser(User) returns (User); | |
| } | |
| EOF | |
| cp /tmp/contract/demo.proto /tmp/contract/demo-same.proto | |
| cat > /tmp/contract/demo-break.proto <<'EOF' | |
| syntax = "proto3"; | |
| package demo; | |
| message User { | |
| int64 id = 1; | |
| } | |
| service UserService { | |
| rpc GetAccount(User) returns (User); | |
| } | |
| EOF | |
| - name: api check — schema validity | |
| run: /tmp/gofly api check --file /tmp/contract/v1.api | |
| - name: api diff — info only (non-blocking) | |
| run: /tmp/gofly api diff --base /tmp/contract/v1.api --target /tmp/contract/v2-break.api || true | |
| - name: api breaking — no-change case (must succeed) | |
| run: /tmp/gofly api breaking --base /tmp/contract/v1.api --target /tmp/contract/v1-same.api | |
| - name: api breaking — with-change case (must fail) | |
| id: break_case | |
| run: | | |
| set +e | |
| /tmp/gofly api breaking --base /tmp/contract/v1.api --target /tmp/contract/v2-break.api | |
| rc=$? | |
| set -e | |
| if [ $rc -eq 0 ]; then | |
| echo "expected non-zero exit for breaking changes, got 0" | |
| exit 1 | |
| fi | |
| echo "breaking changes correctly detected (rc=$rc)" | |
| - name: rpc check — proto validity | |
| run: /tmp/gofly rpc check --file /tmp/contract/demo.proto | |
| - name: rpc doc — OpenAPI from proto transcoding | |
| run: | | |
| /tmp/gofly rpc doc --file /tmp/contract/demo.proto --output /tmp/contract/demo-openapi.json --format openapi | |
| test -s /tmp/contract/demo-openapi.json | |
| - name: rpc breaking — no-change case (must succeed) | |
| run: /tmp/gofly rpc breaking --base /tmp/contract/demo.proto --target /tmp/contract/demo-same.proto | |
| - name: rpc breaking — with-change case (must fail) | |
| run: | | |
| set +e | |
| /tmp/gofly rpc breaking --base /tmp/contract/demo.proto --target /tmp/contract/demo-break.proto | |
| rc=$? | |
| set -e | |
| if [ $rc -eq 0 ]; then | |
| echo "expected non-zero exit for rpc breaking changes, got 0" | |
| exit 1 | |
| fi | |
| echo "rpc breaking changes correctly detected (rc=$rc)" | |
| governance: | |
| name: governance gates | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: 10-round architecture and quality governance | |
| env: | |
| GOVERNANCE_ISOLATE_GOMODCACHE: ${{ github.event_name == 'schedule' && 'true' || 'false' }} | |
| GOVERNANCE_SKIP_GENERATED_MATRIX: "true" | |
| GOVERNANCE_SKIP_GENERATED_CONTROL_PLANE_SMOKE: ${{ startsWith(github.ref, 'refs/tags/v') && 'false' || 'true' }} | |
| GOVERNANCE_SKIP_REPORT: ${{ runner.temp }}/governance-skip-report.json | |
| run: make governance-10-rounds | |
| - name: Governance skip summary | |
| if: always() | |
| run: | | |
| if [ -s "${{ runner.temp }}/governance-skip-report.json" ]; then | |
| python3 - "${{ runner.temp }}/governance-skip-report.json" >> "$GITHUB_STEP_SUMMARY" <<'PY' | |
| import json | |
| import sys | |
| from pathlib import Path | |
| data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) | |
| skips = data.get("skips", []) | |
| print("## Governance skip report") | |
| if not skips: | |
| print("No skipped governance rounds.") | |
| else: | |
| print("| Round | Gate | Env | Risk | Compensating gate | Required for release |") | |
| print("| --- | --- | --- | --- | --- | --- |") | |
| for item in skips: | |
| print( | |
| "| {} | {} | {} | {} | {} | {} |".format( | |
| item["round"], | |
| item["name"], | |
| item["env"], | |
| item["risk"], | |
| item["compensating_gate"], | |
| item["required_for_release"], | |
| ) | |
| ) | |
| PY | |
| fi | |
| - name: Upload governance skip report | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: governance-skip-report | |
| path: ${{ runner.temp }}/governance-skip-report.json | |
| if-no-files-found: error | |
| bench-fuzz: | |
| name: bench + fuzz smoke | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Build examples | |
| run: | | |
| if [ -d examples ] && find examples -type f -name '*.go' | grep -q .; then | |
| go build ./examples/... | |
| else | |
| echo "examples/ not present or empty; skipping build" | |
| fi | |
| - name: Vet examples | |
| run: | | |
| if [ -d examples ] && find examples -type f -name '*.go' | grep -q .; then | |
| go vet ./examples/... | |
| else | |
| echo "examples/ not present or empty; skipping vet" | |
| fi | |
| - name: Benchmark evidence gate | |
| run: make bench-evidence-check | |
| - name: Benchmark baseline smoke | |
| run: bash bin/scripts/benchstat.sh --smoke | |
| - name: Benchmark allocation regression gate | |
| run: make bench-regression-check | |
| - name: Benchmark trend summary | |
| run: bash bin/scripts/benchstat.sh --trend | |
| - name: Upload benchmark artifacts | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: benchmark-smoke | |
| path: | | |
| bench/current.txt | |
| bench/regression-report.json | |
| bench/summary.md | |
| if-no-files-found: error | |
| - name: Fuzz smoke — api parser | |
| run: go test -run=Fuzz -fuzz=FuzzParseAPI -fuzztime=20s ./cmd/gofly/internal/generator/ | |
| - name: Fuzz smoke — proto parser | |
| run: go test -run=Fuzz -fuzz=FuzzParseProto -fuzztime=20s ./cmd/gofly/internal/generator/ | |
| - name: Fuzz smoke — rest binding (json) | |
| run: go test -run=Fuzz -fuzz=FuzzBindJSON -fuzztime=20s ./rest/ | |
| - name: Fuzz smoke — rest binding (query) | |
| run: go test -run=Fuzz -fuzz=FuzzBindQuery -fuzztime=20s ./rest/ | |
| integration: | |
| name: integration tests (${{ matrix.area }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - area: storage-mysql-postgres | |
| packages: ./core/storage/ | |
| - area: config-consul-nacos-etcd | |
| packages: ./core/config/... ./core/discovery/... | |
| - area: mq-brokers | |
| packages: ./core/mq/... | |
| - area: gateway-transcode | |
| packages: ./gateway/ | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Run Docker-backed integration tests | |
| run: go test -tags=integration -count=1 ${{ matrix.packages }} | |
| cloud-native-live-render: | |
| name: cloud-native live render | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Install cloud-native render tools | |
| run: | | |
| go install helm.sh/helm/v3/cmd/helm@v3.18.6 | |
| go install sigs.k8s.io/kustomize/kustomize/v5@v5.7.1 | |
| go install github.com/yannh/kubeconform/cmd/kubeconform@v0.7.0 | |
| helm version --short | |
| kustomize version | |
| kubeconform -v | |
| - name: Cloud-native render evidence gate | |
| env: | |
| CLOUD_NATIVE_RENDER_REPORT: .tmp-test/cloud-native-render/render-report.json | |
| run: make cloud-native-render-check | |
| - name: Upload cloud-native live render evidence | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: cloud-native-live-render-evidence | |
| path: | | |
| .tmp-test/cloud-native-render/render-report.json | |
| if-no-files-found: error | |
| docker: | |
| name: docker build + trivy | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c | |
| - name: Build Docker image | |
| run: | | |
| docker buildx build \ | |
| --load \ | |
| --metadata-file docker-build-metadata.json \ | |
| -t gofly:${{ github.sha }} \ | |
| . | |
| docker image inspect gofly:${{ github.sha }} > docker-image-inspect.json | |
| python3 -c 'import json; from pathlib import Path; inspect = json.loads(Path("docker-image-inspect.json").read_text(encoding="utf-8"))[0]; metadata_path = Path("docker-build-metadata.json"); metadata = json.loads(metadata_path.read_text(encoding="utf-8")) if metadata_path.exists() else {}; evidence = {"schema": "gofly.docker_build_evidence.v1", "image_ref": "gofly:${{ github.sha }}", "image_id": inspect.get("Id", ""), "repo_digests": inspect.get("RepoDigests") or [], "repo_tags": inspect.get("RepoTags") or [], "build_metadata": metadata}; Path("docker-build-evidence.json").write_text(json.dumps(evidence, indent=2, sort_keys=True) + "\n", encoding="utf-8")' | |
| - name: Trivy image scan | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 | |
| with: | |
| image-ref: gofly:${{ github.sha }} | |
| format: sarif | |
| output: trivy-results.sarif | |
| severity: CRITICAL,HIGH | |
| ignore-unfixed: true | |
| exit-code: "1" | |
| - name: Upload Trivy scan results | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 | |
| with: | |
| sarif_file: trivy-results.sarif | |
| - name: Upload Docker and Trivy evidence | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: docker-trivy-evidence | |
| path: | | |
| docker-build-evidence.json | |
| docker-build-metadata.json | |
| docker-image-inspect.json | |
| trivy-results.sarif | |
| if-no-files-found: error | |
| scorecard: | |
| name: OSSF Scorecard | |
| if: github.event_name == 'schedule' || github.event_name == 'push' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: read | |
| security-events: write | |
| id-token: write | |
| steps: | |
| - name: Scorecard analysis | |
| uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc | |
| with: | |
| results_format: sarif | |
| results_file: scorecard-results.sarif | |
| publish_results: true | |
| - name: Upload Scorecard SARIF | |
| uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 | |
| with: | |
| sarif_file: scorecard-results.sarif | |
| release: | |
| name: release (tagged) | |
| if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'gofly/gofly' | |
| needs: [build-test, platform-smoke, lint, security, supply-chain, codeql, dependency-upgrade-validation, gateway-profile-contract, contract-check, governance, bench-fuzz, integration, cloud-native-live-render, docker, scorecard] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache: true | |
| - name: Install GoReleaser | |
| uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 | |
| with: | |
| distribution: goreleaser | |
| version: v2.16.0 | |
| args: --version | |
| install-only: true | |
| - name: Install Syft for release SBOMs | |
| run: go install github.com/anchore/syft/cmd/syft@v1.40.0 | |
| - name: Login to GHCR for release image publish | |
| run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin | |
| - name: Validate Homebrew tap publishing token | |
| env: | |
| HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} | |
| run: | | |
| if [ -z "$HOMEBREW_TAP_GITHUB_TOKEN" ]; then | |
| echo "HOMEBREW_TAP_GITHUB_TOKEN is required to publish the gofly/homebrew-tap formula." >&2 | |
| echo "Create a fine-grained token scoped to the tap repository, or temporarily remove the brew publisher from .goreleaser.yml for an intentionally degraded release." >&2 | |
| exit 1 | |
| fi | |
| - name: Run GoReleaser | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} | |
| run: goreleaser release --clean | |
| - name: Verify release artifacts | |
| run: make release-artifacts-check | |
| - name: Collect release Docker digest evidence | |
| run: | | |
| mkdir -p release-evidence/docker | |
| image="ghcr.io/gofly/gofly:${{ github.ref_name }}" | |
| docker buildx imagetools inspect --raw "$image" > release-evidence/docker/release-docker-manifest.json | |
| docker buildx imagetools inspect "$image" > release-evidence/docker/release-docker-inspect.txt | |
| digest="$(python3 - release-evidence/docker/release-docker-inspect.txt <<'PY' | |
| import re | |
| import sys | |
| from pathlib import Path | |
| match = re.search(r"^Digest:\s*(sha256:[0-9a-f]{64})$", Path(sys.argv[1]).read_text(encoding="utf-8"), re.MULTILINE) | |
| if not match: | |
| raise SystemExit("could not find canonical registry digest in docker buildx imagetools inspect output") | |
| print(match.group(1)) | |
| PY | |
| )" | |
| python3 - release-evidence/docker/release-docker-manifest.json "$image" "$digest" <<'PY' | |
| import json | |
| import sys | |
| from pathlib import Path | |
| manifest_path = Path(sys.argv[1]) | |
| image = sys.argv[2] | |
| digest = sys.argv[3] | |
| manifest = json.loads(manifest_path.read_text(encoding="utf-8")) | |
| manifests = manifest.get("manifests") or [] | |
| platforms = sorted( | |
| "{}/{}".format(item.get("platform", {}).get("os"), item.get("platform", {}).get("architecture")) | |
| for item in manifests | |
| ) | |
| if "linux/amd64" not in platforms or "linux/arm64" not in platforms: | |
| raise SystemExit(f"release manifest missing required platforms: {platforms}") | |
| evidence = { | |
| "schema": "gofly.release_docker_digest_evidence.v1", | |
| "image": image, | |
| "manifest_digest": digest, | |
| "platforms": platforms, | |
| "manifest_count": len(manifests), | |
| "digest_source": "docker buildx imagetools inspect", | |
| } | |
| Path("release-evidence/docker/release-docker-digests.json").write_text( | |
| json.dumps(evidence, indent=2, sort_keys=True) + "\n", | |
| encoding="utf-8", | |
| ) | |
| PY | |
| echo "RELEASE_DOCKER_IMAGE=$image" >> "$GITHUB_ENV" | |
| echo "RELEASE_DOCKER_DIGEST=$digest" >> "$GITHUB_ENV" | |
| - name: Collect release Docker SBOM evidence | |
| run: syft "${RELEASE_DOCKER_IMAGE}@${RELEASE_DOCKER_DIGEST}" -o spdx-json=release-evidence/docker/release-docker-sbom.spdx.json | |
| - name: Trivy release image scan | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 | |
| with: | |
| image-ref: ${{ env.RELEASE_DOCKER_IMAGE }}@${{ env.RELEASE_DOCKER_DIGEST }} | |
| format: json | |
| output: release-evidence/docker/release-trivy-results.json | |
| severity: CRITICAL,HIGH | |
| ignore-unfixed: true | |
| exit-code: "1" | |
| - name: Download Docker and Trivy evidence | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| mkdir -p release-evidence/docker/ci | |
| gh run download "${{ github.run_id }}" \ | |
| --name docker-trivy-evidence \ | |
| --dir release-evidence/docker/ci | |
| test -s release-evidence/docker/ci/trivy-results.sarif | |
| test -s release-evidence/docker/ci/docker-build-evidence.json | |
| - name: Download cloud-native live render evidence | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| mkdir -p release-evidence/cloud-native | |
| gh run download "${{ github.run_id }}" \ | |
| --name cloud-native-live-render-evidence \ | |
| --dir release-evidence/cloud-native | |
| test -s release-evidence/cloud-native/render-report.json | |
| - name: Attest release checksums | |
| uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 | |
| with: | |
| subject-path: dist/checksums.txt | |
| - name: Attest Docker release manifest | |
| uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 | |
| with: | |
| subject-name: ${{ env.RELEASE_DOCKER_IMAGE }} | |
| subject-digest: ${{ env.RELEASE_DOCKER_DIGEST }} | |
| - name: Verify release attestations | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| signer_workflow="github.com/${GITHUB_REPOSITORY}/.github/workflows/ci.yml" | |
| gh attestation verify dist/checksums.txt \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --signer-workflow "$signer_workflow" \ | |
| --source-ref "$GITHUB_REF" \ | |
| --deny-self-hosted-runners \ | |
| --format json > release-evidence/checksums-attestation-verification.json | |
| gh attestation verify "oci://${RELEASE_DOCKER_IMAGE}@${RELEASE_DOCKER_DIGEST}" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --signer-workflow "$signer_workflow" \ | |
| --source-ref "$GITHUB_REF" \ | |
| --deny-self-hosted-runners \ | |
| --format json > release-evidence/docker/release-docker-attestation-verification.json | |
| - name: Verify Docker release evidence | |
| env: | |
| RELEASE_REQUIRE_DOCKER_EVIDENCE: "true" | |
| RELEASE_EVIDENCE_DIR: release-evidence/docker | |
| run: make release-artifacts-check | |
| - name: Upload release verification evidence | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: release-dist-evidence | |
| path: | | |
| dist/checksums.txt | |
| dist/*.spdx.json | |
| release-evidence/*.json | |
| release-evidence/cloud-native/** | |
| release-evidence/docker/** | |
| if-no-files-found: error |