diff --git a/.dockerignore b/.dockerignore index 5565f6c..550060e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,10 +3,8 @@ # stage of the Dockerfile. .git/ bin/ -# Third-party browser assets are fetched and hash-verified inside the build by -# script/fetch-assets. Excluding any host copy keeps a developer's working tree -# from supplying the bytes that get shipped. The script and its -# static/vendor.sha256 manifest stay in the context. +# Extracted from 3p/ by `make assets` inside the build; a host copy is not +# needed. The tarball in 3p/ must stay in the context. static/js/alpine.min.js *.md LICENSE diff --git a/.gitignore b/.gitignore index 50cd133..6da4ee4 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,6 @@ temp/ # CI cache barrier, written into the build context by the check workflow .ci-fingerprint -# Third-party browser assets, fetched and hash-verified by -# script/fetch-assets against static/vendor.sha256. Not committed: -# REPO_POLICIES.md forbids minified bundles in version control. -/static/js/alpine.min.js \ No newline at end of file +# Alpine.js, extracted by `make assets` from its tarball in 3p/, which is +# what is committed. +/static/js/alpine.min.js diff --git a/3p/alpinejs-3.14.9.tgz b/3p/alpinejs-3.14.9.tgz new file mode 100644 index 0000000..4d31381 Binary files /dev/null and b/3p/alpinejs-3.14.9.tgz differ diff --git a/Dockerfile b/Dockerfile index bb47620..385a27b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,15 +51,8 @@ RUN go mod download # the lint stage above. COPY . . -# Fetch the third-party browser assets the UI serves. They are not committed -# (REPO_POLICIES.md forbids minified bundles in version control) and -# .dockerignore keeps any host copy out of the build context, so this step is -# the only way they enter the image. Each download is checked against a -# hardcoded sha256 and the build fails on mismatch; make test re-checks the -# hashes against the bytes go:embed actually put in the binary. -RUN script/fetch-assets - -# Run tests and build +# Run tests and build. Both first run script/assets, which extracts Alpine.js +# from its tarball in 3p/. RUN make test # Version stamped into the binary. .dockerignore excludes .git/, so @@ -67,8 +60,8 @@ RUN make test # host and passes it in. The default is what a bare `docker build .` # with no --build-arg gets, and it names no tag the tree may not be at. # -# Declared here, below the test and asset steps, so a changed version -# does not invalidate their cached layers. +# Declared here, below the test step, so a changed version does not +# invalidate its cached layer. ARG VERSION=unknown RUN make build VERSION="$VERSION" diff --git a/Makefile b/Makefile index ceed419..b5438f8 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ setup: @script/setup assets: - @script/fetch-assets + @script/assets test: @script/test @@ -45,13 +45,13 @@ fmt-check: check: @script/check -build: +build: assets go build -ldflags '$(strip -X main.version=$(VERSION) $(GO_LDFLAGS))' -o bin/webhooker ./cmd/webhooker run: build ./bin/webhooker -dev: +dev: assets go run ./cmd/webhooker deps: diff --git a/README.md b/README.md index 0536a4e..c11b46c 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,6 @@ before deploying one. - Go 1.26.1+ (the version in `go.mod`) - Docker (for linting, for the test stage of the CI gate, and for containerized deployment) -- `curl`, used by `script/fetch-assets` to download the third-party - browser assets, which are not committed (`make bootstrap` installs - it if missing) golangci-lint is not a prerequisite and must not be installed on the host: `script/bootstrap` does not install it, and `make lint` runs the @@ -36,9 +33,7 @@ digest-pinned linter image via `Dockerfile.lint`. git clone https://git.eeqj.de/sneak/webhooker.git cd webhooker -# Install Go dependencies and the third-party browser assets. -# `make deps` alone is not enough: it only runs go mod download/tidy, -# and the checks below need the fetched assets. +# Install the Go toolchain if missing, and the Go dependencies make bootstrap # Run all checks (test, lint, format check) @@ -58,7 +53,7 @@ make docker ```bash make bootstrap # Install all dependencies (idempotent) make setup # Bootstrap + install git pre-commit hook -make assets # Fetch + verify third-party browser assets +make assets # Extract Alpine.js from 3p/ (test, check, build, dev run it) make fmt # Format code (gofmt + goimports) make fmt-check # Fail if gofmt would change anything (writes nothing) make lint # Run golangci-lint in Docker (Dockerfile.lint) @@ -1254,14 +1249,14 @@ This repository adheres to the standard: normalized scripts in `script/` are the entrypoints for the development workflow. Ten of the Makefile's seventeen targets are thin shims that call them; `build`, `run`, `dev`, `deps`, `clean`, `css` and -`version` are inline commands with no script behind them, though -`build` and `version` both take their value from `script/version`. +`version` are inline commands with no script behind them, though `build` +and `version` both take their value from `script/version`. -`make check` needs the third-party browser assets in `static/`, which -are not committed, so run `make bootstrap` (or just `make assets`) once -after cloning. Without them the tests fail with a message naming that -remedy. `make check` does not fetch them itself because it must not -change any files in the repo. +`script/test`, `make build` and `make dev` each run `script/assets` +first, which writes the ignored `static/js/alpine.min.js` (see +[Third-party browser assets](#third-party-browser-assets)), so +`make test`, `make check` and the pre-commit hook work on a fresh clone +without a separate step. We provide: @@ -1269,8 +1264,8 @@ We provide: - `script/setup` — make a fresh clone ready for development (bootstrap, then install-precommit) - `script/projectname` — output the project name ("webhooker") -- `script/fetch-assets` — download the third-party browser assets into - `static/`, verifying each against its pinned sha256 +- `script/assets` — extract Alpine.js from its tarball in `3p/` (see + [Third-party browser assets](#third-party-browser-assets)) - `script/test` — run the test suite - `script/lint` — run golangci-lint in Docker (see Linting below) - `script/fmt` — format all code (writes) @@ -1292,24 +1287,25 @@ We provide: ## Third-party browser assets -The web UI serves one third-party script, Alpine.js. It is **not** committed: -a minified bundle in the tree is unreviewable, and `REPO_POLICIES.md` bars -both committed build artifacts and unpinned external references. +The web UI serves one third-party script, Alpine.js. Its npm package tarball +is committed as `3p/alpinejs-3.14.9.tgz`, byte for byte as the npm registry +publishes it. It is a dependency, not this repo's build output, so +`REPO_POLICIES.md`'s rule against committed build artifacts does not apply. +The directory is `3p/` rather than `vendor/` because Go treats a root +`vendor/` directory as its module vendor directory. -Instead `script/fetch-assets` downloads it from a pinned URL, checks the -download against a hardcoded sha256, and installs it under `static/`. The -sha256 of every installed asset is recorded in `static/vendor.sha256`, and -`static/vendor_test.go` re-hashes the bytes `go:embed` put in the binary -against that manifest — so the pin is enforced on what actually ships, not -merely written down. Any mismatch fails the build. +`script/assets` (`make assets`) extracts the browser build, +`package/dist/cdn.min.js`, from the tarball to `static/js/alpine.min.js`, +where `go:embed` picks it up. `script/test`, `make build` and `make dev` run +it first, and the Dockerfile builds through `make test` and `make build`, so +nothing downloads Alpine.js. The extracted file is not committed, and +`.dockerignore` keeps any host copy out of the build context. -`make bootstrap` runs the fetch for local development, and the Dockerfile -runs it in the build stage; `.gitignore` and `.dockerignore` keep the -artifact out of both the repo and the build context. - -To move to a new version: update the version, URL, and tarball sha256 in -`script/fetch-assets` and the asset sha256 in `static/vendor.sha256`, then -run `make assets && make check`. +To move to a new version: download +`https://registry.npmjs.org/alpinejs/-/alpinejs-.tgz`, check it +against the `dist.integrity` hash listed at +`https://registry.npmjs.org/alpinejs/`, replace the tarball in `3p/` +with it, update its file name in `script/assets`, and run `make check`. ## Rationale @@ -2788,6 +2784,8 @@ imports. The entry point is `cmd/webhooker/main.go`. ``` webhooker/ +├── 3p/ +│ └── alpinejs-3.14.9.tgz # Alpine.js npm package, extracted by make assets ├── cmd/webhooker/ │ └── main.go # Entry point: subcommand dispatch; no args locks DATA_DIR and wires fx ├── internal/ @@ -2881,8 +2879,7 @@ webhooker/ │ ├── css/tailwind.css # Generated stylesheet the pages load │ ├── css/style.css # Older hand-written stylesheet, no longer loaded │ ├── js/app.js # Progressive-enhancement copy-to-clipboard -│ ├── js/alpine.min.js # Alpine.js, fetched by script/fetch-assets, not committed -│ └── vendor.sha256 # Pinned hashes the fetched assets are verified against +│ └── js/alpine.min.js # Alpine.js, extracted from 3p/ by make assets, not committed ├── templates/ # Go HTML templates (base, login, sources, etc.) ├── script/ # Scripts to Rule Them All entrypoints ├── Dockerfile # Three stages: lint, test+build, Alpine runtime @@ -3190,14 +3187,14 @@ version is fixed independently of the compiler's: `make fmt-check`, then `golangci-lint config verify` and `golangci-lint run`, both with `--network=none`. 2. **Builder stage** (`golang:1.26.1-bookworm`) — depends on the lint - stage passing (it copies a file from it), runs `script/fetch-assets` - to download and verify the third-party browser assets, then runs - `make test` and `make build`, and finally rebuilds the binary with - `CGO_ENABLED=1` and static linking so it runs on musl. Both builds - go through `make build`, the relink adding its `-extldflags` via - `GO_LDFLAGS`, so neither can drop the `-X` that stamps the version. - The version arrives as the `VERSION` build arg, since the context - has no `.git` (see [Version stamping](#version-stamping)). + stage passing (it copies a file from it), runs `make test` and + `make build` (both extract Alpine.js from `3p/` first), and finally + rebuilds the binary with `CGO_ENABLED=1` and static linking so it + runs on musl. Both builds go through `make build`, the relink adding + its `-extldflags` via `GO_LDFLAGS`, so neither can drop the `-X` that + stamps the version. The version arrives as the `VERSION` build arg, + since the context has no `.git` (see + [Version stamping](#version-stamping)). 3. **Runtime stage** (`alpine:3.21`) — copies the static binary, creates the `/var/lib/webhooker` directory for all SQLite databases, runs as the non-root `webhooker` user (UID 1000), exposes port 8080, diff --git a/internal/server/static_assets_test.go b/internal/server/static_assets_test.go index c7d389f..884977b 100644 --- a/internal/server/static_assets_test.go +++ b/internal/server/static_assets_test.go @@ -13,9 +13,9 @@ import ( // TestBaseTemplateScriptsAreServed walks every /s/ script the base // template loads on each page and fetches it through the real router. -// Alpine.js is fetched at build time rather than committed, so nothing -// in the repo guarantees it is present: this is the check that the page -// still gets the JavaScript it asks for. +// Alpine.js is extracted from its tarball in 3p/ at build time, so the +// file is not in the tree: this is the check that the page still gets +// the JavaScript it asks for. func TestBaseTemplateScriptsAreServed(t *testing.T) { t.Parallel() diff --git a/script/assets b/script/assets new file mode 100755 index 0000000..69db60d --- /dev/null +++ b/script/assets @@ -0,0 +1,16 @@ +#!/bin/sh +# script/assets: extract Alpine.js from its npm package tarball, committed +# in 3p/, to static/js/alpine.min.js, where go:embed reads it. The +# extracted file is not committed. script/test, make build and make dev run +# this first. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" + +main() { + cd "$ROOT" + tar -xzOf 3p/alpinejs-3.14.9.tgz package/dist/cdn.min.js \ + >static/js/alpine.min.js +} + +main "$@" diff --git a/script/bootstrap b/script/bootstrap index 49001f8..1f014a1 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -4,9 +4,7 @@ # installed tools are skipped. Base tooling comes from nix, apt, brew, # or apk (detected in that order); assumes NOTHING is present (not git, # make, or go). golangci-lint is deliberately not installed: linting runs -# only in docker, via script/lint and Dockerfile.lint. Finishes by running -# script/fetch-assets, which installs the hash-pinned third-party browser -# assets the repo does not commit. +# only in docker, via script/lint and Dockerfile.lint. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" @@ -69,11 +67,6 @@ main() { go mod download - # Third-party browser assets are not committed; fetch and verify them - # so a fresh clone can build and test. - if missing curl; then pkg_install curl curl curl curl; fi - "$ROOT/script/fetch-assets" - echo "bootstrap complete" } diff --git a/script/fetch-assets b/script/fetch-assets deleted file mode 100755 index 5148b4b..0000000 --- a/script/fetch-assets +++ /dev/null @@ -1,104 +0,0 @@ -#!/bin/sh -# script/fetch-assets: download the third-party browser assets the web UI -# ships and install them under static/. Minified bundles are not committed -# (REPO_POLICIES.md: no build artifacts in version control), so the build -# fetches them here. Every download is verified against a hardcoded sha256 -# before it is installed, and any mismatch aborts. Idempotent: an asset -# already present with its pinned hash is left alone. -set -eu - -ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" - -# The sha256 of each installed asset lives in static/vendor.sha256, in -# sha256sum(1) format, with paths relative to static/. That file is the -# single source of truth: this script verifies against it, and -# static/vendor_test.go asserts the bytes embedded into the binary match -# it, so the hash cannot rot into a value nothing checks. -MANIFEST="static/vendor.sha256" - -# Alpine.js 3.14.9, 2026-08-17. Fetched from registry.npmjs.org, the -# publisher of record; the jsDelivr and unpkg copies are mirrors of this -# same tarball. dist/cdn.min.js is the browser build Alpine publishes for -# a