Merge pull request #44 from xraph/fix/remaining-criticals #317
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: Go CI | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| pull_request: | |
| branches: [ main, develop ] | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| security-events: write | |
| jobs: | |
| # Build and test | |
| # Gating test legs: ubuntu + macos. Runs through the shared workflow, which | |
| # provisions the Node toolchain the generated-client runtime tests need. | |
| test: | |
| uses: xraph/workflows/.github/workflows/go-ci.yml@v1 | |
| with: | |
| go-versions: '["1.26"]' | |
| os: '["ubuntu-latest","macos-latest"]' | |
| only-test: true | |
| node-version: '20' | |
| npm-global-packages: 'typescript@5.8.2 esbuild@0.28.1' | |
| # `make test` sweeps every module in the repo; CI deliberately tests only | |
| # the root module and leaves the rest to build-all-modules. | |
| prefer-makefile: false | |
| secrets: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| # Windows is optional and must never block a merge - ci-summary deliberately | |
| # leaves this out of its gate, matching the pre-migration policy. | |
| test-windows: | |
| uses: xraph/workflows/.github/workflows/go-ci.yml@v1 | |
| with: | |
| go-versions: '["1.26"]' | |
| os: '["windows-latest"]' | |
| only-test: true | |
| node-version: '20' | |
| npm-global-packages: 'typescript@5.8.2 esbuild@0.28.1' | |
| prefer-makefile: false | |
| coverage: false | |
| # Build and vet ALL submodules (extensions, examples, cmd) | |
| build-all-modules: | |
| name: Build All Modules | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !docs/ | |
| sparse-checkout-cone-mode: false | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache-dependency-path: | | |
| **/go.sum | |
| - name: Build and vet all modules | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # cmd/forge and extensions/database require github.com/xraph/forge at | |
| # its last PUBLISHED version, because cmd/forge/go.mod has to stay free | |
| # of replace directives for `go install ...@latest` to accept it (see | |
| # the comment at the top of that file). Building them standalone would | |
| # therefore compile against the released root module instead of this | |
| # tree, so any root API this branch adds but has not published yet | |
| # shows up here as "undefined". A throwaway workspace restores local | |
| # resolution for those two modules, exactly as the Build CLI job does. | |
| # | |
| # The workspace file deliberately lives OUTSIDE the repo: a go.work at | |
| # the root would apply to every module in the loop below, and the ones | |
| # it does not list would fail with "directory prefix . does not contain | |
| # modules listed in go.work". Everything else builds with GOWORK=off so | |
| # each module is still checked standalone. | |
| WORKFILE="${RUNNER_TEMP:-/tmp}/forge-ci.work" | |
| rm -f "$WORKFILE" | |
| GOWORK="$WORKFILE" go work init "$PWD" "$PWD/cmd/forge" "$PWD/extensions/database" | |
| FAILED=0 | |
| TOTAL=0 | |
| FAILED_MODULES="" | |
| for modfile in $(find . -name "go.mod" -not -path "./docs/*" | sort); do | |
| dir=$(dirname "$modfile") | |
| mod_name=$(grep -m1 '^module ' "$modfile" | awk '{print $2}') | |
| TOTAL=$((TOTAL + 1)) | |
| case "$dir" in | |
| ./cmd/forge|./extensions/database) export GOWORK="$WORKFILE" ;; | |
| *) export GOWORK=off ;; | |
| esac | |
| echo "::group::Building $dir ($mod_name)" | |
| if ! (cd "$dir" && go build ./... 2>&1); then | |
| echo "::error::Build failed for module: $dir ($mod_name)" | |
| FAILED=$((FAILED + 1)) | |
| FAILED_MODULES="$FAILED_MODULES\n - $dir ($mod_name)" | |
| fi | |
| if ! (cd "$dir" && go vet ./... 2>&1); then | |
| echo "::error::Vet failed for module: $dir ($mod_name)" | |
| FAILED=$((FAILED + 1)) | |
| FAILED_MODULES="$FAILED_MODULES\n - $dir ($mod_name) [vet]" | |
| fi | |
| echo "::endgroup::" | |
| done | |
| echo "" | |
| echo "=====================================" | |
| echo "Module Build Summary: $TOTAL modules checked" | |
| echo "=====================================" | |
| if [ "$FAILED" -gt 0 ]; then | |
| echo "FAILED modules ($FAILED failures):$FAILED_MODULES" | |
| exit 1 | |
| else | |
| echo "All $TOTAL modules built and vetted successfully." | |
| fi | |
| - name: Test submodules | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Same published-vs-local resolution problem as the build step above; | |
| # see the comment there for why this workspace exists and why it lives | |
| # outside the repo. | |
| WORKFILE="${RUNNER_TEMP:-/tmp}/forge-ci.work" | |
| rm -f "$WORKFILE" | |
| GOWORK="$WORKFILE" go work init "$PWD" "$PWD/cmd/forge" "$PWD/extensions/database" | |
| FAILED=0 | |
| FAILED_MODULES="" | |
| for modfile in $(find . -name "go.mod" -not -path "./docs/*" -not -path "./go.mod" | sort); do | |
| dir=$(dirname "$modfile") | |
| mod_name=$(grep -m1 '^module ' "$modfile" | awk '{print $2}') | |
| case "$dir" in | |
| ./cmd/forge|./extensions/database) export GOWORK="$WORKFILE" ;; | |
| *) export GOWORK=off ;; | |
| esac | |
| echo "::group::Testing $dir ($mod_name)" | |
| if ! (cd "$dir" && go test -short -count=1 -timeout=5m ./... 2>&1); then | |
| echo "::error::Tests failed for module: $dir ($mod_name)" | |
| FAILED=$((FAILED + 1)) | |
| FAILED_MODULES="$FAILED_MODULES\n - $dir ($mod_name)" | |
| fi | |
| echo "::endgroup::" | |
| done | |
| if [ "$FAILED" -gt 0 ]; then | |
| echo "" | |
| echo "FAILED submodule tests ($FAILED failures):$FAILED_MODULES" | |
| exit 1 | |
| fi | |
| # Linting | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| check-latest: true | |
| cache-dependency-path: | | |
| **/go.sum | |
| - name: Run golangci-lint | |
| uses: golangci/golangci-lint-action@v6 | |
| with: | |
| version: latest | |
| args: --timeout=5m --exclude-dirs=bk | |
| continue-on-error: true | |
| # bk/ no longer exists, so the old `grep -v '/bk/'` filter was a no-op. | |
| - name: Run go vet | |
| run: go vet ./... | |
| # Security scanning | |
| security: | |
| name: Security Scan | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| - name: Run gosec | |
| run: | | |
| go install github.com/securego/gosec/v2/cmd/gosec@latest | |
| gosec -exclude-dir=bk -exclude-dir=vendor -exclude-dir=examples -fmt=sarif -out=gosec.sarif ./... || true | |
| - name: Fix SARIF file format | |
| if: always() | |
| run: | | |
| chmod +x .github/scripts/fix_sarif.py | |
| python3 .github/scripts/fix_sarif.py | |
| - name: Run govulncheck | |
| continue-on-error: true | |
| run: | | |
| go install golang.org/x/vuln/cmd/govulncheck@latest | |
| govulncheck ./... | |
| # - name: Upload SARIF file | |
| # uses: github/codeql-action/upload-sarif@v4 | |
| # if: always() | |
| # with: | |
| # sarif_file: gosec.sarif | |
| # Build CLI binary | |
| build-cli: | |
| name: Build CLI | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| - name: Cache Go modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} | |
| - name: Build CLI | |
| # cmd/forge/go.mod deliberately carries NO replace directives, because | |
| # `go install github.com/xraph/forge/cmd/forge@latest` refuses any | |
| # module whose go.mod has them. That means a plain `go build` here | |
| # would compile the CLI against the last PUBLISHED github.com/xraph/forge | |
| # rather than this tree — so a change that broke the root module's API | |
| # would still pass this job. A throwaway workspace restores local | |
| # resolution for CI only, without putting replace directives back into | |
| # the published go.mod. go.work is gitignored, so this file never | |
| # escapes the runner. | |
| run: | | |
| go work init . ./cmd/forge ./extensions/database | |
| cd cmd/forge | |
| go build -v -ldflags="-s -w -X main.version=dev-${{ github.sha }}" -o forge . | |
| - name: Test CLI binary | |
| run: | | |
| cd cmd/forge | |
| ./forge --version | |
| ./forge doctor || true | |
| - name: Upload CLI artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: forge-cli-${{ github.sha }} | |
| path: cmd/forge/forge | |
| retention-days: 7 | |
| # Summary | |
| ci-summary: | |
| name: CI Summary | |
| runs-on: ubuntu-latest | |
| needs: [test, test-windows, build-all-modules, lint, security, build-cli] | |
| if: always() | |
| steps: | |
| - name: Generate summary | |
| env: | |
| TEST_RESULT: ${{ needs.test.result }} | |
| WINDOWS_RESULT: ${{ needs.test-windows.result }} | |
| MODULES_RESULT: ${{ needs.build-all-modules.result }} | |
| LINT_RESULT: ${{ needs.lint.result }} | |
| SECURITY_RESULT: ${{ needs.security.result }} | |
| CLI_RESULT: ${{ needs.build-cli.result }} | |
| run: | | |
| set -uo pipefail | |
| { | |
| echo "# CI Results Summary" | |
| echo | |
| echo "| Job | Result |" | |
| echo "|---|---|" | |
| echo "| Test (ubuntu, macos) | ${TEST_RESULT} |" | |
| echo "| Test (windows, optional) | ${WINDOWS_RESULT} |" | |
| echo "| Build All Modules | ${MODULES_RESULT} |" | |
| echo "| Lint | ${LINT_RESULT} |" | |
| echo "| Security | ${SECURITY_RESULT} |" | |
| echo "| Build CLI | ${CLI_RESULT} |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # Windows is explicitly optional and is NOT part of the gate. | |
| failed=0 | |
| for entry in "test:${TEST_RESULT}" "build-all-modules:${MODULES_RESULT}" \ | |
| "lint:${LINT_RESULT}" "security:${SECURITY_RESULT}" \ | |
| "build-cli:${CLI_RESULT}"; do | |
| name="${entry%%:*}"; status="${entry##*:}" | |
| if [ "$status" != "success" ]; then | |
| echo "::error::Required CI job '$name' did not succeed (result: $status)" | |
| failed=1 | |
| fi | |
| done | |
| if [ "$failed" -ne 0 ]; then | |
| echo "**CI failed** - see the job logs above." >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| if [ "$WINDOWS_RESULT" != "success" ]; then | |
| echo "**Passed**, with optional Windows tests failing." >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "**All CI checks passed**" >> "$GITHUB_STEP_SUMMARY" | |
| fi |