Skip to content

CI

CI #310

Workflow file for this run

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