From 779b7d6709b01326db9cffd83dda64fea015891c Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 16 Feb 2026 21:24:34 -0800 Subject: [PATCH] Eliminate double towncrier run in release workflow (#199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Added a new `build_quartz` Dagger function that builds the Quartz site from a pre-processed source tree (no towncrier) - Reordered the release workflow so towncrier runs **once** on the runner, then passes the updated working tree to `build-quartz` - `build_docs` and `build_changelog` are preserved for standalone use — `build_docs` now delegates to `build_quartz` internally ## Motivation Previously towncrier ran twice per release: once inside a Dagger container (via `build_docs` → `build_changelog`) and once on the runner to capture CHANGELOG.md changes for the git commit. This was wasteful and fragile — if towncrier behavior changed, the two runs could produce different results. ## Test plan - [ ] Review diff to confirm workflow step ordering is correct - [ ] Trigger a release and confirm towncrier runs only once - [ ] Verify the docs tarball contains the updated CHANGELOG.md - [ ] `dagger call build-quartz --src=. --version=vX.Y.Z` should work standalone Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/199 --- .dagger/src/blumeops_ci/main.py | 25 ++----------- .forgejo/workflows/build-blumeops.yaml | 35 +++++++++---------- .../eliminate-double-towncrier.infra.md | 1 + docs/how-to/update-documentation.md | 2 +- docs/reference/tools/dagger.md | 3 +- 5 files changed, 21 insertions(+), 45 deletions(-) create mode 100644 docs/changelog.d/eliminate-double-towncrier.infra.md diff --git a/.dagger/src/blumeops_ci/main.py b/.dagger/src/blumeops_ci/main.py index 8f2373f..5cd70e4 100644 --- a/.dagger/src/blumeops_ci/main.py +++ b/.dagger/src/blumeops_ci/main.py @@ -23,36 +23,15 @@ class BlumeopsCi: ref = f"{registry}/blumeops/{container_name}:{version}" return await ctr.publish(ref) - @function - async def build_changelog( - self, src: dagger.Directory, version: str - ) -> dagger.Directory: - """Run towncrier to build changelog, return modified source tree.""" - return await ( - dag.container() - .from_("python:3.12-slim") - .with_env_variable("TZ", "America/Los_Angeles") - # git is required because towncrier stages CHANGELOG.md via git add - .with_exec(["apt-get", "update", "-qq"]) - .with_exec(["apt-get", "install", "-y", "-qq", "git"]) - .with_exec(["pip", "install", "towncrier"]) - .with_directory("/workspace", src) - .with_workdir("/workspace") - .with_exec(["git", "init"]) - .with_exec(["towncrier", "build", "--version", version, "--yes"]) - .directory("/workspace") - ) - @function async def build_docs(self, src: dagger.Directory, version: str) -> dagger.File: - """Build changelog then Quartz site. Returns docs tarball.""" - updated_src = await self.build_changelog(src, version) + """Build Quartz docs site. Returns docs tarball.""" return await ( dag.container() .from_("node:22-slim") .with_exec(["apt-get", "update", "-qq"]) .with_exec(["apt-get", "install", "-y", "-qq", "git"]) - .with_directory("/workspace", updated_src) + .with_directory("/workspace", src) .with_workdir("/workspace") .with_exec( [ diff --git a/.forgejo/workflows/build-blumeops.yaml b/.forgejo/workflows/build-blumeops.yaml index dca745a..e771033 100644 --- a/.forgejo/workflows/build-blumeops.yaml +++ b/.forgejo/workflows/build-blumeops.yaml @@ -108,30 +108,14 @@ jobs: with: fetch-depth: 0 - - name: Build docs - run: | - VERSION="${{ steps.version.outputs.version }}" - TARBALL="docs-${VERSION}.tar.gz" - echo "Building docs via Dagger..." - # build-docs calls build_changelog internally (towncrier runs inside - # the Dagger container). The host working tree is not modified — only - # the tarball is exported. Towncrier runs a second time on the runner - # in the next step so that CHANGELOG.md and fragment deletion are - # captured in the git commit. - dagger call build-docs --src=. --version="$VERSION" \ - export --path="./$TARBALL" - echo "Build complete!" - ls -lh "$TARBALL" - - name: Build changelog id: changelog run: | VERSION="${{ steps.version.outputs.version }}" - # Run towncrier on the runner (not in Dagger) so that CHANGELOG.md - # updates and fragment deletions appear in the working tree for the - # git commit step. This is intentionally a second towncrier run — - # the first happened inside the Dagger build-docs container above. + # Run towncrier on the runner so that CHANGELOG.md updates and + # fragment deletions appear in the working tree for both the Quartz + # build (next step) and the git commit step. # Check if there are any changelog fragments FRAGMENTS=$(find docs/changelog.d -name "*.md" -not -name ".gitkeep" 2>/dev/null | wc -l) @@ -157,6 +141,19 @@ jobs: echo "" > /tmp/release_notes.md fi + - name: Build docs + run: | + VERSION="${{ steps.version.outputs.version }}" + TARBALL="docs-${VERSION}.tar.gz" + echo "Building docs via Dagger..." + # Towncrier already ran on the runner above, so the working tree + # has an up-to-date CHANGELOG.md. build-docs now only runs the + # Quartz static site build (no towncrier). + dagger call build-docs --src=. --version="$VERSION" \ + export --path="./$TARBALL" + echo "Build complete!" + ls -lh "$TARBALL" + - name: Create release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/changelog.d/eliminate-double-towncrier.infra.md b/docs/changelog.d/eliminate-double-towncrier.infra.md new file mode 100644 index 0000000..3c6524d --- /dev/null +++ b/docs/changelog.d/eliminate-double-towncrier.infra.md @@ -0,0 +1 @@ +Eliminate double towncrier run in release workflow — changelog is now built once on the runner, then the pre-processed source tree is passed to a new `build_quartz` Dagger function for the Quartz site build only. diff --git a/docs/how-to/update-documentation.md b/docs/how-to/update-documentation.md index e9991e6..4df624c 100644 --- a/docs/how-to/update-documentation.md +++ b/docs/how-to/update-documentation.md @@ -26,7 +26,7 @@ Direct link: https://forge.ops.eblu.me/eblume/blumeops/actions?workflow=build-bl The `build-blumeops` workflow (`.forgejo/workflows/build-blumeops.yaml`): 1. **Resolves version** — Uses input or auto-increments from latest release -2. **Builds changelog** — Calls `dagger call build-changelog` (towncrier in a container) +2. **Builds changelog** — Runs towncrier on the runner to update `CHANGELOG.md` 3. **Builds docs** — Calls `dagger call build-docs` (Quartz build in a container) 4. **Creates release** — Uploads `docs-.tar.gz` to Forgejo releases 5. **Updates deployment** — Edits `argocd/manifests/docs/deployment.yaml` with new URL diff --git a/docs/reference/tools/dagger.md b/docs/reference/tools/dagger.md index c78a706..a793d9b 100644 --- a/docs/reference/tools/dagger.md +++ b/docs/reference/tools/dagger.md @@ -27,8 +27,7 @@ Build engine for BlumeOps CI/CD pipelines. Replaces shell-based build scripts wi |----------|-----------|-------------| | `build` | `(src, container_name) → Container` | Build a container from `containers//Dockerfile` | | `publish` | `(src, container_name, version, registry?) → str` | Build and push to registry (default: `registry.ops.eblu.me`) | -| `build_changelog` | `(src, version) → Directory` | Run towncrier to collect changelog fragments | -| `build_docs` | `(src, version) → File` | Build changelog then Quartz site, return docs tarball | +| `build_docs` | `(src, version) → File` | Build Quartz docs site, return docs tarball | ## CLI Examples