5 Commits
Author SHA1 Message Date
clawbot fd3cd4c18c Add Makefile shims for cibuild and precommit (closes #34)
check / check (push) Successful in 16s
script/cibuild and script/precommit both existed and were already the
documented CI and pre-commit entrypoints, but neither had a Makefile
target, so the standing rule to drive the repo through make targets
rather than the underlying tool could not be followed for either.

It matters most for the build. A bare `docker build .` fails closed on
the CHECK_EPOCH guard by design, so script/cibuild is one of only three
supported ways to build an image here, and it was the only one of the
three without a target while `make docker` had one.

The two targets are thin shims in the same style as every other target
and change nothing about what the scripts do. .PHONY was already
complete for the targets that existed and now lists both new ones.

README.md's Entrypoints section gains the script-to-target mapping so
the two documents agree, including the two names that do not match:
script/install-precommit is `make hooks`, and script/precommit is
`make precommit`. It also notes that `make cibuild` is the slowest
target, because it is the only one that runs two container builds --
while still taking seconds once the shared script/bootstrap layer is
cached, since Dockerfile.lint's first four instructions are
byte-identical to the main Dockerfile's and the pinned-Hugo compile is
therefore paid once per machine rather than twice.

Two accuracy fixes to text the same section already carried. The
script/install-precommit bullet said the installed hook runs
script/check; the script writes script/precommit into
.git/hooks/pre-commit, and its own header comment says so. And the
Makefile is described as listing the operations you are expected to run
rather than as the authoritative list of everything the repo can do,
which is not literally true: script/projectname is an internal helper
that script/docker calls to compute a tag, and it has no target
deliberately -- a target for it would be noise in the `make<tab>`
listing this change exists to make useful.

TODO.md also loses the stale Future Step asking someone to confirm the
static/_headers file took effect in production. That was confirmed live
on both hostnames on 2026-08-10 and recorded at
#14 , so the item is work
already done. The Status paragraph's matching "unverified in production
until the next deploy" clause is corrected for the same reason: a commit
that edits TODO.md should not leave a known-false statement in it.
2026-08-10 13:57:34 +00:00
clawbot 25b6c0a9de Run the lint inside Docker via Dockerfile.lint (closes #38)
check / check (push) Successful in 1m23s
Add a root Dockerfile.lint that runs `hugo --minify --printPathWarnings`
as a build step, so a successful build IS a clean lint, and reduce
script/lint to building that file. There is no host lint path and
deliberately no "am I already inside a container?" branch, which would
be a host lint path in disguise.

The containerisation boundary is lint only, per the owner ruling on the
issue: formatting is not a lint, so script/fmt and script/fmt-check stay
on the host, unchanged in version, scope and flags. That also removes
the forced duplication of prettier's settings between a script and a
Dockerfile, and with it the keep-in-sync notes that duplication needed.

Dockerfile.lint has exactly one stage on purpose. A whole-file
`docker build -f Dockerfile.lint .` builds only the file's last stage,
and sibling stages off a shared base carry no ordering edge, so a second
stage beside the lint would be silently skipped by exactly the
invocation the canonical org-wide script/lint uses -- a green that
linted nothing, which the per-stage CHECK_EPOCH guard cannot catch
because the stage that did run satisfies it. With one stage there is
nothing to skip and script/lint needs no --target. A comment in the file
says that any second check added here must be chained or carry an
explicit ordering edge, never left as a sibling.

Its first four instructions are byte-identical to the main Dockerfile's
and in the same order, so the expensive `RUN script/bootstrap` layer
that compiles the pinned Hugo from source is shared between the two
images rather than paid twice.

Resolve the recursion by direction, not detection. `make check` calls
script/lint, and script/lint is now a `docker build`, so `RUN make
check` in an image would attempt a docker build inside a build step
where there is no daemon. The main Dockerfile therefore runs the
individual non-lint checks -- script/test and script/fmt-check, as
separate RUN lines under the CHECK_EPOCH guard -- matching the canonical
shape, and only the lint is absent from it. script/cibuild runs
script/lint first, for fail-fast feedback: on a runner with no cached
bootstrap layer a lint failure should not wait behind a Hugo build from
source. CI coverage is therefore unchanged, and it runs the same scripts
a developer runs.

Caching is waived for the lint in the shape this repo already settled:
ARG CHECK_EPOCH with no default, guarded with
`[ -n "$CHECK_EPOCH" ] || exit 1`, and the value expanded into the
linted command as well as the guard, so invalidation never rests on
BuildKit's treatment of an unreferenced ARG. Every image-building
entrypoint generates and passes it -- script/cibuild, script/docker,
script/lint -- each as a whole assignment rather than inline, for the
`set -e` reason script/cibuild documents.

script/lint builds with `--output type=cacheonly`: the build is run for
its exit status, not for an image, and because the lint layer is
cache-busted on every invocation an exporting build leaves one dangling
image per lint run. On a host shared with other work that accumulates.
The build cache is unaffected, so script/bootstrap still hits, and
failures still propagate.

Two divergences from REPO_POLICIES.md, stated rather than buried:

  - REPO_POLICIES.md:92, "all Dockerfiles must run `make check`". That
    rule and "every lint run happens in Docker" cannot both hold once
    `make check` contains the lint.
  - REPO_POLICIES.md:102-168, which requires a separate lint stage whose
    result the build stage depends on through
    `COPY --from=lint /src/go.sum /dev/null`, on the stated grounds that
    without the edge "the build stage would not wait for lint to finish
    and a lint failure might not fail the overall build". No such edge
    exists here: the lint is its own file and its own build, sequenced
    by script/cibuild rather than by BuildKit. Both sections are
    superseded upstream by 12e8db8 in sneak/prompts, which deletes the
    Go multistage lint stage and its ordering trick for the same reason
    -- that stage ran `make lint`, which is now a docker build.

Verified: two consecutive script/lint runs on an unchanged tree both
executed hugo for real, distinct epochs echoed, script/bootstrap CACHED,
second run 0.85s; a whole-file `docker build -f Dockerfile.lint .` with
the argument and no --target ran the lint for real; a bare build with no
argument failed closed on the guard; a planted template error failed the
lint with hugo's own render error and made script/cibuild exit non-zero
in 0.6s with the main image build never starting; a planted over-long
line failed the host script/fmt-check; both reverted and re-run clean;
`make check`, script/docker and script/cibuild all green with every
check layer observed executing rather than served from cache, and the
bootstrap layer CACHED in both images. The deploy path is byte-identical
to main: .gitea/, script/bootstrap, script/test and .dockerignore are
untouched.
2026-08-10 13:20:40 +00:00
clawbot 407b0a0d79 Add the MIT LICENSE and state it in the README (closes #10)
check / check (push) Successful in 11s
The repo had no LICENSE, which REPO_POLICIES.md lists as a mandatory
minimum file, and the README's License section said "Content is provided
as-is for community use." That granted nothing explicitly and matched no
committed file.

The repo is public, verified on the Gitea API rather than assumed, so
the standing policy applies: MIT on any public repo lacking a license.
LICENSE is byte-identical to the canonical sneak/homoicon copy (same git
blob, 3274443) and its body is word-for-word the SPDX MIT text, with
only the line wrapping differing.

The README License section now reads "MIT. See LICENSE.", and says
explicitly that the licence covers content/ as well as the code: this
repo carries both a Hugo site and its community content, while MIT's own
text speaks only of "the Software". The Description first line gains the
licence, which the README requirements call for and which was the one
field it was missing.

Nothing published contradicts the choice. The built public/ tree carries
no copyright, all-rights-reserved or terms-of-use string in index.html,
css/style.css, index.xml or sitemap.xml; the rendered footer names
@sneak and links the repo but reserves no rights, and the RSS carries no
copyright element. The content is factual mesh channel data asserting no
licence of its own.

LICENSE needed no .prettierignore entry, measured rather than assumed:
script/fmt passes prettier the explicit globs '**/*.md' and '**/*.css',
and an extensionless root file matches neither. A script/fmt run leaves
the file's hash unchanged, and a counterfactual LICENSE.md copy was
reflowed by that same run, which is the direct evidence that the
extension is what excludes it and not an ignore rule.

Per-file licence headers and SPDX identifiers are deliberately omitted;
no org standard mandates them.

Nothing on the deploy path is touched.
2026-08-10 12:31:30 +00:00
clawbot 5f998c6e70 Add a Cloudflare Pages _headers file with security headers (closes #14)
check / check (push) Successful in 9s
Hugo copies static/ verbatim into public/, so static/_headers lands at
the deploy output root, which is where Pages reads it from. This is the
first root-level static/ in the repo; Hugo unions it with the theme's
static/ per path rather than shadowing it, and the built tree confirms
that: public/css/style.css and public/index.html are byte-identical to
the previous build and the static file count goes from 1 to 2.

The live "before" was measured rather than assumed. Cloudflare already
sends X-Content-Type-Options and Referrer-Policy by default, so those
two lines are restatements; the substance is Strict-Transport-Security,
Content-Security-Policy, X-Frame-Options and Permissions-Policy, none of
which the site sends today.

Every value is checked against the built page, which loads nothing: no
script, img, link, iframe, form or media element, no style= and no on*=
attribute. It has exactly one inline <style> block, filled by readFile
in baseof.html. So default-src 'none' with style-src 'unsafe-inline' is
both achievable and tight, and 'unsafe-inline' is required by, and only
by, that deliberate inlining. There is no script-src allowance because
there are no scripts. X-Frame-Options: DENY and frame-ancestors 'none'
agree.

HSTS carries neither preload nor includeSubDomains. www.lora.vegas is
the only other name in DNS and it is served by this same Pages project,
so this file sets HSTS on its responses directly; includeSubDomains
would instead bind every future subdomain for a year, with no way to
walk it back inside the max-age window without also dropping the apex
protection.

Verified in a headless Chrome against a local server that parses the
committed _headers and applies it as real response headers: zero CSP
violations, the inlined stylesheet parses to 17 rules with the computed
body padding, tagline colour and link colour all coming from the theme
CSS, framing from another origin refused by frame-ancestors, and all
five named outbound links still navigating with status 200.

Whether Pages actually parses the file cannot be verified from here.
Pages silently ignores a malformed _headers, so the green build proves
nothing about it; that check belongs after the next deploy and must be
made on Strict-Transport-Security or Content-Security-Policy, since
X-Content-Type-Options would pass either way. It has to be run against
both lora.vegas and www.lora.vegas: dropping includeSubDomains rests on
www being served by this same Pages project, which was established from
identical response bodies rather than from the Cloudflare dashboard.
2026-08-09 17:01:59 +00:00
clawbot bcb90e74b4 Run script/lint from script/check (closes #9)
check / check (push) Successful in 14s
script/check ran only fmt-check then test, so script/lint was never
invoked anywhere in the gate: make check shims to script/check, the
Dockerfile runs make check, script/cibuild builds the Dockerfile, and
the pre-commit hook calls script/check. The script was dead code that
the README advertised as part of the gate.

It now runs test, lint, fmt-check in the canonical order. script/lint
is hugo --minify --printPathWarnings, which reports render-target
collisions that the plain hugo --minify in script/test does not; that
signal was being discarded.

The gate still modifies no tracked files. script/test and script/lint
both write to public/, which is gitignored and was already written by
script/test before this change.

Corrects the two documents that enumerated the old two-step gate: the
README Entrypoints line for script/check, and the Dockerfile header
comment above the RUN make check that executes it.
2026-08-09 10:09:13 +00:00