blumeops/.forgejo/workflows/build-blumeops.yaml
Erich Blume e84ffb7d7f Set TZ on build-blumeops workflow job (#161)
## Summary

The runner pod's `TZ` env var (#159, #160) doesn't propagate to workflow job containers — jobs run inside Docker containers spawned by the DinD sidecar, not in the runner process itself. Set `TZ: America/Los_Angeles` at the job level so `uvx towncrier build` uses the correct timezone.

This is the actual fix for the Feb 12 changelog dates. The runner pod TZ is still useful for runner daemon logs but doesn't affect job execution.

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/161
2026-02-11 17:06:44 -08:00

334 lines
12 KiB
YAML

# BlumeOps Release Workflow
#
# Creates a versioned release of BlumeOps with all build artifacts.
# Currently includes:
# - Documentation site (Quartz static build)
# - Changelog (built from towncrier fragments)
#
# Usage:
# 1. Go to Actions > Build BlumeOps > Run workflow
# 2. Select version bump type (patch/minor/major) or choose specific version
# 3. The workflow creates a release with attached artifacts
#
# Documentation asset URL:
# https://forge.ops.eblu.me/eblume/blumeops/releases/download/<tag>/docs-<version>.tar.gz
name: Build BlumeOps
on:
workflow_dispatch:
inputs:
version_type:
description: 'Version bump type'
required: true
default: 'BUMP_PATCH'
type: choice
options:
- BUMP_PATCH
- BUMP_MINOR
- BUMP_MAJOR
- SPECIFIC_VERSION
specific_version:
description: 'Specific version (only used when version_type is SPECIFIC_VERSION, e.g., v1.2.0)'
required: false
default: ''
type: string
jobs:
build:
runs-on: k8s
env:
TZ: America/Los_Angeles
steps:
- name: Resolve version
id: version
run: |
VERSION_TYPE="${{ inputs.version_type }}"
SPECIFIC_VERSION="${{ inputs.specific_version }}"
# Fetch latest release
echo "Fetching latest release..."
LATEST=$(curl -s "https://forge.ops.eblu.me/api/v1/repos/eblume/blumeops/releases/latest" | jq -r '.tag_name // empty' || true)
if [ -z "$LATEST" ]; then
LATEST="v0.0.0"
echo "No previous releases found, using base version: $LATEST"
else
echo "Latest release: $LATEST"
fi
# Parse current version components (strip 'v' prefix)
CURRENT="${LATEST#v}"
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
MINOR=$(echo "$CURRENT" | cut -d. -f2)
PATCH=$(echo "$CURRENT" | cut -d. -f3)
case "$VERSION_TYPE" in
BUMP_MAJOR)
VERSION="v$((MAJOR + 1)).0.0"
echo "Bumping major: $LATEST -> $VERSION"
;;
BUMP_MINOR)
VERSION="v${MAJOR}.$((MINOR + 1)).0"
echo "Bumping minor: $LATEST -> $VERSION"
;;
BUMP_PATCH)
VERSION="v${MAJOR}.${MINOR}.$((PATCH + 1))"
echo "Bumping patch: $LATEST -> $VERSION"
;;
SPECIFIC_VERSION)
if [ -z "$SPECIFIC_VERSION" ]; then
echo "Error: specific_version is required when version_type is SPECIFIC_VERSION"
exit 1
fi
# Validate format
if [[ ! "$SPECIFIC_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Version must be in format vX.Y.Z (e.g., v1.0.0)"
exit 1
fi
VERSION="$SPECIFIC_VERSION"
echo "Using specific version: $VERSION"
;;
*)
echo "Error: Unknown version_type: $VERSION_TYPE"
exit 1
;;
esac
# Check if this version already exists
if curl -sf "https://forge.ops.eblu.me/api/v1/repos/eblume/blumeops/releases/tags/$VERSION" > /dev/null 2>&1; then
echo "Error: Release $VERSION already exists"
echo "See: https://forge.ops.eblu.me/eblume/blumeops/releases/tag/$VERSION"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Building BlumeOps release: $VERSION"
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Ensure Dagger CLI
run: |
# Bootstrap: install dagger if not already in the runner image.
# Remove once all runners include dagger (Phase 3).
if ! command -v dagger &>/dev/null; then
echo "Dagger not found, installing..."
curl -fsSL https://dl.dagger.io/dagger/install.sh | DAGGER_VERSION=0.19.11 sh
mv ./bin/dagger /usr/local/bin/dagger && rmdir ./bin
fi
dagger version
- 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.
# Check if there are any changelog fragments
FRAGMENTS=$(find docs/changelog.d -name "*.md" -not -name ".gitkeep" 2>/dev/null | wc -l)
if [ "$FRAGMENTS" -gt 0 ]; then
echo "Found $FRAGMENTS changelog fragments, building changelog..."
uvx towncrier build --version "$VERSION" --yes
echo "changelog_updated=true" >> "$GITHUB_OUTPUT"
# Extract the changelog section for this release to include in release body
RELEASE_NOTES=$(awk -v ver="$VERSION" '
/^## \[/ {
if (found) exit
if (index($0, "[" ver "]")) found=1
}
found {print}
' CHANGELOG.md | tail -n +2)
echo "$RELEASE_NOTES" > /tmp/release_notes.md
echo "Release notes extracted for $VERSION"
else
echo "No changelog fragments found, skipping towncrier"
echo "changelog_updated=false" >> "$GITHUB_OUTPUT"
echo "" > /tmp/release_notes.md
fi
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.version.outputs.version }}"
TARBALL="docs-${VERSION}.tar.gz"
CHANGELOG_UPDATED="${{ steps.changelog.outputs.changelog_updated }}"
echo "Creating release $VERSION..."
# Build release body with changelog if available
{
echo "BlumeOps release $VERSION"
echo ""
if [ "$CHANGELOG_UPDATED" = "true" ] && [ -s /tmp/release_notes.md ]; then
echo "## What's Changed"
echo ""
cat /tmp/release_notes.md
echo ""
fi
echo "## Documentation"
echo ""
echo "Download \`$TARBALL\` and configure the quartz container with:"
echo ""
echo "\`\`\`"
echo "DOCS_RELEASE_URL=https://forge.ops.eblu.me/eblume/blumeops/releases/download/$VERSION/$TARBALL"
echo "\`\`\`"
} > /tmp/release_body.txt
# Use jq to properly escape the body for JSON
RELEASE_DATA=$(jq -n \
--arg tag "$VERSION" \
--arg name "BlumeOps $VERSION" \
--rawfile body /tmp/release_body.txt \
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
RELEASE_RESPONSE=$(curl -s \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: token $GITHUB_TOKEN" \
-d "$RELEASE_DATA" \
"https://forge.ops.eblu.me/api/v1/repos/eblume/blumeops/releases")
echo "API Response: $RELEASE_RESPONSE"
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "Error: Failed to create release"
exit 1
fi
echo "Created release ID: $RELEASE_ID"
# Upload the asset
echo "Uploading $TARBALL..."
UPLOAD_RESPONSE=$(curl -s \
-X POST \
-H "Content-Type: application/gzip" \
-H "Authorization: token $GITHUB_TOKEN" \
--data-binary "@$TARBALL" \
"https://forge.ops.eblu.me/api/v1/repos/eblume/blumeops/releases/$RELEASE_ID/assets?name=$TARBALL")
echo "Upload Response: $UPLOAD_RESPONSE"
echo ""
echo "Release created successfully!"
- name: Update docs deployment
run: |
VERSION="${{ steps.version.outputs.version }}"
TARBALL="docs-${VERSION}.tar.gz"
DEPLOYMENT_FILE="argocd/manifests/docs/deployment.yaml"
RELEASE_URL="https://forge.ops.eblu.me/eblume/blumeops/releases/download/${VERSION}/${TARBALL}"
echo "Updating $DEPLOYMENT_FILE with new release URL..."
sed -i "s|value: \"https://forge.ops.eblu.me/eblume/blumeops/releases/download/[^\"]*\"|value: \"${RELEASE_URL}\"|" "$DEPLOYMENT_FILE"
echo "Updated deployment:"
grep -A1 "DOCS_RELEASE_URL" "$DEPLOYMENT_FILE"
- name: Commit release changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.version.outputs.version }}"
CHANGELOG_UPDATED="${{ steps.changelog.outputs.changelog_updated }}"
# Configure git
git config user.name "Forgejo Actions"
git config user.email "actions@forge.ops.eblu.me"
# Stage deployment changes
git add argocd/manifests/docs/deployment.yaml
# Stage changelog changes if updated
if [ "$CHANGELOG_UPDATED" = "true" ]; then
git add CHANGELOG.md docs/changelog.d/
fi
# Check if there are changes to commit
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "Update docs release to $VERSION
$([ "$CHANGELOG_UPDATED" = "true" ] && echo "- Built changelog from towncrier fragments")
[skip ci]"
# Push to main
git push origin HEAD:main
echo "Changes committed and pushed"
fi
- name: Deploy docs
env:
ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
run: |
echo "Syncing docs app via ArgoCD..."
# Sync docs app (uses ARGOCD_AUTH_TOKEN env var for auth)
argocd app sync docs \
--server argocd.ops.eblu.me \
--grpc-web \
--prune
# Wait for sync to complete
argocd app wait docs \
--server argocd.ops.eblu.me \
--grpc-web \
--timeout 120
echo "Docs app synced successfully!"
- name: Install flyctl
run: |
curl -L https://fly.io/install.sh | sh
echo "/root/.fly/bin" >> "$GITHUB_PATH"
- name: Purge Fly.io proxy cache
env:
FLY_API_TOKEN: ${{ secrets.FLY_DEPLOY_TOKEN }}
run: |
echo "Purging nginx cache on Fly.io proxy..."
fly ssh console -a blumeops-proxy -C "sh -c 'rm -rf /tmp/cache && nginx -s reload'"
echo "Cache purged"
- name: Summary
run: |
VERSION="${{ steps.version.outputs.version }}"
TARBALL="docs-${VERSION}.tar.gz"
echo "================================================"
echo "BlumeOps Release: $VERSION"
echo "================================================"
echo ""
echo "Release URL:"
echo " https://forge.ops.eblu.me/eblume/blumeops/releases/tag/$VERSION"
echo ""
echo "Asset URL (for DOCS_RELEASE_URL ConfigMap):"
echo " https://forge.ops.eblu.me/eblume/blumeops/releases/download/$VERSION/$TARBALL"