Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Release workflow for Aozora Flavored Markdown (afm).
#
# Trigger on: annotated tags matching `v*`, plus manual `workflow_dispatch`
# for dry runs. Builds the `afm` CLI on five target triples and uploads
# for dry runs. Builds the `afm` CLI on three target triples and uploads
# the archives + SHA256SUMS to the GitHub Release.
#
# ADR-0002 scope exception: release builds run on native GitHub Actions
Expand Down
10 changes: 6 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ yarn-error.log*
playwright-report/
test-results/

# Fuzz
fuzz/target/
fuzz/corpus/
fuzz/artifacts/
# Fuzz (cargo-fuzz crates live at crates/<crate>/fuzz/, e.g.
# crates/afm-markdown/fuzz/ — the bare `fuzz/*` form matched a
# nonexistent top-level dir and never ignored anything).
crates/*/fuzz/target/
crates/*/fuzz/corpus/
crates/*/fuzz/artifacts/

# Criterion
/target/criterion/
Expand Down
23 changes: 11 additions & 12 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -159,19 +159,18 @@ COPY --from=cargo-tools /usr/local/bin/ /usr/local/bin/
# means the sync never fires.
RUN rustup component add rustfmt clippy rust-src

ENV CARGO_HOME=/workspace/.cargo \
CARGO_TARGET_DIR=/workspace/target \
ENV CARGO_HOME=/cargo/home \
CARGO_TARGET_DIR=/cargo/target \
RUSTC_WRAPPER=sccache \
SCCACHE_DIR=/workspace/.sccache \
SCCACHE_DIR=/cargo/sccache \
RUST_BACKTRACE=1

# Pre-create cache mount targets so the runtime volume mounts at
# /workspace/{target,.cargo,.sccache} can attach without docker
# needing to mkdirat() into a read-only `/workspace`. Without these
# the `:ro` bind mount of the source tree blocks volume attachment
# and `docker compose run --rm ci ...` fails at container start with
# "read-only file system" during mountpoint creation.
RUN mkdir -p /workspace/target /workspace/.cargo /workspace/.sccache
# Pre-create the cache mount targets at /cargo/* so the named volume
# mounts attach cleanly. These live OUTSIDE the /workspace bind mount
# on purpose (see docker-compose.yml): nesting them under /workspace
# made the daemon create root-owned ./target / ./.cargo / ./.sccache
# on the host, littering the working tree and breaking host-side cargo.
RUN mkdir -p /cargo/target /cargo/home/registry /cargo/home/git /cargo/sccache

WORKDIR /workspace

Expand All @@ -192,9 +191,9 @@ FROM dev AS fuzz

# `rustup toolchain install` tries to self-update by looking for the
# rustup binary at $CARGO_HOME/bin/rustup. The inherited
# `CARGO_HOME=/workspace/.cargo` (set in the dev stage for runtime
# `CARGO_HOME=/cargo/home` (set in the dev stage for runtime
# volume mounts) is empty at image-build time, so the self-update step
# bails with "rustup is not installed at '/workspace/.cargo'". Override
# bails with "rustup is not installed at '/cargo/home'". Override
# the env for this one RUN so rustup finds itself at the parent rust
# image's `/usr/local/cargo` location; the runtime CARGO_HOME setting
# is unaffected.
Expand Down
73 changes: 45 additions & 28 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,35 @@
name: afm

x-common-env: &common-env
CARGO_HOME: /workspace/.cargo
CARGO_TARGET_DIR: /workspace/target
# Cargo caches live at /cargo/* — OUTSIDE the /workspace bind mount — on
# purpose. Nesting a named volume inside the bind mount (the old
# /workspace/target etc.) makes the Docker daemon create the host-side
# mountpoint (./target, ./.cargo, ./.sccache) as **root**, which litters
# the working tree with root-owned junk and breaks any host-side cargo.
# Keeping the caches at /cargo/* leaves the host's own ./target free and
# keeps the checkout clean. The named volumes still persist them.
CARGO_HOME: /cargo/home
CARGO_TARGET_DIR: /cargo/target
RUSTC_WRAPPER: sccache
SCCACHE_DIR: /workspace/.sccache
SCCACHE_DIR: /cargo/sccache
# sccache cannot cache crates compiled with cargo's incremental mode.
# Cargo defaults incremental=true for workspace members in dev/test profiles,
# which silently bypasses sccache for the very crates we build most often.
# Pinning this here applies to every service that inherits x-common-env.
CARGO_INCREMENTAL: "0"
SCCACHE_CACHE_SIZE: "10G"
RUST_BACKTRACE: "1"
# Use mold as the default linker for faster host-triple builds. The
# Dockerfile installs mold + clang and writes /root/.cargo/config.toml
# with the same intent, but CARGO_HOME=/cargo/home above means cargo
# never reads it — so drive the linker via target-scoped env here.
# CRUCIAL: scope to the host triple. A plain RUSTFLAGS=-fuse-ld=mold
# would leak into `wasm-pack build --target wasm32-unknown-unknown`
# and fail ("rust-lld: error: unknown argument: -fuse-ld=mold"); the
# wasm linker is rust-lld, not mold. The per-target form keeps host
# builds fast without breaking wasm. (Mirrors the sibling aozora repo.)
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS: "-C link-arg=-fuse-ld=mold"
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: "clang"
TERM: ${TERM:-xterm-256color}
# /workspace is bind-mounted from the host as a non-root UID, but the
# container runs as root. Git otherwise refuses operations with
Expand Down Expand Up @@ -55,10 +73,10 @@ services:
environment: *common-env
volumes:
- .:/workspace:cached
- cargo-registry:/workspace/.cargo/registry
- cargo-git:/workspace/.cargo/git
- cargo-target:/workspace/target
- sccache:/workspace/.sccache
- cargo-registry:/cargo/home/registry
- cargo-git:/cargo/home/git
- cargo-target:/cargo/target
- sccache:/cargo/sccache
init: true
tty: true
stdin_open: true
Expand All @@ -76,10 +94,10 @@ services:
environment: *common-env
volumes:
- .:/workspace:cached
- cargo-registry:/workspace/.cargo/registry
- cargo-git:/workspace/.cargo/git
- cargo-target:/workspace/target
- sccache:/workspace/.sccache
- cargo-registry:/cargo/home/registry
- cargo-git:/cargo/home/git
- cargo-target:/cargo/target
- sccache:/cargo/sccache
init: true
tty: true
stdin_open: true
Expand All @@ -95,17 +113,16 @@ services:
<<: *common-env
CI: "true"
volumes:
# `:cached` (not `:ro`): a `:ro` bind mount HIDES `/workspace/{target,
# .cargo,.sccache}` that were `mkdir -p`-ed in the image (overlayfs
# bind-mount semantics — the source dir replaces image contents),
# which then blocks the named-volume mountpoints below from
# attaching at runtime ("mkdirat ... read-only file system"). The
# runner is ephemeral; `:cached` is safe and the canonical CI mode.
# `:cached` (writable): CI steps emit artifacts back into the tree
# (wasm `pkg/`, generated docs, coverage reports), so the source
# mount stays writable. The cargo caches now live at /cargo/*
# (outside this bind mount), so the old ":ro hides the nested
# /workspace/{target,.cargo,.sccache} mountpoints" hazard is gone.
- .:/workspace:cached
- cargo-registry:/workspace/.cargo/registry
- cargo-git:/workspace/.cargo/git
- cargo-target:/workspace/target
- sccache:/workspace/.sccache
- cargo-registry:/cargo/home/registry
- cargo-git:/cargo/home/git
- cargo-target:/cargo/target
- sccache:/cargo/sccache

book:
build:
Expand All @@ -131,9 +148,9 @@ services:
CI: "true"
volumes:
- .:/workspace:cached
- cargo-registry:/workspace/.cargo/registry
- cargo-git:/workspace/.cargo/git
- cargo-target:/workspace/target
- cargo-registry:/cargo/home/registry
- cargo-git:/cargo/home/git
- cargo-target:/cargo/target

# Vite dev / preview server for the browser playground.
# Reuses the `dev` image (which already has Node 22 + wasm-pack) so no
Expand All @@ -150,10 +167,10 @@ services:
environment: *common-env
volumes:
- .:/workspace:cached
- cargo-registry:/workspace/.cargo/registry
- cargo-git:/workspace/.cargo/git
- cargo-target:/workspace/target
- sccache:/workspace/.sccache
- cargo-registry:/cargo/home/registry
- cargo-git:/cargo/home/git
- cargo-target:/cargo/target
- sccache:/cargo/sccache
- playground-node-modules:/workspace/playground/node_modules
ports:
- "5173:5173"
Expand Down
117 changes: 108 additions & 9 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,37 @@
# Disable: `just hooks-uninstall`
#
# Reference: https://lefthook.dev/configuration/
#
# Status icons in the summary
# ---------------------------
# Lefthook prints one of three markers next to each command in its
# summary line:
# ✔️ success (exit 0)
# 🗙 failed
# 🥊 also a failure path — typically when the command exited
# non-zero but lefthook fell back to its branding icon because
# the underlying tooling (docker compose run, `just`) buried
# the status. Treat 🥊 the same as 🗙 — see the actual command
# output above the summary to find the real error.
#
# `fail_text` on each command below is the explicit human hint that
# lefthook displays on failure so it's obvious what to fix without
# scrolling back through hundreds of lines of cargo output.

# Make the summary noisier on failures — show what command was
# executing, its stdout, the stderr, and the skip reasons (default
# already lists most of these, this is the explicit form so it
# survives lefthook version bumps that change the default set).
output:
- meta
- summary
- success
- failure
- execution
- execution_info
- execution_out
- skips
- empty_summary

pre-commit:
parallel: true
Expand All @@ -20,14 +51,33 @@ pre-commit:
glob: "*.rs"
run: just fmt
stage_fixed: true
fail_text: |
`cargo fmt` failed. Run `just fmt` manually to see the error.
Common cause: rustfmt edition mismatch with rust-toolchain.toml.
clippy:
glob: "*.{rs,toml}"
run: just clippy
fail_text: |
clippy reported an error. Re-run `just clippy` for the full
diagnostic. `[workspace.lints]` in Cargo.toml controls which
lints fire — fix the code rather than silencing the lint.
typos:
run: just typos
fail_text: |
`typos` found a probable typo. Re-run `just typos` to see the
candidate fixes, or add an exception to `_typos.toml` if the
flagged token is intentional (proper noun, identifier, etc.).
# Guard the vendored comrak tree against any drift — ADR-0001 sets a
# 0-line diff budget. Runs only when files under `upstream/comrak/`
# actually changed.
upstream-diff:
glob: "upstream/comrak/**"
run: just upstream-diff
fail_text: |
The vendored `upstream/comrak/` tree drifted from its pinned
upstream (ADR-0001, 0-line diff budget). Re-run
`just upstream-diff` to see the diff. A genuine comrak change
is a fork-divergence decision and needs its own ADR.

commit-msg:
commands:
Expand All @@ -41,18 +91,67 @@ commit-msg:
|| { echo 'Commit message must follow Conventional Commits (https://www.conventionalcommits.org/)'; exit 1; }"

pre-push:
parallel: true
# `just ci` runs gates sequentially in cheap-to-expensive order so
# the early ones fail fast. Parallelising at the lefthook level
# would let coverage start before lint catches a typo — and would
# also let the heavy steps race the cheap ones for the dev image's
# cargo cache. `just ci` already mirrors every CI job reachable from
# inside the dev image, so the hook just delegates to it.
#
# Push budget: a few minutes cold cache, less when warm. The
# alternative this hook exists to eliminate is "ship a PR, watch
# GitHub fail, re-push".
parallel: false
skip:
- rebase
commands:
test:
run: just test
deny:
run: just deny
# Mirror CI. `just ci` is the 18-step fail-fast pipeline (typos /
# fmt-check / upstream-diff / strict-code / verify-version-pins /
# check / doc / deny / audit / lint / build / test / prop /
# spec-commonmark / spec-gfm / coverage / book-build / udeps). The
# CI-only jobs that need extra runtime (the PR-time wasm-build /
# playground build) are deliberately left out — `just ci` keeps the
# push under a typical attention budget.
ci:
run: just ci
fail_text: |
`just ci` failed. The error is the last `error: recipe
\`<name>\` failed ...` line above this summary — that tells you
which sub-recipe (check / lint / build / test / spec-commonmark /
spec-gfm / coverage / book-build / udeps …) bombed.

Common causes:
- upstream-diff → the vendored comrak tree drifted (ADR-0001);
run `just upstream-diff`.
- typos / clippy regressed → run the individual gate
(`just typos`, `just clippy`) for a cleaner repro.
- spec-commonmark / spec-gfm → a CommonMark/GFM conformance
example regressed; `just spec-commonmark` prints the failing
example index.

Re-run `just ci` directly (not via lefthook) for the unwrapped
output if the lefthook summary truncates anything important.
# Deep proptest sweep (4096 cases vs the CI baseline 128) — runs
# locally before push so a regression that needs many cases to
# surface is caught here instead of on a downstream PR. Fuzz is
# deliberately *not* wired in: libfuzzer's nightly + corpus + cold
# start budget overshoots a pre-push hook.
# after `just ci`'s 128-case `prop` step so a regression that needs
# many cases to surface is caught here instead of on a downstream
# PR. Fuzz is deliberately *not* wired in: libfuzzer's nightly +
# corpus + cold start budget overshoots a pre-push hook.
prop-deep:
run: just prop-deep
# Tag for opt-out via `SKIP_TAGS=deep git push`. A deep sweep can
# occasionally surface a pre-existing bug in the git-pinned aozora
# core (parse / serialize / round-trip) that is unrelated to the
# current change. `SKIP_TAGS=deep` lets that unrelated PR through
# without disabling the rest of the hook stack; file an issue
# against the failing aozora crate and bump the pin once fixed.
tags:
- deep
fail_text: |
Deep proptest regression. Re-run `just prop-deep` to reproduce.
The seed for the failing case is printed in nextest's PASS/FAIL
line; pass it via `AOZORA_PROPTEST_SEED=<n>` to shrink locally.

If the failure is in the git-pinned aozora CORE (parse / render /
pipeline) and is unrelated to your change, you may opt out with
`SKIP_TAGS=deep git push`
and file an issue against the failing crate.