Skip to content
Closed
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
109 changes: 109 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: CI

on:
push:
branches: [ develop, main, master ]
pull_request:
branches: [ '**' ]

permissions:
contents: read

jobs:
build:
name: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Print Go version
run: go version

- name: Install ripgrep (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y ripgrep

- name: Install ripgrep (macOS)
if: runner.os == 'macOS'
run: |
brew update
brew install ripgrep

- name: Install make and ripgrep (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
choco install -y make ripgrep
echo "make version:"; make --version || true
echo "rg version:"; rg --version || true

- name: Print Go env
run: go env

- name: Tidy
shell: bash
run: make tidy

- name: lint (includes check-go-version)
shell: bash
run: |
set -o pipefail
make lint 2>&1 | tee lint.log

- name: Assert lint order (check-go-version before golangci-lint)
shell: bash
run: |
test -f lint.log || { echo "lint.log missing"; exit 1; }
L_CHECK=$(rg -n "^check-go-version: OK" -N lint.log | head -1 | cut -d: -f1)
L_GCL=$(rg -n "^golangci-lint version" -N lint.log | head -1 | cut -d: -f1)
if [ -z "$L_CHECK" ]; then echo "Missing 'check-go-version: OK' line in lint.log"; exit 1; fi
if [ -z "$L_GCL" ]; then echo "Missing 'golangci-lint version' line in lint.log"; exit 1; fi
if [ "$L_CHECK" -ge "$L_GCL" ]; then
echo "Ordering incorrect: 'check-go-version: OK' occurs at line $L_CHECK, after golangci-lint version at line $L_GCL"; exit 1;
fi
echo "Lint order OK: check-go-version runs before golangci-lint"

- name: Upload lint.log artifact
uses: actions/upload-artifact@v4
with:
name: lint-${{ matrix.os }}
path: lint.log
if-no-files-found: error

- name: Tools path hygiene
shell: bash
run: make check-tools-paths

- name: Verify tools manifest commands
shell: bash
run: make verify-manifest-paths

- name: Test
shell: bash
run: make test

- name: Test clean-logs guard
shell: bash
run: make test-clean-logs

- name: Build
shell: bash
run: make build

- name: Build tools
shell: bash
run: make build-tools
85 changes: 85 additions & 0 deletions internal/ci/ci_workflow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package ci

import (
"os"
"path/filepath"
"strings"
"testing"
)

// This test asserts two things locally without requiring CI:
// 1) Makefile lint recipe runs check-go-version before invoking golangci-lint
// 2) The CI workflow includes an explicit step that verifies the ordering
func TestLintOrderLocallyAndInWorkflow(t *testing.T) {
repoRoot, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}

// Assert Makefile ordering: check-go-version appears before golangci-lint
mkPath := filepath.Join(repoRoot, "..", "..", "Makefile")
mkBytes, err := os.ReadFile(mkPath)
if err != nil {
t.Fatalf("read Makefile: %v", err)
}
mk := string(mkBytes)
if !strings.Contains(mk, "lint:") {
t.Fatalf("Makefile missing 'lint:' target")
}
// Extract only the lint recipe block (the lines starting with a tab after 'lint:')
lines := strings.Split(mk, "\n")
lintIdx := -1
for i, ln := range lines {
if strings.HasPrefix(ln, "lint:") {
lintIdx = i
break
}
}
if lintIdx < 0 {
t.Fatalf("Makefile missing lint target label")
}
var recipeLines []string
for j := lintIdx + 1; j < len(lines); j++ {
ln := lines[j]
if strings.HasPrefix(ln, "\t") { // recipe lines start with a tab
recipeLines = append(recipeLines, ln)
continue
}
// Stop when we hit the next non-recipe line (new target or blank without tab)
if strings.TrimSpace(ln) == "" {
// allow empty recipe line with tab only
if strings.HasPrefix(ln, "\t") {
recipeLines = append(recipeLines, ln)
continue
}
}
// Not a recipe line: end of recipe
break
}
recipe := strings.Join(recipeLines, "\n")
idxCheck := strings.Index(recipe, "check-go-version")
if idxCheck < 0 {
t.Fatalf("lint recipe missing 'check-go-version' invocation")
}
idxGcl := strings.Index(recipe, "golangci-lint")
if idxGcl < 0 {
t.Fatalf("lint recipe missing 'golangci-lint' invocation")
}
if !(idxCheck < idxGcl) {
t.Fatalf("expected check-go-version to run before golangci-lint inside lint recipe (idx %d < %d)", idxCheck, idxGcl)
}

// Assert CI workflow includes the lint order assertion step
wfPath := filepath.Join(repoRoot, "..", "..", ".github", "workflows", "ci.yml")
wfBytes, err := os.ReadFile(wfPath)
if err != nil {
t.Fatalf("read ci workflow: %v", err)
}
wf := string(wfBytes)
if !strings.Contains(wf, "lint (includes check-go-version)") {
t.Fatalf("workflow missing explicit lint step name indicating check-go-version inclusion")
}
if !strings.Contains(wf, "Assert lint order (check-go-version before golangci-lint)") {
t.Fatalf("workflow missing order assertion step")
}
}
Loading