Skip to content

Merge pull request #44 from xraph/fix/remaining-criticals #317

Merge pull request #44 from xraph/fix/remaining-criticals

Merge pull request #44 from xraph/fix/remaining-criticals #317

Workflow file for this run

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