Add scripts-to-rule-them-all scaffold (refs #1)
All checks were successful
check / check (push) Successful in 6s
All checks were successful
check / check (push) Successful in 6s
Bring the repo into conformance with the scripts-to-rule-them-all (STRTA) scaffold. The real logic that lived inline in the Makefile now lives in POSIX-sh entrypoints under script/, and the Makefile's standard targets are thin @script/NAME shims. - script/: bootstrap, setup, projectname, test, lint, fmt, fmt-check, check, docker, precommit, install-precommit, cibuild. All are executable #!/bin/sh entrypoints; the go mod tidy guard from the old inline hooks recipe moved into script/precommit. - Makefile: the nine standard targets (bootstrap, setup, test, lint, fmt, fmt-check, check, docker, hooks) are now thin shims; the repo-specific sfdupes/build/clean targets and the CGO_ENABLED export are preserved. - .gitea/workflows/check.yml: run script/cibuild instead of a bare docker build. - Dockerfile: run make check (and the build) as an unprivileged builder user rather than root. We should never build or run as root, and doing so also lets the permission-denied tests run legitimately: root bypasses the chmod(0) that TestScanHardlinkRunFailsTogether relies on, which made the in-image make check fail. HOME and the Go caches point at the user's home so go build/test and golangci-lint can write. make check passes locally and docker build . is green (the in-image non-root make check passes, including the hardlink permission test).
This commit is contained in:
@@ -6,4 +6,4 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# actions/checkout v4.2.2, 2026-02-22
|
# actions/checkout v4.2.2, 2026-02-22
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
- run: docker build .
|
- run: script/cibuild
|
||||||
|
|||||||
17
Dockerfile
17
Dockerfile
@@ -14,6 +14,14 @@ FROM golang@sha256:56961d79ea8129efddcc0b8643fd8a5416b4e6228cfd477e3fd61deb2672c
|
|||||||
|
|
||||||
RUN apk add --no-cache make
|
RUN apk add --no-cache make
|
||||||
|
|
||||||
|
# We never build or run as root. Create an unprivileged user and point
|
||||||
|
# HOME and the Go caches at its home so go build/test and golangci-lint
|
||||||
|
# can write their caches when we drop to it below.
|
||||||
|
RUN adduser -D -u 1000 builder
|
||||||
|
ENV HOME=/home/builder
|
||||||
|
ENV GOPATH=/home/builder/go
|
||||||
|
ENV GOCACHE=/home/builder/.cache/go-build
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# Reuse the linter binary from the lint stage; the copy also forces
|
# Reuse the linter binary from the lint stage; the copy also forces
|
||||||
@@ -24,7 +32,14 @@ COPY go.mod go.sum ./
|
|||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Fail the build unless the branch is green.
|
# Hand the sources and caches to the unprivileged user, then drop root
|
||||||
|
# before running any checks or builds.
|
||||||
|
RUN chown -R builder:builder /src /home/builder
|
||||||
|
USER builder
|
||||||
|
|
||||||
|
# Fail the build unless the branch is green. Runs as non-root so the
|
||||||
|
# permission-denied test paths are exercised legitimately (root would
|
||||||
|
# bypass the chmod(0) the tests rely on).
|
||||||
RUN make check
|
RUN make check
|
||||||
|
|
||||||
RUN make build
|
RUN make build
|
||||||
|
|||||||
37
Makefile
37
Makefile
@@ -6,7 +6,10 @@ BINARY := sfdupes
|
|||||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||||
LDFLAGS := -X main.Version=$(VERSION)
|
LDFLAGS := -X main.Version=$(VERSION)
|
||||||
|
|
||||||
.PHONY: sfdupes build test lint fmt fmt-check check docker hooks clean
|
.PHONY: sfdupes build bootstrap setup test lint fmt fmt-check check docker hooks clean
|
||||||
|
|
||||||
|
# Standard targets are thin shims; the implementations live in script/
|
||||||
|
# per the scripts-to-rule-them-all pattern.
|
||||||
|
|
||||||
# Default target: build the binary. Phony so go build (which has its
|
# Default target: build the binary. Phony so go build (which has its
|
||||||
# own build cache) always decides what to recompile.
|
# own build cache) always decides what to recompile.
|
||||||
@@ -15,36 +18,32 @@ sfdupes:
|
|||||||
|
|
||||||
build: sfdupes
|
build: sfdupes
|
||||||
|
|
||||||
|
bootstrap:
|
||||||
|
@script/bootstrap
|
||||||
|
|
||||||
|
setup:
|
||||||
|
@script/setup
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test -timeout 30s -cover ./... || \
|
@script/test
|
||||||
{ echo "--- Rerunning with -v for details ---"; \
|
|
||||||
go test -timeout 30s -v ./...; exit 1; }
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
golangci-lint run --config .golangci.yml ./...
|
@script/lint
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
gofmt -s -w .
|
@script/fmt
|
||||||
|
|
||||||
fmt-check:
|
fmt-check:
|
||||||
@files="$$(gofmt -l -s .)"; if [ -n "$$files" ]; then \
|
@script/fmt-check
|
||||||
echo "gofmt: files not formatted:"; echo "$$files"; exit 1; fi
|
|
||||||
|
|
||||||
check: test lint fmt-check
|
check:
|
||||||
|
@script/check
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
docker build -t $(BINARY) .
|
@script/docker
|
||||||
|
|
||||||
# Hooks are shared between the main checkout and all worktrees, so
|
|
||||||
# resolve the common git dir instead of assuming .git is a directory.
|
|
||||||
HOOKS_DIR := $(shell git rev-parse --git-common-dir)/hooks
|
|
||||||
|
|
||||||
hooks:
|
hooks:
|
||||||
@printf '#!/bin/sh\nset -e\n' > $(HOOKS_DIR)/pre-commit
|
@script/install-precommit
|
||||||
@printf 'go mod tidy\ngo fmt ./...\n' >> $(HOOKS_DIR)/pre-commit
|
|
||||||
@printf 'git diff --exit-code -- go.mod go.sum || { echo "go mod tidy changed files; please stage and retry"; exit 1; }\n' >> $(HOOKS_DIR)/pre-commit
|
|
||||||
@printf 'make check\n' >> $(HOOKS_DIR)/pre-commit
|
|
||||||
@chmod +x $(HOOKS_DIR)/pre-commit
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(BINARY) files.dat
|
rm -f $(BINARY) files.dat
|
||||||
|
|||||||
79
script/bootstrap
Executable file
79
script/bootstrap
Executable file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/bootstrap: install all dependencies needed to build and develop
|
||||||
|
# this repo. Idempotent: every install is guarded by a check so already
|
||||||
|
# installed tools are skipped. Base tooling comes from nix, apt, brew,
|
||||||
|
# or apk (detected in that order); assumes nothing is present.
|
||||||
|
# golangci-lint is installed via `go install` pinned to the same version
|
||||||
|
# the Dockerfile lint stage uses (never "latest").
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
# Pinned versions, 2026-07-25 (same version as the Dockerfile lint stage).
|
||||||
|
# golangci-lint v2.12.1
|
||||||
|
GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.1"
|
||||||
|
|
||||||
|
PKGMGR=""
|
||||||
|
SUDO=""
|
||||||
|
APT_UPDATED=""
|
||||||
|
|
||||||
|
detect_pkgmgr() {
|
||||||
|
[ -n "$PKGMGR" ] && return 0
|
||||||
|
if command -v nix-env >/dev/null 2>&1; then
|
||||||
|
PKGMGR="nix"
|
||||||
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
|
PKGMGR="apt"
|
||||||
|
elif command -v brew >/dev/null 2>&1; then
|
||||||
|
PKGMGR="brew"
|
||||||
|
elif command -v apk >/dev/null 2>&1; then
|
||||||
|
PKGMGR="apk"
|
||||||
|
else
|
||||||
|
echo "bootstrap: no supported package manager (nix, apt, brew, apk)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$PKGMGR" = "apt" ]; then
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
SUDO="sudo"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# pkg_install <nix-attr> <apt-pkg> <brew-formula> <apk-pkg>
|
||||||
|
pkg_install() {
|
||||||
|
detect_pkgmgr
|
||||||
|
case "$PKGMGR" in
|
||||||
|
nix) nix-env -iA "nixpkgs.$1" ;;
|
||||||
|
apt)
|
||||||
|
if [ -z "$APT_UPDATED" ]; then
|
||||||
|
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get update
|
||||||
|
APT_UPDATED=1
|
||||||
|
fi
|
||||||
|
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2"
|
||||||
|
;;
|
||||||
|
brew) brew install "$3" ;;
|
||||||
|
apk) apk add --no-cache "$4" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
missing() {
|
||||||
|
! command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
if missing git; then pkg_install git git git git; fi
|
||||||
|
if missing make; then pkg_install gnumake make make make; fi
|
||||||
|
if missing go; then pkg_install go golang go go; fi
|
||||||
|
|
||||||
|
# Lint tooling, pinned via go install (installs into
|
||||||
|
# "$(go env GOPATH)/bin"; ensure that is on your PATH).
|
||||||
|
if missing golangci-lint; then go install "$GOLANGCI_LINT_REF"; fi
|
||||||
|
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
echo "bootstrap complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
14
script/check
Executable file
14
script/check
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/check: run all checks (test, lint, fmt-check). Our own
|
||||||
|
# extension to scripts-to-rule-them-all. Must not modify any files.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
"$SCRIPT_DIR/test"
|
||||||
|
"$SCRIPT_DIR/lint"
|
||||||
|
"$SCRIPT_DIR/fmt-check"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
14
script/cibuild
Executable file
14
script/cibuild
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/cibuild: run the CI build. The Dockerfile runs make check (via
|
||||||
|
# script/check), so a successful build implies all checks pass. The
|
||||||
|
# Gitea workflow runs this on push.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
docker build .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
14
script/docker
Executable file
14
script/docker
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/docker: build the Docker image tagged with the project name.
|
||||||
|
# Identical in all repos; the tag comes from script/projectname.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
docker build -t "$("$SCRIPT_DIR/projectname")" .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
12
script/fmt
Executable file
12
script/fmt
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/fmt: format all files (writes).
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
gofmt -s -w .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
18
script/fmt-check
Executable file
18
script/fmt-check
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/fmt-check: check formatting (read-only). Same scope as
|
||||||
|
# script/fmt, but fails instead of writing.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
files="$(gofmt -s -l .)"
|
||||||
|
if [ -n "$files" ]; then
|
||||||
|
echo "gofmt: files not formatted:" >&2
|
||||||
|
echo "$files" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
20
script/install-precommit
Executable file
20
script/install-precommit
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/install-precommit: install the git pre-commit hook that runs
|
||||||
|
# script/precommit. Our own extension to scripts-to-rule-them-all.
|
||||||
|
# Hooks are shared between the main checkout and all worktrees, so
|
||||||
|
# resolve the common git dir instead of assuming .git is a directory.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
hooks_dir="$(git rev-parse --git-common-dir)/hooks"
|
||||||
|
mkdir -p "$hooks_dir"
|
||||||
|
hook="$hooks_dir/pre-commit"
|
||||||
|
printf '#!/bin/sh\nset -e\nscript/precommit\n' > "$hook"
|
||||||
|
chmod +x "$hook"
|
||||||
|
echo "pre-commit hook installed: runs script/precommit"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
12
script/lint
Executable file
12
script/lint
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/lint: run the linter.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
golangci-lint run --config .golangci.yml ./...
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
21
script/precommit
Executable file
21
script/precommit
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/precommit: run by the git pre-commit hook; fails the commit if
|
||||||
|
# checks fail. Our own extension to scripts-to-rule-them-all. Go extra:
|
||||||
|
# go mod tidy must be a no-op before the checks run.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
go mod tidy
|
||||||
|
if ! git diff --exit-code -- go.mod go.sum; then
|
||||||
|
echo "precommit: go mod tidy changed go.mod/go.sum;" \
|
||||||
|
"stage the changes and retry" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"$SCRIPT_DIR/check"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
12
script/projectname
Executable file
12
script/projectname
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/projectname: output the name of this project. Our own
|
||||||
|
# extension to scripts-to-rule-them-all. Other scripts that need the
|
||||||
|
# name (e.g. script/docker) call this, so they can stay identical
|
||||||
|
# across all repos.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
main() {
|
||||||
|
echo "sfdupes"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
13
script/setup
Executable file
13
script/setup
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/setup: set up the repo for development after a fresh clone:
|
||||||
|
# installs dependencies (script/bootstrap) and the git pre-commit hook.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
"$SCRIPT_DIR/bootstrap"
|
||||||
|
"$SCRIPT_DIR/install-precommit"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
17
script/test
Executable file
17
script/test
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/test: run the test suite. Reruns verbosely on failure so CI
|
||||||
|
# logs show which test failed.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
go test -timeout 30s -cover ./... || {
|
||||||
|
echo "--- Rerunning with -v for details ---"
|
||||||
|
go test -timeout 30s -v ./...
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user