Adopt the scripts-to-rule-them-all layout and the canonical Docker gates
check / check (push) Successful in 1m3s
check / check (push) Successful in 1m3s
script/ holds the entrypoints and the Makefile targets shim to them. Linting and testing are phases of the Dockerfile, built alone by script/lint and script/test with a fresh CACHEBUST value and cache mounts for Go's build cache and golangci-lint's; the build stage depends on both, and the runtime stage ships the binary and the source without .git. Formatting stays on the host in script/fmt and script/fmt-check (gofmt and the prettier pinned in yarn.lock). script/cibuild bootstraps, runs script/check and builds the tagged image, and the Gitea workflow calls it. script/precommit refuses a commit whose go.mod or go.sum is not tidy. cgo stays off, so the test phase runs on the alpine Go image without -race; that is the one departure from the canonical Go Dockerfile. Re-vendored from the prompts repository's next branch: REPO_POLICIES.md (2026-11-04), .golangci.yml (byte-identical, so the local exhaustruct_v5 addition is gone now that canonical carries it), .gitea/workflows/check.yml, .editorconfig, .gitignore and .dockerignore, the last two followed by this repository's own entries. AGENTS.md holds the repository-specific guidance for agents. The README gains the Entrypoints section and describes the new gates.
This commit is contained in:
+79
-8
@@ -1,9 +1,80 @@
|
|||||||
.git
|
# .dockerignore does NOT use .gitignore semantics. Docker matches with
|
||||||
node_modules
|
# moby/patternmatcher: filepath.Match plus `**`, so `*` does not cross
|
||||||
.DS_Store
|
# `/` and an unprefixed pattern is anchored at the context root. Every
|
||||||
|
# depth-independent pattern therefore needs `**/`, or `config/.env` and
|
||||||
|
# `certs/server.key` still ship while this file reads as solved. Only
|
||||||
|
# genuinely root-anchored entries go unprefixed. Never transplant these
|
||||||
|
# into .gitignore, where `**/` is wrong.
|
||||||
|
#
|
||||||
|
# Matching is case-sensitive, so secrets use character ranges rather
|
||||||
|
# than an ALL-CAPS twin, which would still miss `Server.Key`.
|
||||||
|
#
|
||||||
|
# Extend with this repo's own host-built artifacts, written anchored:
|
||||||
|
# `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and
|
||||||
|
# deletes the package directory from the context.
|
||||||
|
|
||||||
|
# .git is sent without its config. Without a VERSION build argument the
|
||||||
|
# stage that compiles runs `git describe --tags --always` on .git, which
|
||||||
|
# does not need .git/config; that file can hold a credential, such as a
|
||||||
|
# password in a remote URL or the token the CI checkout step stores there.
|
||||||
|
# Each submodule keeps a config with the same exposure in its git directory
|
||||||
|
# under .git/modules/, nested again for a submodule's own submodules, or in
|
||||||
|
# its own .git directory when it keeps one.
|
||||||
|
# KNOWN GAP: a submodule whose name has a `config` segment (`config`,
|
||||||
|
# `deploy/config`, `config/lib`) loses its whole git directory, because
|
||||||
|
# `**/.git/modules/**/config` also matches that segment's directory
|
||||||
|
# under .git/modules/. Go's version stamping then fails the build;
|
||||||
|
# nothing leaks. Name such a submodule without that segment:
|
||||||
|
# `git submodule add --name`.
|
||||||
|
**/.git/config
|
||||||
|
**/.git/modules/**/config
|
||||||
|
|
||||||
|
# Agent scratch: one full checkout of the repo per in-flight agent.
|
||||||
|
# Anchored because it occurs once where agents run at the repo root.
|
||||||
|
# KNOWN GAP: a repo running agents in subdirectories still ships
|
||||||
|
# `services/api/.claude/` and must add its own anchored entry.
|
||||||
|
.claude
|
||||||
|
|
||||||
|
# Environment files. `*.env` covers bare `.env` and the `prod.env`
|
||||||
|
# convention. Re-include a committed template with a negation if the
|
||||||
|
# build needs one: `!docs/example.env`.
|
||||||
|
**/*.[eE][nN][vV]
|
||||||
|
**/.[eE][nN][vV].*
|
||||||
|
**/.[eE][nN][vV][rR][cC]
|
||||||
|
|
||||||
|
# Private keys and the bundles carrying them. Public certificates
|
||||||
|
# (*.crt, *.cer) are deliberately absent: they are legitimate inputs.
|
||||||
|
**/*.[pP][eE][mM]
|
||||||
|
**/*.[kK][eE][yY]
|
||||||
|
**/*.[pP]12
|
||||||
|
**/*.[pP][fF][xX]
|
||||||
|
**/[iI][dD]_[rR][sS][aA]
|
||||||
|
**/[iI][dD]_[dD][sS][aA]
|
||||||
|
**/[iI][dD]_[eE][cC][dD][sS][aA]
|
||||||
|
**/[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
|
||||||
|
**/[iI][dD]_[eE][dD]25519
|
||||||
|
**/[iI][dD]_[eE][dD]25519_[sS][kK]
|
||||||
|
|
||||||
|
# Dependencies: restored inside the image, never copied in.
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
# OS metadata.
|
||||||
|
**/.DS_Store
|
||||||
|
**/Thumbs.db
|
||||||
|
|
||||||
|
# Editor state: never a build input, and it churns COPY.
|
||||||
|
**/*.swp
|
||||||
|
**/*.swo
|
||||||
|
**/*~
|
||||||
|
**/*.bak
|
||||||
|
**/.idea
|
||||||
|
**/.vscode
|
||||||
|
**/*.sublime-*
|
||||||
|
|
||||||
|
# This repository's own host-built artifacts, anchored.
|
||||||
/sutil
|
/sutil
|
||||||
*.test
|
**/*.test
|
||||||
*.out
|
**/*.out
|
||||||
*.log
|
**/*.log
|
||||||
*.prof
|
**/*.prof
|
||||||
coverage.*
|
**/coverage.*
|
||||||
|
|||||||
@@ -10,3 +10,9 @@ insert_final_newline = true
|
|||||||
|
|
||||||
[Makefile]
|
[Makefile]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
# This repository's own sections, such as one for another language it
|
||||||
|
# uses, go below this comment, and a re-vendor keeps them.
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
name: check
|
name: check
|
||||||
on: [push]
|
on: [push]
|
||||||
|
# Free the shared runner: a new push cancels only the same branch's older run.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Free the shared runner from a hung build.
|
||||||
|
timeout-minutes: 20
|
||||||
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 .
|
# script/cibuild needs no token, so none is left in .git/config.
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
# All history and tags, so git describe finds the version tag.
|
||||||
|
fetch-depth: 0
|
||||||
|
- run: script/cibuild
|
||||||
|
|||||||
+39
-7
@@ -11,17 +11,49 @@ Thumbs.db
|
|||||||
.vscode/
|
.vscode/
|
||||||
*.sublime-*
|
*.sublime-*
|
||||||
|
|
||||||
# Node (prettier for markdown formatting)
|
# Agent scratch (worktrees of this repo, created and destroyed by
|
||||||
|
# in-flight tooling). Unanchored: .gitignore patterns already match at
|
||||||
|
# every depth, so no prefix is wanted here. This is not a .dockerignore
|
||||||
|
# entry and must not be given a `**/` prefix on the way into one.
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Node
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# Environment / secrets
|
# Secrets. Unanchored like every entry above, so each matches at every
|
||||||
.env
|
# depth. Matching is case-sensitive on Linux, so names use character
|
||||||
.env.*
|
# ranges rather than a lowercase form that misses `Server.Key`.
|
||||||
*.pem
|
|
||||||
*.key
|
|
||||||
|
|
||||||
# Go build and test artifacts
|
# Environment files. `*.env` covers bare `.env` and the `prod.env`
|
||||||
|
# convention. Only the templates `example.env` and `sample.env` are
|
||||||
|
# re-included below. A repository that commits any other template adds
|
||||||
|
# its own negation at the end of this file, for example `!.env.example`.
|
||||||
|
*.[eE][nN][vV]
|
||||||
|
.[eE][nN][vV].*
|
||||||
|
.[eE][nN][vV][rR][cC]
|
||||||
|
!example.env
|
||||||
|
!sample.env
|
||||||
|
|
||||||
|
# Private keys and the bundles carrying them.
|
||||||
|
*.[pP][eE][mM]
|
||||||
|
*.[kK][eE][yY]
|
||||||
|
*.[pP]12
|
||||||
|
*.[pP][fF][xX]
|
||||||
|
[iI][dD]_[rR][sS][aA]
|
||||||
|
[iI][dD]_[dD][sS][aA]
|
||||||
|
[iI][dD]_[eE][cC][dD][sS][aA]
|
||||||
|
[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
|
||||||
|
[iI][dD]_[eE][dD]25519
|
||||||
|
[iI][dD]_[eE][dD]25519_[sS][kK]
|
||||||
|
|
||||||
|
# This repository's own entries, such as its build outputs, go below
|
||||||
|
# this comment, and a re-vendor keeps them. Anchor a binary built at the
|
||||||
|
# root: `/myapp`, never `myapp`, which also ignores `cmd/myapp/`.
|
||||||
|
|
||||||
|
# The sutil binary that `make build` writes at the root.
|
||||||
/sutil
|
/sutil
|
||||||
|
|
||||||
|
# Go test and profiling outputs.
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
+33
-10
@@ -3,6 +3,9 @@ version: "2"
|
|||||||
# Config schema uses the golangci-lint v2 layout (settings live under
|
# Config schema uses the golangci-lint v2 layout (settings live under
|
||||||
# linters.settings, not top-level linters-settings) so that the
|
# linters.settings, not top-level linters-settings) so that the
|
||||||
# thresholds below are actually applied by golangci-lint >= v2.
|
# thresholds below are actually applied by golangci-lint >= v2.
|
||||||
|
#
|
||||||
|
# This file has no formatters section, so golangci-lint run does not run
|
||||||
|
# gofmt. A repository's script/fmt-check runs it, on the host.
|
||||||
|
|
||||||
run:
|
run:
|
||||||
timeout: 5m
|
timeout: 5m
|
||||||
@@ -16,13 +19,13 @@ linters:
|
|||||||
- gomodguard_v2
|
- gomodguard_v2
|
||||||
disable:
|
disable:
|
||||||
# Genuinely incompatible with project patterns
|
# Genuinely incompatible with project patterns
|
||||||
- exhaustruct # Requires all struct fields
|
- exhaustruct_v5 # Requires all struct fields
|
||||||
- exhaustruct_v5 # Same linter, newer major version (since v2.13.0)
|
|
||||||
- godot # Requires comments to end with periods
|
- godot # Requires comments to end with periods
|
||||||
- wrapcheck # Too verbose for internal packages
|
- wrapcheck # Too verbose for internal packages
|
||||||
- varnamelen # Short names like db, id are idiomatic Go
|
- varnamelen # Short names like db, id are idiomatic Go
|
||||||
# Deprecated: the warning is attached to the old name, so it is
|
# Deprecated: the warning is attached to the old name, so it is
|
||||||
# silenced by disabling that name, not by enabling the successor.
|
# silenced by disabling that name, not by enabling the successor.
|
||||||
|
- exhaustruct # Deprecated, replaced by exhaustruct_v5
|
||||||
- wsl # Deprecated, replaced by wsl_v5
|
- wsl # Deprecated, replaced by wsl_v5
|
||||||
- gomodguard # Deprecated, replaced by gomodguard_v2
|
- gomodguard # Deprecated, replaced by gomodguard_v2
|
||||||
settings:
|
settings:
|
||||||
@@ -40,7 +43,10 @@ linters:
|
|||||||
# test-support package exists to hand a test privileges the program
|
# test-support package exists to hand a test privileges the program
|
||||||
# itself must never have, so a file that is not a test must not import
|
# itself must never have, so a file that is not a test must not import
|
||||||
# one. Test files, and the files inside a package whose directory name
|
# one. Test files, and the files inside a package whose directory name
|
||||||
# ends in `test`, are where that code belongs, and are exempt.
|
# ends in `test`, are where that code belongs, and are exempt. A package
|
||||||
|
# in a subdirectory of that directory is not exempt. `${base-path}` is
|
||||||
|
# the directory holding this file, so the name of the directory a
|
||||||
|
# repository is cloned into does not count.
|
||||||
#
|
#
|
||||||
# The deny list below is the one part of this file a repository is
|
# The deny list below is the one part of this file a repository is
|
||||||
# expected to extend, and the only part it may. depguard matches an
|
# expected to extend, and the only part it may. depguard matches an
|
||||||
@@ -55,24 +61,25 @@ linters:
|
|||||||
files:
|
files:
|
||||||
- "$all"
|
- "$all"
|
||||||
- "!$test"
|
- "!$test"
|
||||||
- "!**/*test/**"
|
- "!${base-path}**/*test/*"
|
||||||
deny:
|
deny:
|
||||||
- pkg: net/http/httptest
|
- pkg: net/http/httptest
|
||||||
desc: >-
|
desc: >-
|
||||||
Test-support code belongs in test files and in packages whose
|
Test-support code belongs in test files and in packages whose
|
||||||
directory name ends in test, not in the shipped binary.
|
directory name ends in test, not in the shipped binary.
|
||||||
# Only decisions already recorded in the Go package defaults are
|
# Each entry names a module not to use and what to use instead. Every
|
||||||
# listed here. Every entry matches the module path exactly.
|
# entry matches the module path exactly.
|
||||||
gomodguard_v2:
|
gomodguard_v2:
|
||||||
blocked:
|
blocked:
|
||||||
- module: github.com/rs/zerolog
|
- module: github.com/rs/zerolog
|
||||||
recommendations:
|
recommendations:
|
||||||
- log/slog
|
- log/slog
|
||||||
reason: "Structured logging is stdlib log/slog."
|
reason: "Structured logging is stdlib log/slog."
|
||||||
# One entry per pre-fork module path, because the later releases
|
# These entries cover only the github.com/go-redis/redis module paths,
|
||||||
# are separate paths. A prefix match would be shorter but would
|
# one entry per path, because the later releases are separate paths.
|
||||||
# also reach github.com/go-redis/redismock, the test double for
|
# A prefix match would be shorter but would also reach
|
||||||
# the successor these entries recommend.
|
# github.com/go-redis/redismock/v9, the test double for the successor
|
||||||
|
# these entries recommend.
|
||||||
- module: github.com/go-redis/redis
|
- module: github.com/go-redis/redis
|
||||||
recommendations:
|
recommendations:
|
||||||
- github.com/redis/go-redis/v9
|
- github.com/redis/go-redis/v9
|
||||||
@@ -85,6 +92,10 @@ linters:
|
|||||||
recommendations:
|
recommendations:
|
||||||
- github.com/redis/go-redis/v9
|
- github.com/redis/go-redis/v9
|
||||||
reason: "Pre-fork module; use the maintained go-redis v9."
|
reason: "Pre-fork module; use the maintained go-redis v9."
|
||||||
|
- module: github.com/go-redis/redis/v9
|
||||||
|
recommendations:
|
||||||
|
- github.com/redis/go-redis/v9
|
||||||
|
reason: "Pre-fork module; use the maintained go-redis v9."
|
||||||
- module: github.com/sergi/go-diff
|
- module: github.com/sergi/go-diff
|
||||||
recommendations:
|
recommendations:
|
||||||
- github.com/aymanbagabas/go-udiff
|
- github.com/aymanbagabas/go-udiff
|
||||||
@@ -93,7 +104,19 @@ linters:
|
|||||||
recommendations:
|
recommendations:
|
||||||
- github.com/aymanbagabas/go-udiff
|
- github.com/aymanbagabas/go-udiff
|
||||||
reason: "Unmaintained fork; use go-udiff."
|
reason: "Unmaintained fork; use go-udiff."
|
||||||
|
exclusions:
|
||||||
|
rules:
|
||||||
|
# homoicon keeps a local copy of sobek, its JavaScript engine, as sneak
|
||||||
|
# decided in
|
||||||
|
# https://git.eeqj.de/sneak/homoicon/issues/1759#issuecomment-153261.
|
||||||
|
# This is an exclusion because gomoddirectives does not check its
|
||||||
|
# replace-allow-list for a replace that points at a local path.
|
||||||
|
- path: ^go\.mod$
|
||||||
|
linters:
|
||||||
|
- gomoddirectives
|
||||||
|
text: '^local replacement are not allowed: github\.com/grafana/sobek$'
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
max-issues-per-linter: 0
|
max-issues-per-linter: 0
|
||||||
max-same-issues: 0
|
max-same-issues: 0
|
||||||
|
uniq-by-line: false
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Guidance for coding agents working in this repository. The repository policies
|
||||||
|
are in `REPO_POLICIES.md`, vendored from the `prompts` repository; read that
|
||||||
|
first. Everything below is specific to this repository, and the README's Design
|
||||||
|
section explains each point.
|
||||||
|
|
||||||
|
- `pkg/` holds only packages designed to be imported by other programs. Anything
|
||||||
|
that exists to support `sutil` lives under `internal/`.
|
||||||
|
- `sutil` is not a command-line front end for the packages in `pkg/`. It holds
|
||||||
|
only the utilities the author wants available everywhere; a command is added
|
||||||
|
because a tool is wanted, not because a package exists.
|
||||||
|
- `pkg/cbk` derives crypto_box keypairs from BIP39 mnemonics with BIP85 and does
|
||||||
|
nothing else. BIP39 passphrases are deliberately unsupported.
|
||||||
|
- cgo stays off: builds are static, and the test phase runs without `-race`.
|
||||||
|
- Version tags are created by the author alone, when the author decides. Do not
|
||||||
|
propose, schedule, or create tags.
|
||||||
|
- `.golangci.yml` is byte-identical to the canonical copy and is not edited
|
||||||
|
here; changes go to the `prompts` repository.
|
||||||
+75
-36
@@ -1,56 +1,95 @@
|
|||||||
# The checks behind `make check` are split across independent stages so that
|
# In every stage ARG CACHEBUST comes right after the steps that install
|
||||||
# formatting and lint failures surface in seconds, before any compilation:
|
# system packages and download Go's modules, or right after FROM in a
|
||||||
|
# stage with neither, and every step that lints, tests or compiles
|
||||||
|
# comes after it. script/ passes a new value on every build, which
|
||||||
|
# reruns every RUN step after it. Docker keeps the layers above it
|
||||||
|
# until the base image, their own lines or go.mod and go.sum change.
|
||||||
|
# The steps that compile mount Go's build cache, and golangci-lint run
|
||||||
|
# also mounts golangci-lint's; both survive from build to build.
|
||||||
#
|
#
|
||||||
# lint - gofmt and golangci-lint (make fmt-check-go, make lint)
|
# This repository keeps cgo off, so its test phase departs from the
|
||||||
# docs - prettier check of all markdown (make fmt-check-md)
|
# canonical Go Dockerfile in REPO_POLICIES.md in one way: it runs on the
|
||||||
# builder - tests (make test) and the release build
|
# alpine Go image without -race, which needs a C toolchain. Everything
|
||||||
#
|
# else follows the canonical file.
|
||||||
# The builder stage declares explicit dependencies on lint and docs so
|
|
||||||
# BuildKit cannot finish the build unless every check passed.
|
|
||||||
|
|
||||||
# Lint stage: fast feedback on Go formatting and lint issues
|
# Lint phase
|
||||||
# golangci/golangci-lint:v2.15.0, 2026-10-16 (keep in sync with Makefile)
|
# golangci/golangci-lint:v2.15.0, 2026-10-16
|
||||||
FROM golangci/golangci-lint@sha256:fbfcfd12a6f8d7f1b97b3362ca80fc2a244d46863148c73cb88e7793af96c481 AS lint
|
FROM golangci/golangci-lint@sha256:fbfcfd12a6f8d7f1b97b3362ca80fc2a244d46863148c73cb88e7793af96c481 AS lint
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
ARG CACHEBUST
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN make fmt-check-go
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
RUN make lint
|
--mount=type=cache,target=/root/.cache/golangci-lint \
|
||||||
|
golangci-lint run --config .golangci.yml ./...
|
||||||
|
|
||||||
# Docs stage: markdown formatting check with the pinned prettier
|
# Test phase. -count=1 because the mounted cache also holds test
|
||||||
# node:24-alpine, 2026-10-28
|
# results, which go test would otherwise replay. A first run that timed
|
||||||
FROM node@sha256:8a32c8cf649a49915a66e81e7a609223c708d0adabf288ee3f4077d765542f85 AS docs
|
# out gets no rerun, because a rerun would only wait for the same hang.
|
||||||
RUN apk add --no-cache make
|
|
||||||
WORKDIR /src
|
|
||||||
COPY package.json yarn.lock ./
|
|
||||||
RUN yarn install --frozen-lockfile
|
|
||||||
COPY . .
|
|
||||||
RUN make fmt-check-md
|
|
||||||
|
|
||||||
# Build stage
|
|
||||||
# golang:1.26-alpine, 2026-10-27
|
# golang:1.26-alpine, 2026-10-27
|
||||||
FROM golang@sha256:a2012b4a1d1f9f7e9f44e0d39b805a901e856b1dd80b4e6ee4338db547050308 AS builder
|
FROM golang@sha256:a2012b4a1d1f9f7e9f44e0d39b805a901e856b1dd80b4e6ee4338db547050308 AS test
|
||||||
RUN apk add --no-cache make
|
ENV CGO_ENABLED=0
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# Force BuildKit to run the lint and docs stages before proceeding
|
|
||||||
COPY --from=lint /src/go.sum /dev/null
|
|
||||||
COPY --from=docs /src/package.json /dev/null
|
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
ARG CACHEBUST
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN make test
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
status=0; \
|
||||||
|
output=$(go test -count=1 -timeout 90s -cover ./... 2>&1) || status=$?; \
|
||||||
|
printf '%s\n' "$output"; \
|
||||||
|
test "$status" -eq 0 && exit 0; \
|
||||||
|
case "$output" in \
|
||||||
|
*"panic: test timed out"*|*"*** Test killed"*) exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
echo "--- Rerunning with -v for details ---"; \
|
||||||
|
go test -count=1 -timeout 90s -v ./...; \
|
||||||
|
exit 1
|
||||||
|
|
||||||
ARG VERSION=dev
|
# Build stage. Nothing is wanted from either phase above; the copies
|
||||||
RUN CGO_ENABLED=0 go build -trimpath \
|
# are what make BuildKit build them first, so nothing after them can
|
||||||
-ldflags="-s -w -X main.Version=${VERSION}" \
|
# run unless lint and test passed.
|
||||||
-o /sutil ./cmd/sutil
|
# golang:1.26-alpine, 2026-10-27
|
||||||
|
FROM golang@sha256:a2012b4a1d1f9f7e9f44e0d39b805a901e856b1dd80b4e6ee4338db547050308 AS builder
|
||||||
|
# git comes from the base image's own distribution package index, which
|
||||||
|
# is trusted, so it needs no version and no hash.
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
ARG CACHEBUST
|
||||||
|
COPY --from=lint /src/go.sum /dev/null
|
||||||
|
COPY --from=test /src/go.sum /dev/null
|
||||||
|
# A tar-stream context keeps the sender's file owners, which git refuses.
|
||||||
|
RUN git config --system --add safe.directory /src
|
||||||
|
COPY . .
|
||||||
|
|
||||||
# Runtime stage: the binary plus the full source it was built from
|
# The VERSION build arg when one is given, otherwise
|
||||||
|
# `git describe --tags --always` on the .git in the build context. With
|
||||||
|
# .git present, a version that is still empty, dev or unknown fails the
|
||||||
|
# build: git is missing or could not read the checkout.
|
||||||
|
ARG VERSION
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
VERSION="${VERSION:-$(git describe --tags --always)}"; \
|
||||||
|
if [ -e .git ]; then \
|
||||||
|
case "$VERSION" in ""|dev|unknown) \
|
||||||
|
echo "version is '$VERSION' although .git is present" >&2; \
|
||||||
|
exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
fi; \
|
||||||
|
CGO_ENABLED=0 go build -trimpath \
|
||||||
|
-ldflags="-s -w -X main.Version=${VERSION}" \
|
||||||
|
-o /sutil ./cmd/sutil
|
||||||
|
# The runtime stage ships the source; the repository history is not
|
||||||
|
# part of it.
|
||||||
|
RUN rm -rf /src/.git
|
||||||
|
|
||||||
|
# Runtime stage, and the last one: the binary plus the source it was
|
||||||
|
# built from.
|
||||||
# alpine:3.24, 2026-10-23
|
# alpine:3.24, 2026-10-23
|
||||||
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
|
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
|
||||||
|
ARG CACHEBUST
|
||||||
COPY --from=builder /sutil /usr/local/bin/sutil
|
COPY --from=builder /sutil /usr/local/bin/sutil
|
||||||
COPY --from=builder /src /usr/local/src/x
|
COPY --from=builder /src /usr/local/src/x
|
||||||
ENTRYPOINT ["sutil"]
|
ENTRYPOINT ["sutil"]
|
||||||
|
|||||||
@@ -1,30 +1,45 @@
|
|||||||
# sneak.berlin/go/x
|
.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks \
|
||||||
#
|
build install tidy clean
|
||||||
# This Makefile is the single source of truth for how this repo is built,
|
|
||||||
# tested, linted, and formatted. Always use these targets rather than
|
|
||||||
# invoking the underlying tools directly.
|
|
||||||
|
|
||||||
.PHONY: all build install test lint fmt fmt-go fmt-md fmt-check \
|
# Makefile targets are thin shims; the implementations live in script/
|
||||||
fmt-check-go fmt-check-md check docker hooks tidy setup clean
|
# per the scripts-to-rule-them-all pattern (see the Entrypoints section
|
||||||
|
# of README.md). The targets after the shims are this repo's own.
|
||||||
|
|
||||||
|
bootstrap:
|
||||||
|
@script/bootstrap
|
||||||
|
|
||||||
|
setup:
|
||||||
|
@script/setup
|
||||||
|
|
||||||
|
test:
|
||||||
|
@script/test
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@script/lint
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
@script/fmt
|
||||||
|
|
||||||
|
fmt-check:
|
||||||
|
@script/fmt-check
|
||||||
|
|
||||||
|
check:
|
||||||
|
@script/check
|
||||||
|
|
||||||
|
docker:
|
||||||
|
@script/docker
|
||||||
|
|
||||||
|
hooks:
|
||||||
|
@script/install-precommit
|
||||||
|
|
||||||
|
# This repository's own targets.
|
||||||
|
|
||||||
BINARY := sutil
|
BINARY := sutil
|
||||||
VERSION := $(shell git describe --always --dirty 2>/dev/null || echo dev)
|
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo unknown)
|
||||||
GOLDFLAGS := -X main.Version=$(VERSION)
|
GOLDFLAGS := -X main.Version=$(VERSION)
|
||||||
GO_DIRS := cmd internal pkg
|
|
||||||
|
|
||||||
# golangci/golangci-lint:v2.15.0, 2026-10-16 (keep in sync with Dockerfile)
|
# cgo stays off everywhere: builds are static, and the test phase of the
|
||||||
GOLANGCI_LINT_IMAGE := golangci/golangci-lint@sha256:fbfcfd12a6f8d7f1b97b3362ca80fc2a244d46863148c73cb88e7793af96c481
|
# Dockerfile runs without -race, which would need a C toolchain.
|
||||||
|
|
||||||
# flags are repeated here (also in .prettierrc) so this Makefile works
|
|
||||||
# standalone when copied as a template
|
|
||||||
PRETTIER := yarn run --silent prettier
|
|
||||||
PRETTIER_FLAGS := --tab-width 4 --prose-wrap always
|
|
||||||
MARKDOWN_GLOB := '**/*.md'
|
|
||||||
|
|
||||||
all: build
|
|
||||||
|
|
||||||
# cgo stays off everywhere: builds are static and the tests do not use
|
|
||||||
# -race, which would require a C toolchain.
|
|
||||||
export CGO_ENABLED := 0
|
export CGO_ENABLED := 0
|
||||||
|
|
||||||
# Build the sutil binary in the repo root with the git version embedded.
|
# Build the sutil binary in the repo root with the git version embedded.
|
||||||
@@ -35,81 +50,9 @@ build:
|
|||||||
install:
|
install:
|
||||||
go install -trimpath -ldflags "$(GOLDFLAGS)" ./cmd/$(BINARY)
|
go install -trimpath -ldflags "$(GOLDFLAGS)" ./cmd/$(BINARY)
|
||||||
|
|
||||||
# Run the tests quietly; on failure rerun verbosely for diagnostics and
|
# Tidy go.mod and go.sum; script/precommit refuses a commit that needs it.
|
||||||
# still fail.
|
|
||||||
test:
|
|
||||||
@go test -timeout 30s -cover ./... || \
|
|
||||||
{ echo "--- Rerunning with -v for details ---"; \
|
|
||||||
go test -timeout 30s -v ./...; exit 1; }
|
|
||||||
|
|
||||||
# Lint with the pinned golangci-lint image when docker is available so local
|
|
||||||
# results match CI exactly. Inside the Docker build (where there is no
|
|
||||||
# docker) the image's own golangci-lint binary is used. The host module
|
|
||||||
# cache is shared read-only; the linter and build caches live in named
|
|
||||||
# volumes so repeated runs are fast.
|
|
||||||
#
|
|
||||||
# .golangci.yml is verified against the linter's schema first: golangci-lint
|
|
||||||
# otherwise ignores keys it does not know and silently runs with defaults.
|
|
||||||
LINT_COMMAND := golangci-lint config verify && golangci-lint run ./...
|
|
||||||
|
|
||||||
lint:
|
|
||||||
@if command -v docker >/dev/null 2>&1; then \
|
|
||||||
docker run --rm \
|
|
||||||
-v "$(CURDIR)":/src \
|
|
||||||
-v "$$(go env GOMODCACHE)":/go/pkg/mod:ro \
|
|
||||||
-v sutil-golangci-lint-cache:/root/.cache/golangci-lint \
|
|
||||||
-v sutil-go-build-cache:/root/.cache/go-build \
|
|
||||||
-w /src $(GOLANGCI_LINT_IMAGE) sh -c '$(LINT_COMMAND)'; \
|
|
||||||
else \
|
|
||||||
$(LINT_COMMAND); \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Format everything (writes files).
|
|
||||||
fmt: fmt-go fmt-md
|
|
||||||
|
|
||||||
fmt-go:
|
|
||||||
gofmt -w $(GO_DIRS)
|
|
||||||
|
|
||||||
fmt-md:
|
|
||||||
$(PRETTIER) --write $(MARKDOWN_GLOB) $(PRETTIER_FLAGS)
|
|
||||||
|
|
||||||
# Check formatting without writing anything.
|
|
||||||
fmt-check: fmt-check-go fmt-check-md
|
|
||||||
|
|
||||||
fmt-check-go:
|
|
||||||
@unformatted="$$(gofmt -l $(GO_DIRS))"; \
|
|
||||||
if [ -n "$$unformatted" ]; then \
|
|
||||||
echo "gofmt: the following files need formatting:"; \
|
|
||||||
echo "$$unformatted"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
fmt-check-md:
|
|
||||||
$(PRETTIER) --check $(MARKDOWN_GLOB) $(PRETTIER_FLAGS)
|
|
||||||
|
|
||||||
# Everything CI gates on. Must not modify any files.
|
|
||||||
check: test lint fmt-check
|
|
||||||
|
|
||||||
# Build the Docker image; the Dockerfile runs the same checks as `make check`.
|
|
||||||
docker:
|
|
||||||
docker build -t $(BINARY) .
|
|
||||||
|
|
||||||
# Install the pre-commit hook.
|
|
||||||
hooks:
|
|
||||||
@printf '#!/bin/sh\nset -e\n' > .git/hooks/pre-commit
|
|
||||||
@if [ -f go.mod ]; then \
|
|
||||||
printf 'go mod tidy\ngo fmt ./...\ngit diff --exit-code -- go.mod go.sum || { echo "go mod tidy changed files; please stage and retry"; exit 1; }\n' >> .git/hooks/pre-commit; \
|
|
||||||
fi
|
|
||||||
@printf 'make check\n' >> .git/hooks/pre-commit
|
|
||||||
@chmod +x .git/hooks/pre-commit
|
|
||||||
|
|
||||||
# Tidy go.mod and go.sum.
|
|
||||||
tidy:
|
tidy:
|
||||||
go mod tidy
|
go mod tidy
|
||||||
|
|
||||||
# One-time developer setup: install the pinned prettier for markdown checks.
|
|
||||||
setup:
|
|
||||||
yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(BINARY)
|
rm -f $(BINARY)
|
||||||
|
|||||||
@@ -217,38 +217,74 @@ added because a tool is wanted, not because a package exists.
|
|||||||
|
|
||||||
### Build, test, and CI
|
### Build, test, and CI
|
||||||
|
|
||||||
The `Makefile` is the single entry point: `make test` (30-second timeout, quiet
|
The gates live in the `Dockerfile`, in the canonical Go layout that
|
||||||
unless something fails), `make lint`, `make fmt` and `make fmt-check` (gofmt for
|
`REPO_POLICIES.md` prescribes: a `lint` phase on the hash-pinned golangci-lint
|
||||||
Go, prettier for markdown), `make check` for all three, `make build`,
|
image and a `test` phase on the Go image. `script/lint` and `script/test` each
|
||||||
`make install`, `make docker`, `make hooks`, `make tidy`, and `make setup` to
|
build one phase alone, with a fresh `CACHEBUST` value so the check step runs on
|
||||||
install the pinned prettier.
|
every build instead of being served from Docker's cache, and write no image. The
|
||||||
|
build stage depends on both phases, so the image cannot be built unless they
|
||||||
|
pass. Go's build cache and golangci-lint's cache are mounted into the phases and
|
||||||
|
survive between builds. Formatting is the one check that runs on the host:
|
||||||
|
`script/fmt-check` runs gofmt and the prettier pinned in `yarn.lock`.
|
||||||
|
`script/check` runs all three. `script/cibuild`, which the Gitea workflow calls,
|
||||||
|
bootstraps, runs `script/check`, and then builds the tagged image with the
|
||||||
|
version from `git describe`.
|
||||||
|
|
||||||
`make lint` runs golangci-lint inside the same hash-pinned Docker image the CI
|
Every external reference, from base images to the GitHub Action in
|
||||||
build uses, so local results match CI exactly, and first verifies
|
|
||||||
`.golangci.yml` against the linter's schema, because golangci-lint otherwise
|
|
||||||
ignores keys it does not know and silently runs with defaults. The `Dockerfile`
|
|
||||||
splits `make check` across stages: a lint stage and a markdown-check stage fail
|
|
||||||
fast in seconds, and the build stage, which runs the tests and compiles `sutil`,
|
|
||||||
depends on both. The runtime image carries the binary and the source it was
|
|
||||||
built from. Every external reference, from base images to the GitHub Action in
|
|
||||||
`.gitea/workflows/check.yml`, is pinned by content hash with a version and date
|
`.gitea/workflows/check.yml`, is pinned by content hash with a version and date
|
||||||
comment. `.golangci.yml` is this repo's copy of the standard configuration, kept
|
comment. `.golangci.yml` is byte-identical to the canonical copy in the
|
||||||
in the v2 layout with `exhaustruct_v5` and the deprecated `gomodguard` disabled.
|
`prompts` repository and is never edited here; the lint phase pins the
|
||||||
cgo stays off: the Makefile exports `CGO_ENABLED=0` for every build and test, so
|
golangci-lint release that the policy names. cgo stays off: the Makefile exports
|
||||||
binaries are static and no C toolchain is needed anywhere.
|
`CGO_ENABLED=0`, binaries are static, and the test phase runs on the alpine Go
|
||||||
|
image without `-race`, which is this repository's one departure from the
|
||||||
|
canonical `Dockerfile`. The runtime image carries the binary and the source it
|
||||||
|
was built from.
|
||||||
|
|
||||||
Version tags are created by the author alone, when the author decides. Tooling,
|
Version tags are created by the author alone, when the author decides. Tooling,
|
||||||
contributors, and agents working in this repository do not propose, schedule, or
|
contributors, and agents working in this repository do not propose, schedule, or
|
||||||
create tags.
|
create tags.
|
||||||
|
|
||||||
|
## Entrypoints
|
||||||
|
|
||||||
|
This repository adheres to the
|
||||||
|
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
|
||||||
|
standard: normalized scripts in `script/` are the entrypoints for the
|
||||||
|
development workflow, and the Makefile targets are thin shims that call them. We
|
||||||
|
provide:
|
||||||
|
|
||||||
|
- `script/bootstrap`: install everything the repository needs, idempotently:
|
||||||
|
make, git, go, and, because `yarn.lock` is present, node, yarn 1 and the
|
||||||
|
pinned prettier.
|
||||||
|
- `script/setup`: make a fresh clone ready for development by running
|
||||||
|
`script/bootstrap` and then `script/install-precommit`.
|
||||||
|
- `script/projectname`: print the project name, `x`, which `script/docker` and
|
||||||
|
`script/cibuild` use as the image tag.
|
||||||
|
- `script/test`: build the `test` phase of the `Dockerfile` with a new
|
||||||
|
`CACHEBUST` value, writing no image.
|
||||||
|
- `script/lint`: build the `lint` phase the same way.
|
||||||
|
- `script/fmt`: gofmt and prettier, writing files.
|
||||||
|
- `script/fmt-check`: the read-only twin of `script/fmt`.
|
||||||
|
- `script/check`: `script/test`, `script/lint`, then `script/fmt-check`. It
|
||||||
|
modifies nothing.
|
||||||
|
- `script/docker`: build the image tagged `x`, passing the version from
|
||||||
|
`git describe`.
|
||||||
|
- `script/cibuild`: what CI runs: `script/bootstrap`, `script/check`, then the
|
||||||
|
tagged image build.
|
||||||
|
- `script/precommit`: run by the pre-commit hook. It refuses a commit whose
|
||||||
|
`go.mod` or `go.sum` is not tidy, then runs `script/check`.
|
||||||
|
- `script/install-precommit`: install that hook; `make hooks` shims to it.
|
||||||
|
|
||||||
|
The Makefile adds four targets of its own: `make build`, `make install`,
|
||||||
|
`make tidy`, and `make clean`.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make setup # once: install the pinned prettier with yarn
|
make setup # once: bootstrap and install the pre-commit hook
|
||||||
make check # test + lint + fmt-check, exactly what CI gates on
|
make check # test, lint, fmt-check: what the hook and CI run
|
||||||
make fmt # gofmt and prettier, writes files
|
make fmt # gofmt and prettier, writes files
|
||||||
make hooks # install the pre-commit hook that runs make check
|
make build # build ./sutil on the host
|
||||||
make docker # the full CI build
|
make docker # build the image tagged x
|
||||||
```
|
```
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|||||||
+773
-269
File diff suppressed because it is too large
Load Diff
Executable
+104
@@ -0,0 +1,104 @@
|
|||||||
|
#!/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 tools, go, node and yarn come from
|
||||||
|
# nix, apt, brew, or apk (detected in that order), with no version and
|
||||||
|
# no hash, because the distribution is trusted. It installs node, yarn
|
||||||
|
# and the node packages only when yarn.lock is in the tree. Assumes
|
||||||
|
# nothing is present.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
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)
|
||||||
|
# Package lists may be empty (fresh images); refresh once per run.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
# Debian and Ubuntu name the yarn command yarnpkg.
|
||||||
|
run_yarn() {
|
||||||
|
if command -v yarn >/dev/null 2>&1; then
|
||||||
|
yarn "$@"
|
||||||
|
elif command -v yarnpkg >/dev/null 2>&1; then
|
||||||
|
yarnpkg "$@"
|
||||||
|
else
|
||||||
|
echo "no yarn; run script/bootstrap first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
if missing make; then pkg_install gnumake make make make; fi
|
||||||
|
if missing git; then pkg_install git git git git; fi
|
||||||
|
# Go on the host is for script/fmt and script/fmt-check (gofmt) and
|
||||||
|
# for make build; the gates compile inside the Dockerfile's phases.
|
||||||
|
if missing go; then pkg_install go golang-go go go; fi
|
||||||
|
|
||||||
|
# A Dockerfile stage that needs no yarn copies no yarn.lock.
|
||||||
|
if [ -f yarn.lock ]; then
|
||||||
|
if missing node; then pkg_install nodejs nodejs node nodejs; fi
|
||||||
|
if missing yarn && missing yarnpkg; then
|
||||||
|
pkg_install yarn yarnpkg yarn yarn
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Debian 13 and Ubuntu 26.04 package yarn 4, which refuses a yarn 1
|
||||||
|
# yarn.lock, or rewrites it in its own format.
|
||||||
|
yarn_version="$(run_yarn --version)"
|
||||||
|
if [ "${yarn_version%%.*}" != "1" ]; then
|
||||||
|
echo "bootstrap: yarn $yarn_version refuses this yarn 1 yarn.lock;" \
|
||||||
|
"use a distribution that packages yarn 1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_yarn install --frozen-lockfile
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "bootstrap complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/check: run all checks (test, lint, fmt-check). Our own
|
||||||
|
# extension to scripts-to-rule-them-all. test and lint are Docker
|
||||||
|
# phases; fmt-check is native, because a formatter writes the working
|
||||||
|
# tree. 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 "$@"
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/cibuild: run the CI build. It bootstraps first: a CI runner
|
||||||
|
# checks out and runs this and nothing else, and script/fmt-check runs
|
||||||
|
# the formatter on the host, which a pristine checkout cannot do.
|
||||||
|
# A new CACHEBUST value reruns every RUN step after the ARG CACHEBUST
|
||||||
|
# line, since a cached gate phase is a check that did not run; unlike
|
||||||
|
# --no-cache, it keeps the layers above that line and the cache mounts.
|
||||||
|
# This relies on every stage of the Dockerfile declaring ARG CACHEBUST
|
||||||
|
# before every step that lints, tests or compiles; a stage without that
|
||||||
|
# line is served from cache on an unchanged tree, with no warning.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
"$SCRIPT_DIR/bootstrap"
|
||||||
|
"$SCRIPT_DIR/check"
|
||||||
|
# The version and the tag each get their own line: a failing
|
||||||
|
# command substitution inside an argument does not trip `set -e`,
|
||||||
|
# so the inline form degrades silently to an empty constant. The
|
||||||
|
# VERSION build argument takes precedence over the version a build
|
||||||
|
# stage derives from the .git in the context.
|
||||||
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
|
[ -n "$version" ] || version="unknown"
|
||||||
|
tag="$("$SCRIPT_DIR/projectname")"
|
||||||
|
docker build --build-arg CACHEBUST="$(date +%s)-$$" \
|
||||||
|
--build-arg VERSION="$version" \
|
||||||
|
-t "$tag" .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/docker: build the Docker image tagged with the project name.
|
||||||
|
# Identical in all repos; the tag comes from script/projectname.
|
||||||
|
# A new CACHEBUST value reruns every RUN step after the ARG CACHEBUST
|
||||||
|
# line, since a cached gate phase is a check that did not run; unlike
|
||||||
|
# --no-cache, it keeps the layers above that line and the cache mounts.
|
||||||
|
# This relies on every stage of the Dockerfile declaring ARG CACHEBUST
|
||||||
|
# before every step that lints, tests or compiles; a stage without that
|
||||||
|
# line is served from cache on an unchanged tree, with no warning.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
# The version and the tag each get their own line: a failing
|
||||||
|
# command substitution inside an argument does not trip `set -e`,
|
||||||
|
# so the inline form degrades silently to an empty constant. The
|
||||||
|
# VERSION build argument takes precedence over the version a build
|
||||||
|
# stage derives from the .git in the context.
|
||||||
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
|
[ -n "$version" ] || version="unknown"
|
||||||
|
tag="$("$SCRIPT_DIR/projectname")"
|
||||||
|
docker build --build-arg CACHEBUST="$(date +%s)-$$" \
|
||||||
|
--build-arg VERSION="$version" \
|
||||||
|
-t "$tag" .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/fmt: format all files (writes): gofmt for Go, prettier for
|
||||||
|
# markdown.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
GO_DIRS="cmd internal pkg"
|
||||||
|
|
||||||
|
# Debian and Ubuntu name the yarn command yarnpkg.
|
||||||
|
run_yarn() {
|
||||||
|
if command -v yarn >/dev/null 2>&1; then
|
||||||
|
yarn "$@"
|
||||||
|
elif command -v yarnpkg >/dev/null 2>&1; then
|
||||||
|
yarnpkg "$@"
|
||||||
|
else
|
||||||
|
echo "no yarn; run script/bootstrap first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck disable=SC2086 # GO_DIRS is a word list on purpose
|
||||||
|
gofmt -w $GO_DIRS
|
||||||
|
run_yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/fmt-check: check formatting (read-only): gofmt for Go, prettier
|
||||||
|
# for markdown.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
GO_DIRS="cmd internal pkg"
|
||||||
|
|
||||||
|
# Debian and Ubuntu name the yarn command yarnpkg.
|
||||||
|
run_yarn() {
|
||||||
|
if command -v yarn >/dev/null 2>&1; then
|
||||||
|
yarn "$@"
|
||||||
|
elif command -v yarnpkg >/dev/null 2>&1; then
|
||||||
|
yarnpkg "$@"
|
||||||
|
else
|
||||||
|
echo "no yarn; run script/bootstrap first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck disable=SC2086 # GO_DIRS is a word list on purpose
|
||||||
|
unformatted="$(gofmt -l $GO_DIRS)"
|
||||||
|
if [ -n "$unformatted" ]; then
|
||||||
|
echo "gofmt: the following files need formatting:" >&2
|
||||||
|
echo "$unformatted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
run_yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/install-precommit: install the git pre-commit hook that runs
|
||||||
|
# script/precommit. Our own extension to scripts-to-rule-them-all.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
hook=".git/hooks/pre-commit"
|
||||||
|
printf '#!/bin/sh\nset -e\nscript/precommit\n' > .git/hooks/pre-commit
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
echo "pre-commit hook installed: runs script/precommit"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/lint: run the linter. Linting is a phase of the Dockerfile and
|
||||||
|
# this builds that phase alone; the linter is never installed or run on
|
||||||
|
# a developer host, where a shared result cache and a host-global lock
|
||||||
|
# make its answer untrustworthy.
|
||||||
|
#
|
||||||
|
# The phase is not the last stage in the file, so it is built only when
|
||||||
|
# --target names it. A new CACHEBUST value reruns every RUN step after
|
||||||
|
# the ARG CACHEBUST line, since a cached lint layer is a lint that did
|
||||||
|
# not run; unlike --no-cache, it keeps the layers above that line and
|
||||||
|
# the cache mounts. This relies on every stage of the Dockerfile
|
||||||
|
# declaring ARG CACHEBUST before every step that lints, tests or
|
||||||
|
# compiles; a stage without that line is served from cache on an
|
||||||
|
# unchanged tree, with no warning.
|
||||||
|
# --output type=cacheonly writes no image, since nothing uses one.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
docker build --build-arg CACHEBUST="$(date +%s)-$$" \
|
||||||
|
--target lint \
|
||||||
|
--output type=cacheonly .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/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. The Go
|
||||||
|
# extra: go.mod and go.sum must already be tidy, so a commit never
|
||||||
|
# carries a module file that go mod tidy would rewrite.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
go mod tidy
|
||||||
|
if ! git diff --quiet --exit-code -- go.mod go.sum; then
|
||||||
|
echo "precommit: go mod tidy changed go.mod or go.sum;" \
|
||||||
|
"stage them and retry" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"$SCRIPT_DIR/check"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+12
@@ -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 "x"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/setup: set up the repo for development after a fresh clone:
|
||||||
|
# installs dependencies and the git pre-commit hook.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
"$SCRIPT_DIR/bootstrap"
|
||||||
|
"$SCRIPT_DIR/install-precommit"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/test: run the test suite. Testing is a phase of the Dockerfile
|
||||||
|
# and this builds that phase alone, on the same terms as script/lint:
|
||||||
|
# --target because a phase that is not the last stage is built only when
|
||||||
|
# named, and a new CACHEBUST value because a cached test layer is a test
|
||||||
|
# that did not run; unlike --no-cache, it keeps the layers above the ARG
|
||||||
|
# CACHEBUST line and the cache mounts. This relies on every stage of the
|
||||||
|
# Dockerfile declaring ARG CACHEBUST before every step that lints, tests
|
||||||
|
# or compiles; a stage without that line is served from cache on an
|
||||||
|
# unchanged tree, with no warning. --output type=cacheonly writes no
|
||||||
|
# image, since nothing uses one.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
docker build --build-arg CACHEBUST="$(date +%s)-$$" \
|
||||||
|
--target test \
|
||||||
|
--output type=cacheonly .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user