Skip to content

Latest commit

 

History

History
213 lines (164 loc) · 9.23 KB

File metadata and controls

213 lines (164 loc) · 9.23 KB

Contributing

Adding a documentation generator

The three existing builders — Pandoc, Zensical, Sphinx — are deliberately different from each other, and they exist as much to be copied from as to be used. If you want a generator that is not here, you are adding one action and changing nothing else.

Pick the closest existing one and work from it:

If your generator… start from
runs a binary over a source directory build-pandoc
installs from PyPI and reads its own config file build-zensical
needs the project's own pinned dependencies build-sphinx

The contract

A build-* action has to satisfy five things. Everything downstream — index-site, deploy-site, the Pages upload, the deploy — depends on these and nothing else.

  1. Write a directory of static HTML to output. Default it to _site. Generators that insist on their own location (Sphinx wants docs/_build/html, Zensical wants site/) relocate afterwards. This is the whole seam: downstream never learns which generator ran.

  2. Put index.html at the root of it. deploy-site refuses to publish without one, because a site that "builds" into a directory with no index deploys a 404 over a working site.

  3. Do not write build state into the source tree. Build straight into output where the generator allows it. Sphinx leaves a .doctrees directory in its output; build-sphinx.sh removes it. Generated files landing in the source tree is how they end up committed.

  4. Keep the logic in bin/, not in action.yml. The action installs a toolchain and marshals arguments. The script does the work, takes no GitHub context, and runs on a laptop. This is what makes a local preview provably the same as what CI publishes. (ADR-0005)

  5. Pin the toolchain version, and say why in the input description. Every builder here pins. An unpinned install means a generator release you did not choose changes your published output.

The five files

bin/build-<engine>.sh                        the logic
.github/actions/build-<engine>/action.yml    install + marshal
test/fixture-<engine>/                       a minimal project
.github/workflows/ci.yml                     a job exercising the action
README.md                                    an entry and an input table

A docs-<engine>.yml reusable workflow is optional. Add one only if the build/index/deploy sequence for that generator is worth wrapping; projects that need setup steps first cannot use it anyway (see below).

Walking through it

1. Write the script. Copy the closest bin/build-*.sh. Keep the flag parsing shape, keep --help, and end by counting the HTML it produced and failing if that count is zero. Silent success on an empty build is the failure mode that matters.

2. Write the action. Copy the matching action.yml. Three things are easy to get wrong:

  • Reach the script with "${{ github.action_path }}/../../../bin/build-<engine>.sh". GitHub checks out the whole repository for a remote action, so bin/ is there.

  • Multi-line inputs arrive as one string. Split them the way the existing actions do:

    while IFS= read -r line; do
      [ -n "$line" ] && ARGS+=(--thing "$line")
    done <<< "$INPUT"
  • Pass inputs through env: rather than interpolating ${{ }} straight into run:. And remember backticks inside double quotes are command substitution in bash — echo "set \foo`"tries to runfoo`.

3. Add a fixture. test/fixture-<engine>/ with the smallest project that still exercises something real: two pages, a link between them, and a table if the generator does anything interesting with one.

4. Add a CI job. Exercise it through the action, not by calling the script, so the argument marshalling is covered too. Then assert on the output — that index.html exists, that the second page rendered, that no build state leaked. Assertions that would have caught a real bug are worth more than a count of pages.

5. Document it. An entry in the README's action table, an input table, and anything surprising about the generator. Both existing entries carry a surprise worth knowing: Zensical writes directory-style URLs (second/index.html, not second.html), and Sphinx dependencies belong to the project rather than to this repository.

Things that will bite you

  • Do not add an engine: input to an existing action. The inputs genuinely differ per generator — Pandoc needs a template and Lua filters, Sphinx needs requirements and a Python version, Zensical needs its own config. One action would become a union of mostly irrelevant options, and every new generator would make it worse for everyone already using it. (ADR-0003)

  • A reusable workflow gives the caller nowhere to add steps. If a project has to compile something before its docs build, it cannot use docs-<engine>.yml — it has to assemble the actions in its own job. CL-web-components is the example: it runs deno task build first, so it uses build-pandoc directly. That is the design working, not a workaround. (ADR-0004)

  • uses: cannot take an expression. Reusable workflows reference this repository's own actions at a literal ref, so releasing means updating those strings. CI fails if they drift from the current major tag — see the self-reference check in ci.yml.

Conventions

  • Keep the input surface small. Once repositories reference @v1, renaming an input is a breaking change for all of them. The inputs are the real API; the implementation behind them can change freely. (ADR-0006)

  • Make assumptions explicit. If an action needs docs/ to exist, that should be an input with a default, not an unstated requirement.

  • Say which repeated values must match. When an example shows the same string twice, a reader cannot tell whether that is required or a coincidence, and both guesses cause bugs. A build action's output and deploy-site's path must be the same directory; prefix and the path in public-base-url need not correspond. Shared defaults make this worse, not better: two inputs that both default to _site look independent until someone changes one.

  • Say what was dropped. If a build skips or truncates something, log it. Silent truncation reads as "covered everything" when it did not.

  • Leaving is allowed. A project that needs behaviour which does not generalise should write its own job and use whichever actions still fit — or none. That is better than growing a flag for every special case until the shared thing serves nobody well. If dropping the shared workflow feels like defection, people will lobby for flags instead, and the flags will win.

Testing

.github/workflows/ci.yml runs on every change:

  • shellcheck --severity=style over bin/*.sh
  • actionlint over the workflows
  • a build per generator against its fixture, plus assertions on the output
  • the deploy-site guards
  • the self-reference check

Run the scripts directly while developing — they need no GitHub context:

bin/build-pandoc.sh --docs-dir test/fixture/docs --output /tmp/site
bin/index-site.sh --site /tmp/site

Decisions

Why this repository is shaped the way it is lives in docs/decisions/, as MADR architecture decision records. Read those before changing the contract, the versioning scheme, or the split between actions and workflows — each records alternatives that were costed and rejected, which is the part the code cannot tell you.

Adding a generator does not need an ADR. Changing the contract every generator satisfies does.

Every change that reaches a consumer gets a CHANGELOG.md entry under ## [Unreleased], following Keep a Changelog. Consumers reference a moving major tag, so the changelog is the only way they learn what moved.

Releasing

Entries accumulate under ## [Unreleased] as pull requests merge. Releasing is a separate, deliberate step — one commit and one tag per release, not per change.

  1. Rename ## [Unreleased] to ## [X.Y.Z] - YYYY-MM-DD, and add a fresh empty ## [Unreleased] above it.
  2. Update the link references at the bottom of CHANGELOG.md.
  3. Commit that on its own: Release X.Y.Z.
  4. Tag that commit: git tag -a vX.Y.Z -m "vX.Y.Z — <summary>".
  5. Move the major tag: git tag -f vX and force-push it.
  6. Push the commit and both tags.

Tagging the release commit means the tag points at a changelog that describes itself, rather than one still saying "Unreleased".

Choosing the number, per ADR-0006:

Change
An input renamed or removed, an action path changed, a default changed major
A new action or input, backward compatible minor
A fix with no interface change patch

Changing a default is breaking even though nothing in the interface moved: existing callers get different behavior without editing anything.