Format the Markdown with prettier in make fmt and make fmt-check (closes #215) #492

Merged
clawbot merged 2 commits from issue-215-prettier-markdown into next 2026-10-03 06:32:28 +02:00
12 changed files with 103 additions and 53 deletions
Showing only changes of commit 9e2a51feaf - Show all commits
+2 -2
View File
@@ -15,8 +15,8 @@ bin/
# 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
# The js-deps stage installs ESLint; a host copy would overwrite it at the
# js-lint stage's `COPY . .`.
# The js-deps stage installs ESLint and prettier; a host copy would overwrite
# them at the `COPY . .` of the stages built on it.
node_modules/
.env
.env.*
+1 -1
View File
@@ -33,5 +33,5 @@ jobs:
# and built cannot report success from cache.
run: git rev-parse HEAD > .ci-fingerprint
- name: Build Docker image (runs make fmt-check, golangci-lint, the stylesheet check, ESLint, make test, make build)
- name: Build Docker image (runs the gofmt check, golangci-lint, the stylesheet check, ESLint, the Markdown check, make test, make build)
run: script/cibuild
+1 -1
View File
@@ -15,7 +15,7 @@ bin/
# Go vendor directory
vendor/
# ESLint and its dependencies, installed from yarn.lock
# ESLint, prettier and their dependencies, installed from yarn.lock
node_modules/
# IDE specific files
+4
View File
@@ -0,0 +1,4 @@
{
"tabWidth": 4,
"proseWrap": "always"
}
+34 -13
View File
@@ -4,8 +4,6 @@
# compile on Alpine musl (off64_t is a glibc type).
FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint
RUN apt-get update && apt-get install -y --no-install-recommends make && rm -rf /var/lib/apt/lists/*
WORKDIR /src
# Copy go mod files first for better layer caching
@@ -19,12 +17,14 @@ RUN go mod download
# .dockerignore.
COPY . .
# Run formatting check and linter. golangci-lint is invoked directly rather
# than through `make lint`: this stage is already the pinned linter image, and
# script/lint is a wrapper that builds Dockerfile.lint, so calling it here
# would need a docker daemon inside the build. Keep these steps in step with
# Dockerfile.lint, including --network=none (see its header for why).
RUN make fmt-check
# Run the Go formatting check and the linter. gofmt and golangci-lint are
# invoked directly rather than through `make fmt-check` and `make lint`: this
# stage is already the pinned linter image, and both scripts build docker
# stages, so calling them here would need a docker daemon inside the build.
# The Markdown half of `make fmt-check` is the markdown-check stage below.
# Keep the golangci-lint steps in step with Dockerfile.lint, including
# --network=none (see its header for why).
RUN if [ -n "$(gofmt -s -l .)" ]; then echo "gofmt needed on:"; gofmt -s -l .; exit 1; fi
RUN script/assets
RUN --network=none golangci-lint config verify --config .golangci.yml
RUN --network=none golangci-lint run --config .golangci.yml --build-tags browser ./...
@@ -66,10 +66,11 @@ RUN sed 's/}/}\n/g' static/css/tailwind.css > /tmp/committed.css \
}
# JavaScript lint stages: ESLint, at the version package.json and yarn.lock
# pin, checks static/js/ against eslint.config.mjs. js-deps installs it and
# stays cached until those two files change. script/lint forces only js-lint
# to re-run, and the build stage below runs it too. COPY . . brings in the CI
# cache barrier described in the lint stage above.
# pin, checks static/js/ against eslint.config.mjs. js-deps installs it, and
# prettier for the Markdown stages below, and stays cached until those two
# files change. script/lint forces only js-lint to re-run, and the build stage
# below runs it too. COPY . . brings in the CI cache barrier described in the
# lint stage above.
# node:24.21.0-alpine (LTS, with yarn 1.22.22), 2026-09-18
FROM node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1 AS js-deps
WORKDIR /src
@@ -80,16 +81,36 @@ FROM js-deps AS js-lint
COPY . .
RUN --network=none node_modules/.bin/eslint static/js
# Markdown stages: prettier, at the version package.json and yarn.lock pin,
# formats every Markdown file in the tree with the settings in .prettierrc.
# `make fmt` (script/fmt) writes the formatted files out from markdown-output.
# markdown-check fails on any file prettier would change; `make fmt-check`
# runs it, and so does the build stage below.
FROM js-deps AS markdown
COPY . .
RUN --network=none node_modules/.bin/prettier --write '**/*.md' \
&& mkdir /out \
&& find . -name '*.md' ! -path './node_modules/*' -exec cp -p --parents {} /out \;
FROM scratch AS markdown-output
COPY --from=markdown /out /
FROM js-deps AS markdown-check
COPY . .
RUN --network=none node_modules/.bin/prettier --check '**/*.md'
# Build stage
# golang:1.26.1-bookworm (Debian-based), 2026-03-17
# Using Debian-based image because gorm.io/driver/sqlite pulls in
# mattn/go-sqlite3 (CGO), which does not compile on Alpine musl.
FROM golang:1.26.1-bookworm@sha256:4465644228bc2857a954b092167e12aa59c006a3492282a6c820bf4755fd64a4 AS builder
# Depend on the lint, stylesheet check and JavaScript lint stages passing
# Depend on the lint, stylesheet check, JavaScript lint and Markdown check
# stages passing
COPY --from=lint /src/go.sum /dev/null
COPY --from=css-check /out/tailwind.css /dev/null
COPY --from=js-lint /src/yarn.lock /dev/null
COPY --from=markdown-check /src/yarn.lock /dev/null
# jq is a runtime dependency of script/ci-mark-superseded, which the test
# suite executes. git is what script/version derives the version with.
+40 -26
View File
@@ -19,16 +19,16 @@ before deploying one.
### Prerequisites
- Go 1.26.1+ (the version in `go.mod`)
- Docker (for `make lint` and `make css`, and so for `make check`, for the
browser test in `make test-browser`, for the CI gate, and for
containerized deployment)
- Docker (for `make lint`, `make fmt` and `make css`, and so for
`make check`, for the browser test in `make test-browser`, for the CI
gate, and for containerized deployment)
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
digest-pinned linter image via `Dockerfile.lint`. The same holds for
tailwindcss (see [Stylesheet](#stylesheet)). ESLint, node and yarn are
not prerequisites either, and `make lint` never uses a host copy of them
(see [Linting](#linting)).
tailwindcss (see [Stylesheet](#stylesheet)). ESLint, prettier, node and
yarn are not prerequisites either, and `make lint` and `make fmt` never
use a host copy of them (see [Linting](#linting)).
### Quick Start
@@ -58,8 +58,8 @@ make docker
make bootstrap # Install all dependencies (idempotent)
make setup # Bootstrap + install git pre-commit hook
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 fmt # Format Go (gofmt + goimports) and Markdown (prettier, in Docker)
make fmt-check # Fail if gofmt or prettier would change anything (writes nothing)
make lint # Run golangci-lint and ESLint in Docker
make test # Run tests with race detection
make test-browser # Run the browser test in Docker (Dockerfile.browser)
@@ -1325,7 +1325,8 @@ We provide:
[Third-party browser assets](#third-party-browser-assets))
- `script/lint` — run golangci-lint and ESLint in Docker (see Linting
below)
- `script/fmt` — format all code (writes)
- `script/fmt` — format the Go code and, in Docker, the Markdown
(writes)
- `script/fmt-check` — check formatting (read-only)
- `script/css` — regenerate `static/css/tailwind.css` in Docker (writes;
see [Stylesheet](#stylesheet))
@@ -3267,13 +3268,14 @@ webhooker/
│ └── js/alpine.min.js # Alpine.js CSP build, extracted from 3p/ by make assets, not committed
├── templates/ # Go HTML templates (base, login, sources, etc.)
├── script/ # Scripts to Rule Them All entrypoints
├── Dockerfile # Stages: lint, stylesheet, JavaScript lint, test+build, Alpine runtime
├── Dockerfile # Stages: lint, stylesheet, JavaScript lint, Markdown, test+build, Alpine runtime
├── Dockerfile.lint # Lint-only image built by script/lint
├── Dockerfile.browser # Browser test image built by script/test-browser
├── Makefile # 13 of 19 targets shim script/; 6 are inline
├── go.mod / go.sum
├── package.json / yarn.lock # ESLint, pinned, for the JavaScript lint stage
├── package.json / yarn.lock # ESLint and prettier, pinned, for the JavaScript lint and Markdown stages
├── eslint.config.mjs # ESLint configuration for static/js/
├── .prettierrc # prettier settings for the Markdown
└── .golangci.yml # golangci-lint configuration
```
@@ -3598,6 +3600,14 @@ summary line to look for; it names the stage once for both `--target`
and `--no-cache-filter`, and `--target` fails on a name that matches no
stage.
prettier formats the Markdown, and it never runs on the host either. It
is pinned in `package.json` and `yarn.lock` beside ESLint, installed by
the same `js-deps` stage, and reads its settings from `.prettierrc`.
`make fmt` builds the `markdown-output` stage and writes the formatted
files back into the tree; `make fmt-check` and the image build run the
`markdown-check` stage, which fails on any Markdown file prettier would
change.
### Docker
The Dockerfile uses a multi-stage build. Each stage is pinned by
@@ -3605,8 +3615,8 @@ digest, and the lint and builder stages are separate images so the
linter's version is fixed independently of the compiler's:
1. **Lint stage** (`golangci/golangci-lint:v2.12.2`, Debian-based) —
installs `make`, downloads dependencies, copies the source, and runs
`make fmt-check`, then `script/assets` to extract Alpine.js from
downloads dependencies, copies the source, and runs the `gofmt`
check, then `script/assets` to extract Alpine.js from
`3p/`, then `golangci-lint config verify` and `golangci-lint run`,
both with `--network=none`.
2. **Stylesheet stages** (`debian:bookworm-slim`, with the Tailwind
@@ -3617,11 +3627,14 @@ linter's version is fixed independently of the compiler's:
one, and `make css` writes the generated file out from `css-output`
(see [Stylesheet](#stylesheet)).
3. **JavaScript lint stages** (`node:24.21.0-alpine`, with yarn) —
`js-deps` installs ESLint from `yarn.lock` and `js-lint` runs it over
`static/js/` (see [Linting](#linting)).
4. **Builder stage** (`golang:1.26.1-bookworm`) — depends on the lint,
`css-check` and `js-lint` stages passing (it copies a file from
each), runs
`js-deps` installs ESLint and prettier from `yarn.lock` and `js-lint`
runs ESLint over `static/js/` (see [Linting](#linting)).
4. **Markdown stages** (on `js-deps`) — `markdown-check` runs prettier
over the Markdown and fails on any file it would change, and
`make fmt` writes the formatted files out from `markdown-output`.
5. **Builder stage** (`golang:1.26.1-bookworm`) — depends on the lint,
`css-check`, `js-lint` and `markdown-check` stages passing (it copies
a file from each), 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
@@ -3631,7 +3644,7 @@ linter's version is fixed independently of the compiler's:
given, otherwise derived from the `.git` in the context, and the
stage fails if a context with `.git` would stamp `unknown` (see
[Version stamping](#version-stamping)).
5. **Runtime stage** (`alpine:3.21`) — copies the static binary and
6. **Runtime stage** (`alpine:3.21`) — copies the static binary and
`deploy/docker-entrypoint.sh`, creates the `/var/lib/webhooker`
directory for all SQLite databases, exposes port 8080, and includes
a health check against `/.well-known/healthcheck`. It sets no
@@ -3639,9 +3652,10 @@ linter's version is fixed independently of the compiler's:
directory's owner and mode, and runs the app as the non-root
`webhooker` user (UID 1000) through `su-exec`.
The lint stage invokes `golangci-lint` directly rather than `make lint`:
it is already the pinned linter image, and `make lint` builds
`Dockerfile.lint`, which would need a docker daemon inside this build.
The lint stage invokes `gofmt` and `golangci-lint` directly rather than
`make fmt-check` and `make lint`: it is already the pinned linter image,
and both targets build docker stages, which would need a docker daemon
inside this build.
The lint and builder stages use Debian rather than Alpine because
`gorm.io/driver/sqlite` pulls in `mattn/go-sqlite3`, which needs CGO
@@ -3654,8 +3668,8 @@ linted, tested and compiled, with a current stylesheet. `script/lint`
also uses Docker (`Dockerfile.lint` and the `js-lint` stage, see Linting
above), so `make lint` and `make check` run the same pinned linter
versions the gate does; of
the steps `make check` runs, only `script/test` and `script/fmt-check`
run on the host.
the steps `make check` runs, only `script/test` and the `gofmt` check in
`script/fmt-check` run on the host.
#### CI gate honesty
@@ -3666,8 +3680,8 @@ check meaningless. The `check` workflow therefore writes
the hash of the commit being checked, so every commit, docs-only ones
and a squash merge whose tree matches an already-built branch included,
gets a new fingerprint, invalidates the `COPY . .` layer of every check
stage, and really runs `make fmt-check`, `golangci-lint`, the stylesheet
check, ESLint, `make test`, and `make build`. A run that reports success
stage, and really runs the `gofmt` check, `golangci-lint`, the stylesheet
check, ESLint, the Markdown check, `make test`, and `make build`. A run that reports success
ran them.
The module download layer sits above `COPY . .` and stays cached.
+2 -1
View File
@@ -1,6 +1,7 @@
{
"private": true,
"devDependencies": {
"eslint": "10.11.0"
"eslint": "10.11.0",
"prettier": "3.9.9"
}
}
+5 -4
View File
@@ -3,8 +3,8 @@
# 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 (not git,
# make, or go). golangci-lint, node and ESLint are deliberately not
# installed: linting runs only in docker, via script/lint.
# make, or go). golangci-lint, node, ESLint and prettier are deliberately
# not installed: they run only in docker, via script/lint and script/fmt.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
@@ -60,9 +60,10 @@ main() {
if missing go; then pkg_install go golang go go; fi
# Not installed here: docker is platform-specific and out of scope for a
# package-manager bootstrap, but script/lint and script/css need it.
# package-manager bootstrap, but script/lint, script/fmt and script/css
# need it.
if missing docker; then
echo "bootstrap: docker not found; script/lint and script/css require it" >&2
echo "bootstrap: docker not found; script/lint, script/fmt and script/css require it" >&2
fi
go mod download
+4 -4
View File
@@ -1,8 +1,8 @@
#!/bin/sh
# script/cibuild: run the CI build. The Dockerfile runs the checks
# (make fmt-check, lint, test), so a successful build implies a green
# repo. Generic: needs no adaptation. The Gitea workflow runs this on
# push.
# script/cibuild: run the CI build. The Dockerfile runs the checks (the
# gofmt check, golangci-lint, the stylesheet check, ESLint, the Markdown
# check, make test), so a successful build implies a green repo. Generic:
# needs no adaptation. The Gitea workflow runs this on push.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
+4 -1
View File
@@ -1,5 +1,7 @@
#!/bin/sh
# script/fmt: format all files (writes).
# script/fmt: format all files (writes): the Go code with gofmt and
# goimports, the Markdown with prettier. prettier is never installed
# locally: it runs in docker, in the Dockerfile's Markdown stages.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
@@ -10,6 +12,7 @@ main() {
if command -v goimports >/dev/null 2>&1; then
goimports -w .
fi
docker build --target markdown-output --output type=local,dest=. .
}
main "$@"
+1
View File
@@ -12,6 +12,7 @@ main() {
gofmt -s -l .
exit 1
fi
docker build --target markdown-check --output type=cacheonly .
}
main "$@"
+5
View File
@@ -470,6 +470,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier@3.9.9:
version "3.9.9"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.9.tgz#09b826918c91cd4cbc80e0cbd1d2a922ff04f233"
integrity sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==
punycode@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"