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
92 changes: 92 additions & 0 deletions .github/scripts/run-mockery.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env sh
# run-mockery.test.sh β€” exercise .github/scripts/run-mockery.sh end-to-end.
#
# run-mockery.sh is the `entry` of the pre-commit `mockery` hook
# (.pre-commit-config.yaml). Its whole reason to exist is the guarded no-op that
# keeps a *fresh clone's* commit green: the scaffold ships no interfaces and no
# mockery config, so the hook must do nothing until the project adds a
# `.mockery.yml`/`.mockery.yaml`, and must fail with a helpful message β€” not a
# bare `command not found` β€” once a config exists but mockery is not installed.
# A silent regression (e.g. "simplifying" it to a bare `exec mockery`) would make
# every fresh clone's first commit fail, and it has no other coverage. This test
# pins its three real behaviours:
# * no mockery config -> exit 0, silent no-op,
# * config present, mockery PATH-absent -> exit 1 with an install hint,
# * config present, mockery present -> it is exec'd (runs to completion).
#
# It runs the REAL script against throwaway working directories with a fully
# stripped PATH, so the result never depends on whatever tools the runner
# happens to have installed (the missing-mockery case is then guaranteed, not
# probabilistic). Run locally with `sh .github/scripts/run-mockery.test.sh`; CI
# runs it via .github/workflows/validate-scaffold.yaml.
set -eu

repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
script="$repo_root/.github/scripts/run-mockery.sh"
# Absolute bash path: the test below runs the (bash) script under a stripped
# PATH, so it must not depend on PATH to locate the interpreter itself.
bash_bin="$(command -v bash)"

[ -f "$script" ] || { echo "FAIL: $script not found" >&2; exit 1; }
[ -n "$bash_bin" ] || { echo "FAIL: bash not on PATH" >&2; exit 1; }

fail() {
echo "FAIL: $*" >&2
exit 1
}

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

# --- Case 1: no mockery config -> silent no-op, exit 0 ------------------------
work1="$tmp/no-config"
mkdir -p "$work1"
out1="$tmp/out1"
err1="$tmp/err1"
rc=0
( cd "$work1" && PATH="" "$bash_bin" "$script" ) >"$out1" 2>"$err1" || rc=$?
[ "$rc" -eq 0 ] || fail "no-config case should exit 0, got $rc (stderr: $(cat "$err1"))"
[ -s "$out1" ] && fail "no-config case must be silent on stdout, got: $(cat "$out1")"
[ -s "$err1" ] && fail "no-config case must be silent on stderr, got: $(cat "$err1")"

# --- Case 2: config present, mockery absent -> exit 1 with install hint -------
work2="$tmp/config-no-mockery"
mkdir -p "$work2"
: >"$work2/.mockery.yml"
err2="$tmp/err2"
rc=0
# PATH="" makes `command -v mockery` inside the script fail deterministically.
( cd "$work2" && PATH="" "$bash_bin" "$script" ) >/dev/null 2>"$err2" || rc=$?
[ "$rc" -eq 1 ] || fail "missing-mockery case should exit 1, got $rc"
grep -qi 'mockery not found' "$err2" || fail "missing-mockery case must explain the failure (stderr: $(cat "$err2"))"
grep -qi 'go install' "$err2" || fail "missing-mockery case must print the install hint (stderr: $(cat "$err2"))"

# Also accept a .mockery.yaml (the script checks both extensions).
work2b="$tmp/config-yaml-no-mockery"
mkdir -p "$work2b"
: >"$work2b/.mockery.yaml"
rc=0
( cd "$work2b" && PATH="" "$bash_bin" "$script" ) >/dev/null 2>/dev/null || rc=$?
[ "$rc" -eq 1 ] || fail ".mockery.yaml should also trigger the run path (exit 1 when mockery absent), got $rc"

# --- Case 3: config present, mockery present -> it is exec'd -------------------
work3="$tmp/config-with-mockery"
mkdir -p "$work3"
: >"$work3/.mockery.yml"
stubbin="$tmp/bin"
mkdir -p "$stubbin"
marker="$tmp/mockery-ran"
# /bin/sh shebang (absolute) so the stub launches under the stripped PATH; it
# uses only shell built-ins, so it needs nothing else on PATH.
cat >"$stubbin/mockery" <<EOF
#!/bin/sh
echo ran >"$marker"
exit 0
EOF
chmod +x "$stubbin/mockery"
rc=0
( cd "$work3" && PATH="$stubbin" "$bash_bin" "$script" ) >/dev/null 2>/dev/null || rc=$?
[ "$rc" -eq 0 ] || fail "present-mockery case should exit 0 (exec mockery), got $rc"
[ -f "$marker" ] || fail "present-mockery case must exec mockery (stub marker not written)"

echo "PASS: run-mockery.sh (no-op without config + helpful error when mockery absent + exec when present)"
21 changes: 13 additions & 8 deletions .github/workflows/validate-scaffold.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ on:

permissions: {}

# Template-repo-only scaffold-integrity gate. scripts/rename-placeholders.sh is
# the first thing a newcomer runs after `Use this template`; a silent regression
# in it would ship broken to every project created from this template, and it has
# no other coverage. This job runs ONLY in the template repo; it no-ops in
# projects created from it (the guard below is false there), mirroring how
# cd.yaml/release.yaml run only downstream. Generated projects delete the
# onboarding script and validate their own code via the injected
# validate-go-project workflow.
# Template-repo-only scaffold-integrity gate for the scaffold's two shell
# helpers, neither of which the Go gate covers:
# * scripts/rename-placeholders.sh β€” the first thing a newcomer runs after
# `Use this template`; a silent regression ships broken to every generated
# project.
# * .github/scripts/run-mockery.sh β€” the pre-commit `mockery` hook's entry
# point; a regression makes every fresh clone's first commit fail.
# This job runs ONLY in the template repo; it no-ops in projects created from it
# (the guard below is false there), mirroring how cd.yaml/release.yaml run only
# downstream. Generated projects delete the onboarding script and validate their
# own code via the injected validate-go-project workflow.
jobs:
validate-scaffold:
name: Validate Scaffold
Expand All @@ -36,3 +39,5 @@ jobs:
go-version-file: go.mod
- name: 🧱 Validate onboarding (rename-placeholders.sh)
run: sh scripts/rename-placeholders.test.sh
- name: πŸ§ͺ Validate pre-commit mockery hook (run-mockery.sh)
run: sh .github/scripts/run-mockery.test.sh
17 changes: 10 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ need.
- `cmd/`, `internal/`, `pkg/` β€” conventional Go layout, each kept with a `.gitkeep` placeholder for new code.
- `go.mod` / `go.sum` β€” module definition and checksums.
- `.golangci.yml` β€” golangci-lint v2 config (formatters + `default: all` linters, with a few opt-outs and mock-file exclusions).
- `.github/workflows/` β€” `ci.yaml` (required-checks aggregation on PRs/merge queue), `cd.yaml` (GoReleaser release on `v*` tags), `validate-scaffold.yaml` (template-repo-only gate that exercises the onboarding script β€” no-ops downstream), `release.yaml`, `sync-labels.yaml`, `todos.yaml`, and `copilot-setup-steps.yml`.
- `.github/workflows/` β€” `ci.yaml` (required-checks aggregation on PRs/merge queue), `cd.yaml` (GoReleaser release on `v*` tags), `validate-scaffold.yaml` (template-repo-only gate that exercises the onboarding script and the pre-commit mockery hook β€” no-ops downstream), `release.yaml`, `sync-labels.yaml`, `todos.yaml`, and `copilot-setup-steps.yml`.
- `.pre-commit-config.yaml` β€” local pre-commit hooks: `golangci-lint-fmt` (Go formatting) and mock generation (`mockery`, via `.github/scripts/run-mockery.sh`).
- `.github/scripts/run-mockery.sh` β€” the pre-commit mockery hook's entry point; a guarded no-op until the project adds a `.mockery.yml`/`.mockery.yaml`, then runs `mockery` (so a fresh clone's hook stays green while the generation step is already wired).
- `.github/scripts/run-mockery.test.sh` β€” hermetic test for the mockery hook: runs `run-mockery.sh` under a stripped PATH and asserts the three branches (silent no-op without a config, exit 1 + install hint when mockery is absent, exec when present). Run with `sh .github/scripts/run-mockery.test.sh`; CI runs it via `validate-scaffold.yaml`.
- `.mega-linter.yml`, `cspell.json` β€” local linting/spell-checking configuration.
- `scripts/rename-placeholders.sh` β€” one-shot onboarding: repoints the module path (`go.mod`, Go imports, README badges) to a new project's path, leaving the upstream **Use this template** links intact.
- `scripts/rename-placeholders.test.sh` β€” end-to-end test for the onboarding script: runs it against a throwaway copy, then asserts the module repoint, the badge rewrite, the upstream-link preservation, no stray temp files, and that the renamed scaffold builds/tests. Run with `sh scripts/rename-placeholders.test.sh`; CI runs it via `validate-scaffold.yaml`.
Expand Down Expand Up @@ -49,12 +50,14 @@ a separate, trivially-passing `CI - Required Checks` aggregator β€” **not** the
gate β€” so do **not** wire `validate-go-project` into `ci.yaml`; that would
double-run every job (see go-template#76, closed as invalid).

The **onboarding script** (`scripts/rename-placeholders.sh`) sits outside that Go
gate β€” it is shell, run once before any Go change β€” so it has its own template-
repo-only check, `validate-scaffold.yaml`, which runs
`scripts/rename-placeholders.test.sh`. Run that test locally (`sh
scripts/rename-placeholders.test.sh`) when touching either script; it no-ops in
generated projects (the `github.repository` guard).
The scaffold's **shell helpers** sit outside that Go gate β€” they are shell, not
Go β€” so they have their own template-repo-only check, `validate-scaffold.yaml`,
which runs both `scripts/rename-placeholders.test.sh` (the onboarding script,
run once before any Go change) and `.github/scripts/run-mockery.test.sh` (the
pre-commit mockery hook). Run the matching test locally (`sh
scripts/rename-placeholders.test.sh` / `sh .github/scripts/run-mockery.test.sh`)
when touching either script; the gate no-ops in generated projects (the
`github.repository` guard).

## Maintenance (autonomous AI assistant)

Expand Down