The Realliance website, realliance.net
  • Shell 65.1%
  • TypeScript 17.3%
  • CSS 9.7%
  • Pug 4.5%
  • HTML 2.4%
  • Other 1%
Find a file
Christopher Miller 64ecb50b09
All checks were successful
ci / build (push) Successful in 14s
ci: publish X.Y.Z only from a signed annotated tag (#6)
Reviewed-on: ⁨#6⁩
2026-11-05 03:45:20 +00:00
.forgejo/workflows ci: publish X.Y.Z only from a signed annotated tag (#6) 2026-11-05 03:45:20 +00:00
.vscode Christopherjmiller/new site (#19) 2022-11-14 17:33:53 -05:00
brand Sync brand assets from realliance/brand v0.1.0 (signed; hashed names via brand/map.json) 2026-11-04 15:20:59 -08:00
members update chris (#4) 2026-11-05 01:40:40 +00:00
release ci: publish X.Y.Z only from a signed annotated tag (#6) 2026-11-05 03:45:20 +00:00
scripts ci: publish X.Y.Z only from a signed annotated tag (#6) 2026-11-05 03:45:20 +00:00
src build: member values are pug data, not pug source; narrow Deno permissions 2026-11-04 16:32:27 -08:00
static Sync brand assets from realliance/brand v0.1.0 (signed; hashed names via brand/map.json) 2026-11-04 15:20:59 -08:00
.gitignore build: member values are pug data, not pug source; narrow Deno permissions 2026-11-04 16:32:27 -08:00
brand.lock Sync brand assets from realliance/brand v0.1.0 (signed; hashed names via brand/map.json) 2026-11-04 15:20:59 -08:00
build.ts build: member values are pug data, not pug source; narrow Deno permissions 2026-11-04 16:32:27 -08:00
deno.lock Reproducible build: content-hashed member images, pinned imports, --frozen 2026-11-04 15:20:59 -08:00
Justfile ci: publish X.Y.Z only from a signed annotated tag (#6) 2026-11-05 03:45:20 +00:00
nginx.conf Updated used docker image (#4) 2021-03-01 20:21:24 -05:00
README.md ci: publish X.Y.Z only from a signed annotated tag (#6) 2026-11-05 03:45:20 +00:00

Realliance website

Live at realliance.net. The source lives on the Realliance forge, git.realliance.net/realliance/realliance.net.

Developing

Install requirements

Arch Linux users: pacman -S just deno watchexec

Running locally

# Build
just

# Build and serve locally
just serve

# Build and serve with directory watching to rebuild (recommended)
just watch

The build runs with --frozen: it uses exactly what deno.lock records and fails if an import would change it. After changing an import, run deno cache --lock=deno.lock build.ts once and commit the lock. The build names member images by a hash of their content, so two builds of one commit give the same files.

scripts/deno-build.sh runs the build with the narrowest Deno permissions it needs (read this directory, write build/, and run nothing else). Member about.toml values reach the page as data, which pug escapes, and links must be https://. scripts/test-members.sh builds a copy with a hostile member to check that, and CI runs it.

Brand files

Fonts, logo and favicons come from the Realliance brand. The sync script copies them into static/brand/ (never loaded from a CDN). scripts/sync-brand.sh --tag vX.Y.Z clones a signed tag of realliance/brand, checks the signature against brand/release-signers.*, checks every file in brand/uses.txt against the tag manifest, and writes static/brand/, brand/map.json and brand.lock. Sources use {{brand:<logical name>}} tokens, which build.ts resolves through brand/map.json (file names carry a content hash). scripts/sync-brand.sh --check --release verifies static/ against brand.lock and requires a release tag (CI runs it).

The image

The repo has no Dockerfile. just image (or scripts/ci.sh build) does what CI does:

  1. scripts/ci-tools.sh downloads Deno and crane at pinned versions and checks their sha256.
  2. scripts/test-members.sh, then the frozen Deno build writes build/.
  3. scripts/build-image.sh packs build/ and nginx.conf into one deterministic layer (sorted, owned by root, dated by the commit) and appends it to the pinned nginx:stable-alpine base. It does this inside a throwaway local registry, so it needs no credentials.
  4. scripts/check-image.sh checks the result: the base's config, except port 8080 and the source and revision labels; manifest annotations that match those labels; the base's layers plus exactly one; only the site and nginx.conf in it.

The result is .ci/image/oci and its digest in .ci/image/digest. The same commit gives the same digest, given the pinned Deno, crane and base, and GNU tar, which writes the layer (the build log prints its version next to the digest). To check a published image, check out its commit and run scripts/check-image.sh <ref>.

scripts/build-image.sh pins the nginx base by digest. Nothing bumps it automatically: change the digest by hand, run just image, and review the change.

CI

.forgejo/workflows/ci.yaml runs on the forge's general runner:

  • every pull request runs scripts/ci.sh build and pushes nothing;
  • a push to main also publishes git.realliance.net/realliance/realliance.net:sha-<7>;
  • a tag X.Y.Z also publishes :X.Y.Z, but only for an annotated tag signed by a release signer (release/signers.asc and release/signers.fpr). A lightweight or unsigned tag fails the publish step before it uses any credential. The runner has no gpg, so the check is scripts/verify-release-tag.ts (Deno and openpgp.js, locked in release/deno.lock). Run just test-release-tag locally after changing it; CI does not run that test.

Publishing never moves a tag. If a tag already points at another digest, the job fails with refusing to move <tag>. Never approve workflow runs from fork pull requests.

Releasing

  1. Merge the PR into main (fast-forward or squash; commits need no signature) and wait for its CI run to publish sha-<7>.
  2. Tag the same commit with a signed tag and push it: git tag -s 2.4.1 -m "realliance.net 2.4.1: <what changed>" origin/main && git push origin 2.4.1.
  3. CI publishes :2.4.1 at the same digest as sha-<7>. A different digest means the build is not reproducible: the job fails, and nobody deploys that build.
  4. Each new X.Y.Z tag deploys to galaxy automatically. The cluster runs the image pinned by digest in luma-homeops (cluster/realliance.net/home.yaml). Its Renovate rule opens a re-pin for the new tag and automerges it, and Argo rolls the site within about an hour. Push only a release tag you mean to ship. Never push tags in bulk (git push --tags): a stray local tag would publish only if signed and annotated, but treat it as live.

The release signers are the Owners and maintainers who may push [0-9]* tags. release/signers.fpr lists their primary fingerprints and release/signers.asc their public keys (today only Chris's key). To add a signer, add the key and fingerprint in a reviewed PR; a tag signed by anyone else does not publish.