diff --git a/.dockerignore b/.dockerignore index 66a74a5..5565f6c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,11 @@ # 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. +static/js/alpine.min.js *.md LICENSE .editorconfig diff --git a/.gitignore b/.gitignore index 16f68ad..50cd133 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,9 @@ tmp/ temp/ # CI cache barrier, written into the build context by the check workflow -.ci-fingerprint \ No newline at end of file +.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 diff --git a/Dockerfile b/Dockerfile index f43c43a..2c7be0e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ FROM golang:1.26.1-bookworm@sha256:4465644228bc2857a954b092167e12aa59c006a349228 # Depend on lint stage passing COPY --from=lint /src/go.sum /dev/null -RUN apt-get update && apt-get install -y --no-install-recommends make && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends make curl ca-certificates && rm -rf /var/lib/apt/lists/* WORKDIR /build @@ -44,6 +44,14 @@ 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 make test RUN make build diff --git a/Makefile b/Makefile index 81ee568..58935b2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: bootstrap setup test lint fmt fmt-check check build run dev deps docker clean hooks css +.PHONY: bootstrap setup assets test lint fmt fmt-check check build run dev deps docker clean hooks css # Default target .DEFAULT_GOAL := check @@ -9,6 +9,9 @@ bootstrap: setup: @script/setup +assets: + @script/fetch-assets + test: @script/test diff --git a/README.md b/README.md index 570f087..6c940f9 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,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 fmt # Format code (gofmt + goimports) make lint # Run golangci-lint make test # Run tests with race detection @@ -247,6 +248,8 @@ them. 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/test` — run the test suite - `script/lint` — run golangci-lint - `script/fmt` — format all code (writes) @@ -260,6 +263,27 @@ them. We provide: - `script/install-precommit` — install the git pre-commit hook that runs `script/precommit` +## 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. + +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. + +`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`. + ## Rationale Webhook integrations between services are inherently fragile. The diff --git a/internal/server/static_assets_test.go b/internal/server/static_assets_test.go new file mode 100644 index 0000000..c7d389f --- /dev/null +++ b/internal/server/static_assets_test.go @@ -0,0 +1,50 @@ +package server_test + +import ( + "net/http" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "sneak.berlin/go/webhooker/templates" +) + +// 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. +func TestBaseTemplateScriptsAreServed(t *testing.T) { + t.Parallel() + + // scriptSrc matches the src of every