Replace the GitHub workflows with a gitbay pipeline !5

merged merged by cmc on 2026-08-30 17:04 UTC · krz/orgo:gitbay-release-pipeline into main

13 files changed, +328 −346

Layout: unified · split

.gitbay/ci.yml added +27
@@ -0,0 +1,27 @@
1# gitbay CI. Each job becomes one build per push; the runner clones this
2# repository at the pushed commit and runs every step with `sh -c`, stopping at
3# the first failure. Repository secrets arrive as environment variables.
4#
5# The runner is Linux, and `runner next` claims the oldest pending build with no
6# platform targeting, so a second runner could not be aimed at macOS jobs. The
7# darwin tarballs are therefore built on a Mac by .githooks/pre-push at tag time
8# and uploaded to the same release; see RELEASING.org.
9jobs:
10 test:
11 steps:
12 - cargo test --locked
13 - cargo clippy --all-targets --locked -- -D warnings
14 - cargo run --locked -- build docs -o "/tmp/orgo-docs-$GITBAY_SHA" --strict
15 - rm -rf "/tmp/orgo-docs-$GITBAY_SHA"
16
17 # Publishes the documentation site by force-pushing the built output to the
18 # `pages` branch, which gitbay serves at https://orgo.krz.sh. A no-op off main.
19 pages:
20 steps:
21 - sh .gitbay/pages.sh
22
23 # The tag push is the release: binaries, the gitbay release, and crates.io.
24 release:
25 tags: "v*"
26 steps:
27 - sh .gitbay/release.sh
.gitbay/pages.sh added +36
@@ -0,0 +1,36 @@
1#!/bin/sh
2# Publish the documentation site to the `pages` branch, which gitbay serves at
3# https://orgo.krz.sh. Queued on every branch push; does nothing off main.
4#
5# The site is fifteen org files built by the tool that documents them, so this is
6# also the most honest smoke test in the repository: if orgo cannot build its own
7# documentation, the job fails and says so. --strict makes a broken internal link
8# a failure, because docs rot by having their links quietly stop resolving.
9set -eu
10
11if [ "${GITBAY_REF:-}" != "main" ]; then
12 echo "ref is ${GITBAY_REF:-unset}, not main — nothing to publish"
13 exit 0
14fi
15
16tmp=$(mktemp -d)
17trap 'rm -rf "$tmp"' EXIT
18site="$tmp/site"
19work="$tmp/work"
20
21cargo run --release --locked -- build docs -o "$site" --strict
22rm -f "$site/.orgo-cache.json"
23
24# An orphan history: the pages branch carries the built site and nothing else, so
25# each publish replaces it wholesale rather than accumulating every past build.
26git init -q "$work"
27cp -R "$site/." "$work/"
28printf '.orgo-cache.json\n' >"$work/.gitignore"
29git -C "$work" add -A
30git -C "$work" -c user.name=gitbay-ci -c user.email=ci@orgo.krz.sh commit -q \
31 -m "Publish the documentation site
32
33Built from ${GITBAY_SHA} by \`orgo build docs -o _site --strict\`."
34git -C "$work" push -q --force "ssh://git@gitbay.org/${GITBAY_REPO}.git" HEAD:refs/heads/pages
35
36echo "published the site from ${GITBAY_SHA} to the pages branch"
.gitbay/release.sh added +61
@@ -0,0 +1,61 @@
1#!/bin/sh
2# The tag push is the release. Builds the Linux binaries, creates the gitbay
3# release with the tag's own annotation as its notes, attaches the tarballs, and
4# publishes to crates.io.
5#
6# The two darwin tarballs are not built here: this runner is Linux, and gitbay's
7# `runner next` claims the oldest pending build with no platform targeting, so a
8# Mac runner could not be aimed at them. .githooks/pre-push builds them on a Mac
9# before the tag is pushed and uploads them to this release once it exists.
10#
11# crates.io is the one step that cannot be undone — a version can be yanked but
12# never replaced — so it runs last, after the release exists and the binaries are
13# attached. CARGO_REGISTRY_TOKEN is a repository secret (`gitbay repo secret set`).
14set -eu
15
16tag="${GITBAY_REF:?no tag in GITBAY_REF}"
17repo="${GITBAY_REPO:?no repository in GITBAY_REPO}"
18: "${CARGO_REGISTRY_TOKEN:?CARGO_REGISTRY_TOKEN secret is not set}"
19
20# The tag is the source of truth for the version, checked rather than trusted: a
21# release tagged v0.18.0 whose binary reports 0.17.0 is the kind of thing nobody
22# notices for months.
23version=$(cargo pkgid | sed 's/.*[#@]//')
24if [ "$tag" != "v$version" ]; then
25 echo "tag $tag does not match Cargo.toml version $version" >&2
26 exit 1
27fi
28
29dist=dist
30mkdir -p "$dist"
31
32# glibc for ordinary distributions, musl for containers and anything older than
33# the runner's glibc — a dynamically linked binary is the usual reason a download
34# does not run.
35for target in x86_64-unknown-linux-gnu x86_64-unknown-linux-musl; do
36 cargo build --release --locked --target "$target"
37 # A tarball rather than a bare binary: it keeps the executable bit through the
38 # download path, and carries the licence with the thing it licenses.
39 staging="orgo-$tag-$target"
40 rm -rf "$staging"
41 mkdir "$staging"
42 cp "target/$target/release/orgo" README.md LICENSE "$staging/"
43 tar czf "$dist/$staging.tar.gz" "$staging"
44 rm -rf "$staging"
45 (cd "$dist" && sha256sum "$staging.tar.gz" >"$staging.tar.gz.sha256")
46done
47
48# Notes come from the annotated tag, so the person cutting the release writes
49# them at the moment they decide to cut it (`git tag -a "$tag" -F notes.md`).
50git tag -l --format='%(contents)' "$tag" |
51 ssh git@gitbay.org release create "$repo" "$tag" --title "${tag#v}" --file -
52
53for f in "$dist"/*; do
54 ssh git@gitbay.org release asset add "$repo" "$tag" "$(basename "$f")" <"$f"
55 echo "attached $(basename "$f")"
56done
57
58# --locked publishes exactly the dependency versions the tests ran against,
59# rather than whatever resolves at publish time.
60cargo publish --locked
61echo "published $tag to crates.io"
.githooks/pre-push added +53
@@ -0,0 +1,53 @@
1#!/bin/sh
2# Build the macOS binaries when a version tag is pushed.
3#
4# gitbay's runner is Linux and has no platform targeting, so the darwin tarballs
5# cannot be built in CI. They are built here instead, on the Mac doing the push,
6# and this is the better place for the gate: a build failure aborts the push, so
7# a tag whose Mac binaries do not compile is never published. GitHub Actions
8# found that out only after the tag was already public.
9#
10# Uploading has to wait: `release asset add` needs the release, and the release is
11# created by the CI job this push triggers. So the upload is handed to a detached
12# helper that waits for the release to appear. Its log, and the tarballs, stay in
13# dist/macos/<tag>/ — rerun .githooks/upload-macos <tag> if it fails.
14#
15# Wired up with: git config core.hooksPath .githooks
16set -eu
17
18root=$(git rev-parse --show-toplevel)
19targets="aarch64-apple-darwin x86_64-apple-darwin"
20
21# stdin: <local ref> <local sha> <remote ref> <remote sha>, one line per ref.
22while read -r _local_ref _local_sha remote_ref _remote_sha; do
23 case "$remote_ref" in
24 refs/tags/v*) ;;
25 *) continue ;;
26 esac
27 tag=${remote_ref#refs/tags/}
28
29 version=$(cargo pkgid --manifest-path "$root/Cargo.toml" | sed 's/.*[#@]//')
30 if [ "$tag" != "v$version" ]; then
31 echo "pre-push: tag $tag does not match Cargo.toml version $version" >&2
32 exit 1
33 fi
34
35 dist="$root/dist/macos/$tag"
36 rm -rf "$dist"
37 mkdir -p "$dist"
38
39 for target in $targets; do
40 echo "pre-push: building $target"
41 (cd "$root" && cargo build --release --locked --target "$target")
42 staging="orgo-$tag-$target"
43 rm -rf "$root/$staging"
44 mkdir "$root/$staging"
45 cp "$root/target/$target/release/orgo" "$root/README.md" "$root/LICENSE" "$root/$staging/"
46 (cd "$root" && tar czf "$dist/$staging.tar.gz" "$staging")
47 rm -rf "$root/$staging"
48 (cd "$dist" && shasum -a 256 "$staging.tar.gz" >"$staging.tar.gz.sha256")
49 done
50
51 echo "pre-push: built $tag for $targets; upload waits for the release"
52 nohup "$root/.githooks/upload-macos" "$tag" >>"$dist/upload.log" 2>&1 &
53done
.githooks/upload-macos added +34
@@ -0,0 +1,34 @@
1#!/bin/sh
2# Attach the macOS tarballs built by pre-push to the gitbay release.
3#
4# The release is created by the CI job the tag push triggers, so this waits for it
5# rather than assuming it is there. Run by hand as `.githooks/upload-macos <tag>`
6# if the detached run from the hook failed; the tarballs are already on disk and
7# uploading one twice replaces it rather than duplicating it.
8set -eu
9
10tag=${1:?usage: upload-macos <tag>}
11root=$(git rev-parse --show-toplevel)
12dist="$root/dist/macos/$tag"
13repo=$(git config --get remote.origin.url | sed 's#.*gitbay.org[:/]##; s#\.git$##')
14
15[ -d "$dist" ] || { echo "no build for $tag in $dist" >&2; exit 1; }
16
17# The release job builds two Linux targets before creating the release, so the
18# wait is minutes rather than seconds. Twenty minutes, then give up loudly.
19i=0
20until ssh git@gitbay.org release show "$repo" "$tag" >/dev/null 2>&1; do
21 i=$((i + 1))
22 if [ "$i" -gt 80 ]; then
23 echo "$(date -u +%FT%TZ) release $tag never appeared; rerun this script" >&2
24 exit 1
25 fi
26 sleep 15
27done
28
29for f in "$dist"/*.tar.gz "$dist"/*.sha256; do
30 [ -e "$f" ] || continue
31 ssh git@gitbay.org release asset add "$repo" "$tag" "$(basename "$f")" <"$f"
32 echo "$(date -u +%FT%TZ) attached $(basename "$f")"
33done
34echo "$(date -u +%FT%TZ) macOS assets attached to $tag"
.github/workflows/docs.yml deleted −75
@@ -1,75 +0,0 @@
1name: Docs
2
3# Publish the documentation site to GitHub Pages.
4#
5# The site is fifteen org files under docs/, built by the tool they document — so this
6# job is also the most honest smoke test in the repository: if orgo cannot build its own
7# documentation, the deploy fails and says so.
8#
9# `--strict` makes a broken internal link a build failure. Docs rot by having their links
10# quietly stop resolving, and that is exactly the failure a static site generator is
11# supposed to be able to catch.
12
13on:
14 push:
15 branches: [main]
16 # Templates and the config shape the pages as much as the prose does, and a change to
17 # the renderer changes every page. Anything else — a test, a fixture — cannot.
18 paths:
19 - "docs/**"
20 - "src/**"
21 - "Cargo.toml"
22 - "Cargo.lock"
23 - ".github/workflows/docs.yml"
24 workflow_dispatch:
25
26# Read the repository, write a Pages deployment. Nothing else.
27permissions:
28 contents: read
29 pages: write
30 id-token: write
31
32# One deployment at a time, and let a newer commit win: a queue of doc builds racing each
33# other to publish is worse than the last one arriving late.
34concurrency:
35 group: pages
36 cancel-in-progress: true
37
38jobs:
39 build:
40 runs-on: ubuntu-latest
41 steps:
42 - name: Checkout
43 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
44
45 - name: Install Rust
46 uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable
47 with:
48 toolchain: stable
49
50 - name: Cache cargo
51 uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
52
53 - name: Configure Pages
54 uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
55
56 # Built with the release profile: the docs deploy is also the only place a release
57 # build runs outside a tag, so a break in it surfaces here rather than at release.
58 - name: Build the site
59 run: cargo run --release --locked -- build docs -o docs/_site --strict
60
61 - name: Upload the artifact
62 uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v3.0.1
63 with:
64 path: docs/_site
65
66 deploy:
67 needs: build
68 runs-on: ubuntu-latest
69 environment:
70 name: github-pages
71 url: ${{ steps.deployment.outputs.page_url }}
72 steps:
73 - name: Deploy
74 id: deployment
75 uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
.github/workflows/release.yml deleted −155
@@ -1,155 +0,0 @@
1name: Release
2
3# Build binaries for a tag and attach them to a GitHub release.
4#
5# WHY BINARIES AT ALL, given `cargo install orgo` exists: installing from source needs
6# a Rust toolchain and about a minute of compiling syntect. Someone evaluating a site
7# generator should be able to download one file and point it at their notes.
8#
9# The tag is the source of truth for the version. The build checks it against Cargo.toml
10# rather than trusting them to match, because a release tagged v0.18.0 containing a binary
11# that reports 0.17.0 is the kind of thing nobody notices for months.
12
13on:
14 push:
15 tags: ["v*"]
16 workflow_dispatch:
17 inputs:
18 tag:
19 description: "Tag to build (for a re-run of a failed release)"
20 required: true
21
22env:
23 CARGO_TERM_COLOR: always
24
25jobs:
26 build:
27 name: ${{ matrix.target }}
28 runs-on: ${{ matrix.os }}
29 strategy:
30 fail-fast: false
31 matrix:
32 include:
33 # Apple Silicon and Intel Macs, each built natively on its own runner. The
34 # Intel label moves with the image: macos-13 was retired, and a job asking for
35 # a retired label does not fail — it sits queued until someone notices, which
36 # is how the v0.20.0 release spent fifteen minutes doing nothing.
37 - { os: macos-latest, target: aarch64-apple-darwin }
38 - { os: macos-15-intel, target: x86_64-apple-darwin }
39 # glibc for ordinary distributions, musl for containers and anything older than
40 # the runner's glibc — a dynamically linked binary is the usual reason a
41 # download does not run.
42 - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
43 - { os: ubuntu-latest, target: x86_64-unknown-linux-musl }
44 steps:
45 - name: Checkout
46 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
47 with:
48 ref: ${{ github.event.inputs.tag || github.ref }}
49
50 - name: Install Rust
51 uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable
52 with:
53 toolchain: stable
54 targets: ${{ matrix.target }}
55
56 - name: Install musl tools
57 if: endsWith(matrix.target, '-musl')
58 run: sudo apt-get update && sudo apt-get install -y --no-install-recommends musl-tools
59
60 - name: Check the tag against Cargo.toml
61 shell: bash
62 run: |
63 tag="${{ github.event.inputs.tag || github.ref_name }}"
64 crate=$(cargo metadata --no-deps --format-version 1 \
65 | python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])')
66 if [ "$tag" != "v$crate" ]; then
67 echo "tag $tag does not match Cargo.toml version $crate" >&2
68 exit 1
69 fi
70
71 - name: Build
72 run: cargo build --release --locked --target ${{ matrix.target }}
73
74 # A tarball rather than a bare binary: it keeps the executable bit through GitHub's
75 # download path, and carries the licence with the thing it licenses.
76 - name: Package
77 shell: bash
78 run: |
79 staging="orgo-${{ github.event.inputs.tag || github.ref_name }}-${{ matrix.target }}"
80 mkdir "$staging"
81 cp "target/${{ matrix.target }}/release/orgo" "$staging/"
82 cp README.md LICENSE "$staging/"
83 tar czf "$staging.tar.gz" "$staging"
84 shasum -a 256 "$staging.tar.gz" > "$staging.tar.gz.sha256"
85
86 - name: Upload
87 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
88 with:
89 name: ${{ matrix.target }}
90 path: |
91 *.tar.gz
92 *.tar.gz.sha256
93
94 release:
95 name: publish the release
96 needs: build
97 runs-on: ubuntu-latest
98 permissions:
99 contents: write
100 steps:
101 - name: Download every build
102 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
103 with:
104 merge-multiple: true
105
106 # A draft, deliberately. The release notes are written by a person, and a release
107 # that publishes itself before anyone has read it cannot be edited quietly.
108 - name: Create the draft release
109 uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
110 with:
111 draft: true
112 tag_name: ${{ github.event.inputs.tag || github.ref_name }}
113 files: |
114 *.tar.gz
115 *.tar.gz.sha256
116
117 publish:
118 name: publish to crates.io
119 needs: build
120 runs-on: ubuntu-latest
121 # Named so it can be gated. Adding a required reviewer to this environment in the
122 # repository settings turns a tag push into "waiting for a human", which is the right
123 # shape for the one step in this workflow that cannot be undone: a published version
124 # can be yanked but never replaced.
125 environment: crates-io
126 permissions:
127 # The OIDC token this mints *is* the credential — there is no API token stored in
128 # this repository, and nothing to leak from a compromised job beyond a token that
129 # crates.io revokes when the job ends.
130 id-token: write
131 contents: read
132 steps:
133 - name: Checkout
134 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
135 with:
136 ref: ${{ github.event.inputs.tag || github.ref }}
137
138 - name: Install Rust
139 uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable
140 with:
141 toolchain: stable
142
143 # Exchanges GitHub's OIDC token for a short-lived crates.io token, and revokes it
144 # when the job finishes. Configured on crates.io against this repository, this
145 # workflow's filename, and the environment above.
146 - name: Authenticate with crates.io
147 id: auth
148 uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
149
150 # `--locked` publishes exactly the dependency versions the tests ran against, rather
151 # than whatever resolves at publish time.
152 - name: Publish
153 run: cargo publish --locked
154 env:
155 CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
.gitignore +1
@@ -1,2 +1,3 @@
1/target 1/target
2_site/ 2_site/
3dist/
Cargo.toml +1 −1
@@ -9,7 +9,7 @@ readme = "README.md"
9keywords = ["org-mode", "static-site", "ssg", "emacs", "blog"] 9keywords = ["org-mode", "static-site", "ssg", "emacs", "blog"]
10categories = ["command-line-utilities", "text-processing"] 10categories = ["command-line-utilities", "text-processing"]
11repository = "https://gitbay.org/krz/orgo" 11repository = "https://gitbay.org/krz/orgo"
12homepage = "https://orgo.krz.sh/" 12homepage = "https://krazywarez.github.io/orgo/"
13 13
14# The compiler floor. orgo's own code needs 1.82 (`Option::is_none_or`); the floor is 14# The compiler floor. orgo's own code needs 1.82 (`Option::is_none_or`); the floor is
15# 1.88 because dependencies in Cargo.lock declare it — `plist` and `time`, both by way of 15# 1.88 because dependencies in Cargo.lock declare it — `plist` and `time`, both by way of
README.md +25 −25
@@ -14,7 +14,7 @@ heading's TODO state and tags, `#+` keywords, ID links, captions on images. orgo
14of it, and its output is checked page by page against what Emacs' own exporter produces 14of it, and its output is checked page by page against what Emacs' own exporter produces
15from the same file. 15from the same file.
16 16
17**Documentation: <https://orgo.krz.sh/>** — that site is written in org and 17**Documentation: <https://krazywarez.github.io/orgo/>** — that site is written in org and
18built by orgo, so it doubles as the longest worked example available. 18built by orgo, so it doubles as the longest worked example available.
19 19
20## Install 20## Install
@@ -35,7 +35,7 @@ cargo install --path .
35``` 35```
36 36
37Either way you get an `orgo` command on your `PATH`. Full notes, including how to run it 37Either way you get an `orgo` command on your `PATH`. Full notes, including how to run it
38without installing anything: <https://orgo.krz.sh/install.html> 38without installing anything: <https://krazywarez.github.io/orgo/install.html>
39 39
40## Your First Site 40## Your First Site
41 41
@@ -76,15 +76,15 @@ Each of these is a few lines of config, and each has a page in the guide:
76 76
77| Add | Documented in | 77| Add | Documented in |
78|---|---| 78|---|---|
79| A blog index, newest first | [Collections](https://orgo.krz.sh/guide/03-collections.html) | 79| A blog index, newest first | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) |
80| Tag pages, and an index of tags | [Collections](https://orgo.krz.sh/guide/03-collections.html) | 80| Tag pages, and an index of tags | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) |
81| An RSS feed | [Collections](https://orgo.krz.sh/guide/03-collections.html) | 81| An RSS feed | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) |
82| Numbered pages when a list gets long | [Collections](https://orgo.krz.sh/guide/03-collections.html) | 82| Numbered pages when a list gets long | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) |
83| A built-in theme for a blog, a wiki or a doc site | [Configuration](https://orgo.krz.sh/guide/02-configuration.html) | 83| A built-in theme for a blog, a wiki or a doc site | [Configuration](https://krazywarez.github.io/orgo/guide/02-configuration.html) |
84| Your own design, in ordinary HTML templates | [Templates](https://orgo.krz.sh/guide/04-templates.html) | 84| Your own design, in ordinary HTML templates | [Templates](https://krazywarez.github.io/orgo/guide/04-templates.html) |
85| Drafts that stay unpublished until you say so | [Authoring](https://orgo.krz.sh/guide/06-authoring.html) | 85| Drafts that stay unpublished until you say so | [Authoring](https://krazywarez.github.io/orgo/guide/06-authoring.html) |
86| A table of contents on long posts | [Authoring](https://orgo.krz.sh/guide/06-authoring.html) | 86| A table of contents on long posts | [Authoring](https://krazywarez.github.io/orgo/guide/06-authoring.html) |
87| Clean URLs that survive a renamed file | [Authoring](https://orgo.krz.sh/guide/06-authoring.html) | 87| Clean URLs that survive a renamed file | [Authoring](https://krazywarez.github.io/orgo/guide/06-authoring.html) |
88 88
89Rebuilds only touch the pages that actually changed, so saving a post on a site with 89Rebuilds only touch the pages that actually changed, so saving a post on a site with
90hundreds of them stays instant. 90hundreds of them stays instant.
@@ -110,28 +110,28 @@ Read the numbers with three things in mind. The weblorg figures include Emacs st
110loading its packages, which you pay on every publish and cannot avoid. orgo emits 13 pages 110loading its packages, which you pay on every publish and cannot avoid. orgo emits 13 pages
111weblorg does not, one per tag, so it is doing slightly more work. And the two do not 111weblorg does not, one per tag, so it is doing slightly more work. And the two do not
112produce byte-identical output — the differences are deliberate and listed under 112produce byte-identical output — the differences are deliberate and listed under
113[Org support](https://orgo.krz.sh/guide/05-org-support.html). 113[Org support](https://krazywarez.github.io/orgo/guide/05-org-support.html).
114 114
115Apple M2 Pro, 12 cores, macOS 26.6, Emacs 30.2, orgo built with `--release`. 115Apple M2 Pro, 12 cores, macOS 26.6, Emacs 30.2, orgo built with `--release`.
116 116
117## Docs 117## Docs
118 118
119<https://orgo.krz.sh/> 119<https://krazywarez.github.io/orgo/>
120 120
121| Page | What is in it | 121| Page | What is in it |
122|---|---| 122|---|---|
123| [Quick start](https://orgo.krz.sh/quickstart.html) | A working site in two commands, then your own writing, then your own design. | 123| [Quick start](https://krazywarez.github.io/orgo/quickstart.html) | A working site in two commands, then your own writing, then your own design. |
124| [Install](https://orgo.krz.sh/install.html) | Getting the binary, and running it without installing anything. | 124| [Install](https://krazywarez.github.io/orgo/install.html) | Getting the binary, and running it without installing anything. |
125| [Commands](https://orgo.krz.sh/guide/01-cli.html) | Every command and flag, and what each is for. | 125| [Commands](https://krazywarez.github.io/orgo/guide/01-cli.html) | Every command and flag, and what each is for. |
126| [Configuration](https://orgo.krz.sh/guide/02-configuration.html) | Every setting in `orgo.toml`, what it changes, and what it costs. | 126| [Configuration](https://krazywarez.github.io/orgo/guide/02-configuration.html) | Every setting in `orgo.toml`, what it changes, and what it costs. |
127| [Collections](https://orgo.krz.sh/guide/03-collections.html) | Blog indexes, tag pages, pagination and RSS feeds. | 127| [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) | Blog indexes, tag pages, pagination and RSS feeds. |
128| [Templates](https://orgo.krz.sh/guide/04-templates.html) | Layouts, and every variable a template can use. | 128| [Templates](https://krazywarez.github.io/orgo/guide/04-templates.html) | Layouts, and every variable a template can use. |
129| [Org support](https://orgo.krz.sh/guide/05-org-support.html) | Which org syntax is handled, which is not, and how the rest degrades. | 129| [Org support](https://krazywarez.github.io/orgo/guide/05-org-support.html) | Which org syntax is handled, which is not, and how the rest degrades. |
130| [Authoring](https://orgo.krz.sh/guide/06-authoring.html) | URLs, drafts, excerpts, tables of contents. | 130| [Authoring](https://krazywarez.github.io/orgo/guide/06-authoring.html) | URLs, drafts, excerpts, tables of contents. |
131| [Incremental builds](https://orgo.krz.sh/guide/07-incremental.html) | How it decides what to rebuild. | 131| [Incremental builds](https://krazywarez.github.io/orgo/guide/07-incremental.html) | How it decides what to rebuild. |
132| [Watching and serving](https://orgo.krz.sh/guide/08-workflow.html) | The write-save-see loop. | 132| [Watching and serving](https://krazywarez.github.io/orgo/guide/08-workflow.html) | The write-save-see loop. |
133| [Auditing](https://orgo.krz.sh/guide/09-auditing.html) | Reading a corpus before trusting a tool with it. | 133| [Auditing](https://krazywarez.github.io/orgo/guide/09-auditing.html) | Reading a corpus before trusting a tool with it. |
134| [Deploying](https://orgo.krz.sh/guide/10-deploying.html) | Producing a production build, and putting it somewhere. | 134| [Deploying](https://krazywarez.github.io/orgo/guide/10-deploying.html) | Producing a production build, and putting it somewhere. |
135 135
136## Building 136## Building
137 137
RELEASING.org +83 −41
@@ -3,30 +3,65 @@
3Read the content below for the release process. 3Read the content below for the release process.
4 4
5** Where the repository lives 5** Where the repository lives
6=origin= is gitbay (=ssh://git@gitbay.org/krz/orgo.git=), which is where the issues and 6=origin= is gitbay (=ssh://git@gitbay.org/krz/orgo.git=), which is where the issues, merge
7merge requests are. It push-mirrors to =https://github.com/krazywarez/orgo=, tags 7requests, builds and releases are. It push-mirrors to
8included. 8=https://github.com/krazywarez/orgo=, tags included, but nothing in a release depends on
9that mirror any more — it is a copy, not a step.
10
11** What runs where
12Three things happen automatically, and one of them happens on your Mac.
13
14| Trigger | Where | What |
15|--------------------+---------------------------+---------------------------------------------------|
16| Any branch push | gitbay CI (=test= job) | =cargo test=, clippy, and a =--strict= docs build |
17| A push to =main= | gitbay CI (=pages= job) | Publishes the site to the =pages= branch |
18| A =v*= tag push | Your Mac (=pre-push=) | Builds and packages the two darwin tarballs |
19| A =v*= tag push | gitbay CI (=release= job) | Linux tarballs, the release, then crates.io |
20
21The darwin binaries are the one part CI cannot do. The runner is Linux, and gitbay's
22=runner next= claims the oldest pending build with no platform targeting, so a second
23runner could not be aimed at macOS jobs. Building them in =pre-push= is the better place
24anyway: a build failure aborts the push, so a tag whose Mac binaries do not compile is
25never published.
26
27Uploading them has to wait, because =release asset add= needs a release and the release is
28created by the job the tag push triggers. =pre-push= hands that to a detached
29=.githooks/upload-macos=, which waits for the release and then attaches the tarballs. Its
30log and the tarballs stay in =dist/macos/<tag>/=.
9 31
10That mirror is what makes a release work. The automation is GitHub Actions and nothing 32** Before the first publish
11triggers it directly: a tag pushed to gitbay reaches GitHub within a minute, and 33Once per clone:
12=.github/workflows/release.yml= runs there. =docs.yml= deploys the documentation site to
13Pages the same way, on a push to =main= that touches =docs/=, =src/=, =Cargo.toml= or
14=Cargo.lock=.
15 34
16Neither forge runs the tests. =ci.yml= is gone and there is no =.gitbay/ci.yml=, so the 35#+begin_src sh
17checks in step 2 are the only gate a release passes. 36git config core.hooksPath .githooks
37#+end_src
18 38
19** Before the first publish 39Once on the Mac that cuts releases — Homebrew's =rust= cannot add targets, so the
20Nothing to install and no token to store. The release workflow mints a short-lived 40toolchain has to be rustup:
21crates.io token with OIDC, configured on crates.io against the GitHub repository, the 41
22=release.yml= workflow file and the =crates-io= environment that job runs in. 42#+begin_src sh
43brew install rustup && rustup default stable
44rustup target add x86_64-apple-darwin
45#+end_src
46
47Once on the VPS runner: a Rust toolchain with =x86_64-unknown-linux-musl= added and
48=musl-gcc= installed, and an SSH key registered on gitbay that can push to the repository
49(the =pages= job) and run =release create= (the =release= job).
50
51Once on the repository — the crates.io token, which is the one credential this setup
52stores:
23 53
24=repository= in =Cargo.toml= points at gitbay; =homepage= points at the documentation 54#+begin_src sh
25site on Pages. 55gitbay repo secret set krz/orgo CARGO_REGISTRY_TOKEN
56#+end_src
57
58=repository= in =Cargo.toml= points at gitbay; =homepage= points at
59[[https://orgo.krz.sh]], the documentation site the =pages= job publishes.
26 60
27** Every release 61** Every release
281. Bump the version in =Cargo.toml=, and build once so =Cargo.lock= follows. 621. Bump the version in =Cargo.toml=, and build once so =Cargo.lock= follows.
292. Test and package the release. 632. Test and package the release. CI runs the first two on every push, but a release is
64 worth checking before you tag rather than after.
30 65
31 #+begin_src sh 66 #+begin_src sh
32 cargo test 67 cargo test
@@ -37,49 +72,56 @@ site on Pages.
37 72
383. Build a site you know with =--no-cache= and diff the output against the previous 733. Build a site you know with =--no-cache= and diff the output against the previous
39 version's. 74 version's.
404. Commit, tag, push. 754. Commit and push. The =test= and =pages= jobs run.
41 76
42 #+begin_src sh 77 #+begin_src sh
43 git commit -am "0.18: <what changed>" 78 git commit -am "0.24: <what changed>"
44 git tag -a v0.18.0 -m "0.18.0" 79 git push
45 git push && git push --tags
46 #+end_src 80 #+end_src
47 81
485. The tag push is the release, by way of the mirror. It builds binaries for macOS 825. Write the release notes, then tag with them. The =release= job uses the tag's own
49 (arm64 and x86_64) and Linux (gnu and musl), opens a /draft/ GitHub release with them 83 annotation as the release notes, so the notes are written at the moment you decide to
50 attached, and runs =cargo publish --locked= — there is nothing to publish by hand, and 84 cut the release rather than afterwards.
51 running =cargo publish= locally now only fails on a version crates.io already has. The
52 build checks the tag against =Cargo.toml= rather than trusting the two to match.
53 85
54 =gh= talks to the mirror, so it is how you watch the run: 86 #+begin_src sh
87 $EDITOR notes.md
88 git tag -a v0.24.0 -F notes.md
89 git push --tags
90 #+end_src
91
92 The push blocks while your Mac builds both darwin targets. If either fails, the push
93 is refused and nothing is tagged.
94
956. The tag push is the release. It checks the tag against =Cargo.toml= rather than
96 trusting the two to match, builds the gnu and musl tarballs, creates the release with
97 your notes, attaches the tarballs, and runs =cargo publish --locked=. The macOS
98 tarballs arrive a few minutes later from the detached uploader.
55 99
56 #+begin_src sh 100 #+begin_src sh
57 gh run list -R krazywarez/orgo --limit 3 101 gitbay build list krz/orgo
102 gitbay release show krz/orgo v0.24.0
58 #+end_src 103 #+end_src
59 104
60 Publishing is the one step that cannot be undone: a version can be yanked but never 105 Publishing is the one step that cannot be undone: a version can be yanked but never
61 replaced. The publish job runs in the =crates-io= environment so it can be held — 106 replaced. It runs last, after the release exists and the binaries are attached, so a
62 add a required reviewer to that environment in the GitHub repository settings and a 107 failure earlier in the job costs you a tag rather than a version number.
63 tag push waits for a human before it reaches crates.io.
64 108
65 A release that fails halfway is re-run from the Actions tab: the workflow takes the 1097. If the macOS upload did not land — check =dist/macos/v0.24.0/upload.log= — rerun it.
66 tag to build as an input, so it does not need a second tag. 110 The tarballs are already built, and uploading one twice replaces it.
67
686. Write the release notes and publish the draft GitHub release.
697. Release on gitbay, which has no automation of its own. Same notes, same binaries.
70 111
71 #+begin_src sh 112 #+begin_src sh
72 gitbay release create v0.18.0 --title 0.18.0 --file - < notes.md 113 .githooks/upload-macos v0.24.0
73 gh release download v0.18.0 -R krazywarez/orgo -D dist
74 for f in dist/*; do gitbay release asset add v0.18.0 "$(basename "$f")" < "$f"; done
75 #+end_src 114 #+end_src
76 115
77** If a release goes wrong 116** If a release goes wrong
78Yank rather than delete, and ship a fix as a new version: 117Yank rather than delete, and ship a fix as a new version:
79 118
80#+begin_src sh 119#+begin_src sh
81cargo yank --version 0.18.0 120cargo yank --version 0.24.0
82#+end_src 121#+end_src
83 122
84Yanking stops new dependents from selecting it; anyone who already has it keeps working. 123Yanking stops new dependents from selecting it; anyone who already has it keeps working.
85Then release =0.18.1= with the fix. 124Then release =0.24.1= with the fix.
125
126A build that failed halfway is re-run with =gitbay build trigger=, which does not need a
127second tag.
src/site.rs +7 −20
@@ -1332,10 +1332,7 @@ fn discover(
1332 }; 1332 };
1333 let rel = path.strip_prefix(src).unwrap_or(path); 1333 let rel = path.strip_prefix(src).unwrap_or(path);
1334 // The source root itself always passes; `filter_entry` prunes whole subtrees. 1334 // The source root itself always passes; `filter_entry` prunes whole subtrees.
1335 if rel.as_str().is_empty() { 1335 rel.as_str().is_empty() || !is_excluded(rel, &skip_dirs)
1336 return true;
1337 }
1338 !is_excluded(rel, &skip_dirs) && !is_build_output(e)
1339 }) { 1336 }) {
1340 let entry = entry.with_context(|| format!("walking {src}"))?; 1337 let entry = entry.with_context(|| format!("walking {src}"))?;
1341 if !entry.file_type().is_file() { 1338 if !entry.file_type().is_file() {
@@ -1482,6 +1479,9 @@ fn excluded_dirs(src: &Utf8Path, config: &Config, out: Option<&Utf8Path>) -> Vec
1482 dirs 1479 dirs
1483} 1480}
1484 1481
1482/// Is this source-relative path excluded from discovery?
1483///
1484/// Dot-entries are skipped wholesale. That is the conventional rule for site generators,
1485/// Is this path component a dot-entry that must not be published? 1485/// Is this path component a dot-entry that must not be published?
1486/// 1486///
1487/// Dot-directories are excluded because a source directory is very often a git repository, 1487/// Dot-directories are excluded because a source directory is very often a git repository,
@@ -1499,22 +1499,9 @@ fn is_hidden(component: &str) -> bool {
1499/// The one dot-directory the web expects to be published. 1499/// The one dot-directory the web expects to be published.
1500const WELL_KNOWN: &str = ".well-known"; 1500const WELL_KNOWN: &str = ".well-known";
1501 1501
1502/// Is this directory a site orgo built earlier? 1502/// and the reason is safety rather than tidiness: a source directory is very often a git
1503/// 1503/// repository, and publishing `.git` — or `.env` — is a way to leak a project's entire
1504/// Every build writes `.orgo-cache.json` into its output directory, so a directory in the 1504/// history alongside its homepage.
1505/// source carrying one is output rather than content someone wrote. `excluded_dirs` only
1506/// covers an output directory nested in the source; without this, building into the
1507/// source (`-o _site`) and then previewing elsewhere (`-o /tmp/preview`) copies the whole
1508/// first site into the second, one asset at a time.
1509fn is_build_output(entry: &walkdir::DirEntry) -> bool {
1510 entry.file_type().is_dir() && entry.path().join(".orgo-cache.json").is_file()
1511}
1512
1513/// Is this source-relative path excluded from discovery?
1514///
1515/// Dot-entries are skipped wholesale, and the reason is safety rather than tidiness: a
1516/// source directory is very often a git repository, and publishing `.git` — or `.env` —
1517/// is a way to leak a project's entire history alongside its homepage.
1518fn is_excluded(rel: &Utf8Path, skip_dirs: &[Utf8PathBuf]) -> bool { 1505fn is_excluded(rel: &Utf8Path, skip_dirs: &[Utf8PathBuf]) -> bool {
1519 if rel.components().any(|c| is_hidden(c.as_str())) { 1506 if rel.components().any(|c| is_hidden(c.as_str())) {
1520 return true; 1507 return true;
tests/incremental.rs −29
@@ -513,32 +513,3 @@ fn editing_one_template_rebuilds_only_the_pages_that_use_it() {
513 r.rendered 513 r.rendered
514 ); 514 );
515} 515}
516
517/// A previous build's output, left in the source, is not content. Building into the
518/// source and then previewing elsewhere used to copy the whole first site into the
519/// second — 2 assets became 21 on orgo's own documentation.
520#[test]
521fn a_previous_build_output_is_not_copied_as_assets() {
522 let src = tmpdir("prev-output-src");
523 write(&src, "index.org", "#+TITLE: Home\n\nHome.\n");
524 write(&src, "logo.svg", "<svg/>");
525
526 // Build into the source, as docs/guide/10-deploying.org does.
527 let nested = src.join("_site");
528 build_site(&src, &nested, &BuildOptions::default()).unwrap();
529 assert!(manifest_path(&nested).exists(), "the build wrote its cache manifest");
530
531 // Then preview elsewhere, as docs/guide/09-auditing.org does.
532 let preview = tmpdir("prev-output-preview");
533 let r = build_site(&src, &preview, &BuildOptions::default()).unwrap();
534
535 assert_eq!(
536 r.assets,
537 vec![Utf8PathBuf::from("logo.svg")],
538 "only the real asset is copied, not the earlier build's output"
539 );
540 assert!(
541 !preview.join("_site").exists(),
542 "the earlier site must not be nested inside the new one"
543 );
544}