Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/unreleased/51-go-ci-lane.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **First-class Go lane.** New reusable `go-ci.yml` discovers every tracked `go.mod` via `git ls-files` (skipping gitignored build artifacts and generated dirs) and runs `go build` / `go vet` / `go test` per module, with optional codegen + dirty-tree drift check. `repo-required-gate.yml` gains `stack: go` and `go` support inside `polyglot` (via `go-paths`), plumbed through the classifier (`runGoCi`) and the `decision` gate. (#51)
200 changes: 200 additions & 0 deletions .github/workflows/go-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
name: Go CI (reusable)

# Generic multi-module Go verification for repos with one or many Go modules.
#
# Module discovery uses `git ls-files`, NOT a filesystem `find`: tracked go.mod
# files only. This deliberately skips gitignored build artifacts (e.g.
# dist/<scaffold>/go.mod) and never recurses into gitignored generated dirs
# whose conflicting package decls would break `go build ./...`. Each discovered
# module root is verified independently: `go build`, `go vet`, `go test`.
#
# Designed to be called from repo-required-gate.yml's `go-ci` job, but usable
# standalone. Every step is opt-out via inputs.

on:
workflow_call:
inputs:
go-versions:
description: 'JSON array of Go versions for setup-go, e.g. `["1.26.x"]` or `["stable"]`.'
type: string
default: '["stable"]'
os-matrix:
description: 'JSON array of OS runners, e.g. `["ubuntu-latest"]`.'
type: string
default: '["ubuntu-latest"]'
exclude-modules:
description: |
Newline-separated path prefixes (relative to repo root) whose go.mod
roots are skipped — e.g. `tsunami/demo/`. A module dir is excluded
when `<dir>/` starts with any prefix. Comments (`#`) and blanks
ignored.
type: string
default: ""
run-build:
description: "Run `go build` per module. Empty/false skips."
type: boolean
default: true
run-vet:
description: "Run `go vet ./...` per module."
type: boolean
default: true
run-tests:
description: "Run `go test` per module."
type: boolean
default: true
build-args:
description: "Args for `go build` (word-split). Default builds all packages."
type: string
default: "./..."
test-args:
description: "Args for `go test` (word-split)."
type: string
default: "./..."
cgo-enabled:
description: "CGO_ENABLED override (`0` or `1`). Empty = Go default (1 when a C compiler is present)."
type: string
default: ""
cache:
description: "Enable setup-go module/build cache (keyed on all go.sum)."
type: boolean
default: true
setup-command:
description: |
Optional shell command run after checkout + Go setup, before
generate/verify (e.g. install Task or system deps). Empty = skip.
type: string
default: ""
generate-command:
description: |
Optional codegen command run once at repo root before module
verification (e.g. `task generate`). Empty = skip.
type: string
default: ""
verify-generated:
description: |
When true (and generate-command set), fail if codegen leaves a dirty
git tree — catches stale committed generated output (e.g. Go→TS
bindings). No-op when generate-command is empty.
type: boolean
default: false

jobs:
ci:
name: ${{ matrix.os }} / go ${{ matrix.go }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
go: ${{ fromJSON(inputs.go-versions) }}
os: ${{ fromJSON(inputs.os-matrix) }}

steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
cache: ${{ inputs.cache }}
cache-dependency-path: ${{ inputs.cache && '**/go.sum' || '' }}

- name: Setup command
if: inputs.setup-command != ''
shell: bash
run: ${{ inputs.setup-command }}

- name: Generate
if: inputs.generate-command != ''
shell: bash
run: ${{ inputs.generate-command }}

- name: Verify generated code is in sync
if: inputs.generate-command != '' && inputs.verify-generated
shell: bash
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "::error::generate-command left a dirty tree — committed generated code is out of sync. Run the generate command locally and commit the result."
git --no-pager diff --stat
exit 1
fi
echo "Generated code is in sync (clean tree)."

- name: Discover and verify Go modules
shell: bash
env:
RUN_BUILD: ${{ inputs.run-build }}
RUN_VET: ${{ inputs.run-vet }}
RUN_TESTS: ${{ inputs.run-tests }}
BUILD_ARGS: ${{ inputs.build-args }}
TEST_ARGS: ${{ inputs.test-args }}
CGO_ENABLED_INPUT: ${{ inputs.cgo-enabled }}
EXCLUDE_MODULES: ${{ inputs.exclude-modules }}
run: |
set -euo pipefail

if [ -n "$CGO_ENABLED_INPUT" ]; then
export CGO_ENABLED="$CGO_ENABLED_INPUT"
fi

# Exclusion prefixes (strip comments + blanks).
excludes="$(printf '%s\n' "$EXCLUDE_MODULES" | sed -e 's/#.*//' -e 's/[[:space:]]*$//' | grep -v '^[[:space:]]*$' || true)"

excluded() {
local dir="$1/" pre
[ -z "$excludes" ] && return 1
while IFS= read -r pre; do
[ -z "$pre" ] && continue
case "$dir" in
"$pre"*) return 0 ;;
esac
done <<< "$excludes"
return 1
}

# Tracked go.mod roots only — git ls-files skips gitignored artifacts.
modules="$(git ls-files | grep -E '(^|/)go\.mod$' | xargs -r -n1 dirname | sort -u)"
if [ -z "$modules" ]; then
echo "::error::no tracked go.mod found; is stack=go correct for this repo?"
exit 1
fi

rc=0
{
echo "### Go CI — module verification"
echo ""
} >> "$GITHUB_STEP_SUMMARY"

while IFS= read -r dir; do
[ -z "$dir" ] && continue
if excluded "$dir"; then
echo "skip (exclude-modules): $dir"
echo "- \`$dir\` — skipped (exclude-modules)" >> "$GITHUB_STEP_SUMMARY"
continue
fi
echo "::group::module $dir"
if (
cd "$dir"
go mod download
if [ "$RUN_BUILD" = "true" ]; then
# shellcheck disable=SC2086
go build $BUILD_ARGS
fi
if [ "$RUN_VET" = "true" ]; then
go vet ./...
fi
if [ "$RUN_TESTS" = "true" ]; then
# shellcheck disable=SC2086
go test $TEST_ARGS
fi
); then
echo "- \`$dir\` — ok" >> "$GITHUB_STEP_SUMMARY"
else
rc=1
echo "- \`$dir\` — FAILED" >> "$GITHUB_STEP_SUMMARY"
fi
echo "::endgroup::"
done <<< "$modules"

if [ "$rc" -ne 0 ]; then
echo "::error::one or more Go modules failed verification (see step summary)."
fi
exit $rc
75 changes: 75 additions & 0 deletions .github/workflows/repo-required-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,51 @@ on:
case-insensitive.
type: string
default: "ci:full"
# ---- Go lane (#51) ---------------------------------------------------
go-versions:
description: 'JSON array of Go versions for the Go lane, e.g. `["stable"]`.'
type: string
default: '["stable"]'
go-paths:
description: |
Newline-separated patterns selecting Go-owned roots for a polyglot
stack (same syntax as node-paths). For stack=polyglot, at least one
of node-paths / python-paths / go-paths must be set. Ignored for
non-polyglot stacks.
type: string
default: ""
go-exclude-modules:
description: "Newline-separated path prefixes whose go.mod roots the Go lane skips (e.g. `tsunami/demo/`)."
type: string
default: ""
go-build-args:
description: "Args for `go build` in the Go lane (word-split)."
type: string
default: "./..."
go-test:
description: "Run `go test` in the Go lane."
type: boolean
default: true
go-test-args:
description: "Args for `go test` in the Go lane (word-split)."
type: string
default: "./..."
go-cgo-enabled:
description: "CGO_ENABLED override for the Go lane (`0`/`1`). Empty = Go default."
type: string
default: ""
go-setup-command:
description: "Optional command run before Go verification (e.g. install Task)."
type: string
default: ""
go-generate-command:
description: "Optional codegen command run once before Go verification (e.g. `task generate`)."
type: string
default: ""
go-verify-generated:
description: "Fail if go-generate-command leaves a dirty tree (stale committed generated code)."
type: boolean
default: false
workflow-library-ref:
description: |
Git ref (tag, branch, or SHA) of ArchonVII/github-workflows used
Expand All @@ -166,6 +211,7 @@ jobs:
run-ci: ${{ steps.detect.outputs.run-ci }}
run-node-ci: ${{ steps.detect.outputs.run-node-ci }}
run-python-ci: ${{ steps.detect.outputs.run-python-ci }}
run-go-ci: ${{ steps.detect.outputs.run-go-ci }}
run-dependency-review: ${{ steps.detect.outputs.run-dependency-review }}
run-workflow-validation: ${{ steps.detect.outputs.run-workflow-validation }}
run-policy-validation: ${{ steps.detect.outputs.run-policy-validation }}
Expand Down Expand Up @@ -193,6 +239,7 @@ jobs:
STACK: ${{ inputs.stack }}
NODE_PATHS: ${{ inputs.node-paths }}
PYTHON_PATHS: ${{ inputs.python-paths }}
GO_PATHS: ${{ inputs.go-paths }}
SNAPSHOT_PATHS: ${{ inputs.snapshot-paths }}
SNAPSHOT_TEST_COMMAND: ${{ inputs.snapshot-test-command }}
FORCE_FULL_CI_LABEL: ${{ inputs.force-full-ci-label }}
Expand Down Expand Up @@ -239,6 +286,7 @@ jobs:
stack: process.env.STACK,
nodePaths: process.env.NODE_PATHS,
pythonPaths: process.env.PYTHON_PATHS,
goPaths: process.env.GO_PATHS,
snapshotPaths: process.env.SNAPSHOT_PATHS,
snapshotTestCommand: process.env.SNAPSHOT_TEST_COMMAND,
forceFullCiLabel: process.env.FORCE_FULL_CI_LABEL,
Expand All @@ -252,6 +300,7 @@ jobs:
core.setOutput('run-ci', String(result.outputs.runCi));
core.setOutput('run-node-ci', String(result.outputs.runNodeCi));
core.setOutput('run-python-ci', String(result.outputs.runPythonCi));
core.setOutput('run-go-ci', String(result.outputs.runGoCi));
core.setOutput('run-dependency-review', String(result.outputs.runDependencyReview));
core.setOutput('run-workflow-validation', String(result.outputs.runWorkflowValidation));
core.setOutput('run-policy-validation', String(result.outputs.runPolicyValidation));
Expand Down Expand Up @@ -518,6 +567,25 @@ jobs:
typecheck-command: ${{ inputs.python-typecheck-command }}
test-command: ${{ inputs.python-test-command }}

go-ci:
name: go ci
needs:
- detect
- pr-contract
if: always() && needs.detect.outputs.ok == 'true' && needs.detect.outputs.run-go-ci == 'true' && (github.event_name != 'pull_request' || inputs.run-pr-contract == false || needs.pr-contract.result == 'success')
uses: ArchonVII/github-workflows/.github/workflows/go-ci.yml@v1
with:
go-versions: ${{ inputs.go-versions }}
os-matrix: ${{ inputs.os-matrix }}
exclude-modules: ${{ inputs.go-exclude-modules }}
run-tests: ${{ inputs.go-test }}
build-args: ${{ inputs.go-build-args }}
test-args: ${{ inputs.go-test-args }}
cgo-enabled: ${{ inputs.go-cgo-enabled }}
setup-command: ${{ inputs.go-setup-command }}
generate-command: ${{ inputs.go-generate-command }}
verify-generated: ${{ inputs.go-verify-generated }}

snapshot-validation:
name: snapshot validation
needs:
Expand Down Expand Up @@ -565,6 +633,7 @@ jobs:
- dependency-review
- node-ci
- python-ci
- go-ci
- snapshot-validation
if: always()
runs-on: ubuntu-latest
Expand All @@ -576,6 +645,7 @@ jobs:
OK: ${{ needs.detect.outputs.ok }}
RUN_NODE_CI: ${{ needs.detect.outputs.run-node-ci }}
RUN_PYTHON_CI: ${{ needs.detect.outputs.run-python-ci }}
RUN_GO_CI: ${{ needs.detect.outputs.run-go-ci }}
RUN_DEPENDENCY_REVIEW: ${{ inputs.run-dependency-review && needs.detect.outputs.run-dependency-review == 'true' }}
RUN_WORKFLOW_VALIDATION: ${{ inputs.run-workflow-validation && needs.detect.outputs.run-workflow-validation == 'true' }}
RUN_POLICY_VALIDATION: ${{ inputs.run-policy-validation && needs.detect.outputs.run-policy-validation == 'true' }}
Expand All @@ -587,6 +657,7 @@ jobs:
DEPENDENCY_RESULT: ${{ needs.dependency-review.result }}
NODE_RESULT: ${{ needs.node-ci.result }}
PYTHON_RESULT: ${{ needs.python-ci.result }}
GO_RESULT: ${{ needs.go-ci.result }}
SNAPSHOT_RESULT: ${{ needs.snapshot-validation.result }}
EVENT_NAME: ${{ github.event_name }}
RUN_PR_CONTRACT: ${{ inputs.run-pr-contract }}
Expand Down Expand Up @@ -634,6 +705,10 @@ jobs:
require_success "python ci" "$PYTHON_RESULT"
fi

if [ "$RUN_GO_CI" = "true" ]; then
require_success "go ci" "$GO_RESULT"
fi

if [ "$RUN_SNAPSHOT_VALIDATION" = "true" ]; then
require_success "snapshot validation" "$SNAPSHOT_RESULT"
fi
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ workflow, see [`docs/release-workflow-breakdown.md`](docs/release-workflow-break
| [`repo-required-gate.yml`](.github/workflows/repo-required-gate.yml) | Always-reporting PR gate for branch protection. Detects changed files, runs relevant internal checks, and exposes one stable `repo-required-gate / decision` check. | [`examples/repo-required-gate.yml`](examples/repo-required-gate.yml) |
| [`node-ci.yml`](.github/workflows/node-ci.yml) | Install + lint + typecheck + test for Node projects. Auto-detects `npm` / `pnpm` / `yarn` from lockfile. Matrix over Node versions and OS. | [`examples/node-ci.yml`](examples/node-ci.yml) |
| [`python-ci.yml`](.github/workflows/python-ci.yml) | Install (uv or pip) + ruff lint + ruff format-check + pyright + pytest. Each step opt-out. Matrix over Python versions and OS. | [`examples/python-ci.yml`](examples/python-ci.yml) |
| [`go-ci.yml`](.github/workflows/go-ci.yml) | Discover every tracked `go.mod` (`git ls-files`, skipping build artifacts) and run `go build` / `go vet` / `go test` per module. Optional codegen + dirty-tree drift check. Matrix over Go versions and OS. | [`examples/go-ci.yml`](examples/go-ci.yml) |

### Agent workflow

Expand Down
39 changes: 39 additions & 0 deletions examples/go-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Standalone caller for the reusable Go CI in ArchonVII/github-workflows.
# Most repos consume Go CI through repo-required-gate.yml (stack: go, or
# go-paths under stack: polyglot) instead — use this only when you want a
# separate, non-gated Go check.
#
# Discovers every tracked go.mod (via `git ls-files`, skipping gitignored
# build artifacts) and runs build/vet/test per module.

name: Go CI

on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
go:
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
uses: ArchonVII/github-workflows/.github/workflows/go-ci.yml@v1
with:
go-versions: '["stable"]'
# os-matrix: '["ubuntu-latest","windows-latest"]'
# exclude-modules: |
# examples/
# internal/demo/
# cgo-enabled: "1"
# run-tests: true
# build-args: '-tags osusergo ./...'
# generate-command: task generate
# verify-generated: true
Loading
Loading