From bcb6e01e58613da13126bd272baa6866cccbce54 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 19 Apr 2026 07:59:50 -0700 Subject: [PATCH] Split template build and release workflows --- .forgejo/workflows/build.yaml | 250 ++-------------- .forgejo/workflows/release.yaml | 277 ++++++++++++++++++ .../+forgejo-workflow-templates.infra.md | 1 + 3 files changed, 305 insertions(+), 223 deletions(-) create mode 100644 .forgejo/workflows/release.yaml create mode 100644 docs/changelog.d/+forgejo-workflow-templates.infra.md diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml index af9f8a8..f91cc0c 100644 --- a/.forgejo/workflows/build.yaml +++ b/.forgejo/workflows/build.yaml @@ -1,240 +1,44 @@ -# Release Workflow +# Build Workflow # -# Creates a versioned release with build artifacts. -# Currently includes: -# - Documentation site (Quartz static build) -# - Changelog (built from towncrier fragments) +# Generic CI validation for template-based repositories. +# By default this runs the repo's prek checks, which already cover: +# - workflow linting +# - docs integrity checks +# - formatting/linting hooks configured by the project # -# Usage: -# 1. Go to Actions > Build > Run workflow -# 2. Select version bump type (patch/minor/major) or choose specific version -# 3. The workflow creates a release with attached artifacts +# Projects can extend this with an optional executable hook at: +# .forgejo/scripts/build +# +# The optional hook is the place for project-specific validation such as: +# - unit/integration tests +# - package builds +# - schema validation +# - language-specific linters name: Build 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 + push: + branches: [main] + pull_request: + branches: [main] jobs: - build: + validate: runs-on: k8s steps: - - name: Resolve version - id: version - run: | - VERSION_TYPE="${{ inputs.version_type }}" - SPECIFIC_VERSION="${{ inputs.specific_version }}" - - # Fetch latest release - FORGE_URL="${{ github.server_url }}/api/v1/repos/${{ github.repository }}" - echo "Fetching latest release..." - LATEST=$(curl -s "${FORGE_URL}/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 - 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 "${FORGE_URL}/releases/tags/$VERSION" > /dev/null 2>&1; then - echo "Error: Release $VERSION already exists" - exit 1 - fi - - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "Building release: $VERSION" - - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 0 - - name: Build changelog - id: changelog + - name: Run repository checks run: | - VERSION="${{ steps.version.outputs.version }}" + prek run --all-files - 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 - 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" + - name: Run project-specific build hook + run: | + if [ -x .forgejo/scripts/build ]; then + echo "Running project-specific build hook..." + ./.forgejo/scripts/build else - echo "No changelog fragments found, skipping towncrier" - echo "changelog_updated=false" >> "$GITHUB_OUTPUT" - echo "" > /tmp/release_notes.md + echo "No .forgejo/scripts/build hook found; template validation complete." fi - - - name: Build docs - run: | - VERSION="${{ steps.version.outputs.version }}" - TARBALL="docs-${VERSION}.tar.gz" - echo "Building docs via Dagger..." - 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 }} - run: | - VERSION="${{ steps.version.outputs.version }}" - TARBALL="docs-${VERSION}.tar.gz" - CHANGELOG_UPDATED="${{ steps.changelog.outputs.changelog_updated }}" - - FORGE_URL="${{ github.server_url }}/api/v1/repos/${{ github.repository }}" - - echo "Creating release $VERSION..." - - { - echo "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\` for the documentation site build." - } > /tmp/release_body.txt - - RELEASE_DATA=$(jq -n \ - --arg tag "$VERSION" \ - --arg name "Release $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" \ - "${FORGE_URL}/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..." - curl -s \ - -X POST \ - -H "Content-Type: application/gzip" \ - -H "Authorization: token $GITHUB_TOKEN" \ - --data-binary "@$TARBALL" \ - "${FORGE_URL}/releases/$RELEASE_ID/assets?name=$TARBALL" - - echo "" - echo "Release created successfully!" - - - name: Commit changelog changes - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - VERSION="${{ steps.version.outputs.version }}" - CHANGELOG_UPDATED="${{ steps.changelog.outputs.changelog_updated }}" - - if [ "$CHANGELOG_UPDATED" != "true" ]; then - echo "No changelog changes to commit" - exit 0 - fi - - git config user.name "Forgejo Actions" - git config user.email "actions@forge.eblu.me" - - git add CHANGELOG.md docs/changelog.d/ - - if git diff --cached --quiet; then - echo "No changes to commit" - else - git commit -m "Update changelog for $VERSION [skip ci]" - git push origin HEAD:main - echo "Changelog changes committed and pushed" - fi - - - name: Summary - run: | - VERSION="${{ steps.version.outputs.version }}" - echo "================================================" - echo "Release: $VERSION" - echo "================================================" diff --git a/.forgejo/workflows/release.yaml b/.forgejo/workflows/release.yaml new file mode 100644 index 0000000..f3df47b --- /dev/null +++ b/.forgejo/workflows/release.yaml @@ -0,0 +1,277 @@ +# Release Workflow +# +# Creates a versioned Forgejo release for template-based repositories. +# By default this includes: +# - Documentation site bundle (Quartz static build via Dagger) +# - Changelog section built from towncrier fragments, when present +# +# Projects can optionally attach additional release artifacts by providing +# an executable hook at `.forgejo/scripts/release`. That hook should place +# any extra files under `release-assets/` for upload to the release. +# +# Usage: +# 1. Go to Actions > Release > Run workflow +# 2. Select version bump type (patch/minor/major) or choose specific version +# 3. The workflow creates a release with the docs bundle and optional extras + +name: Release + +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: + release: + runs-on: k8s + steps: + - name: Resolve version + id: version + run: | + VERSION_TYPE="${{ inputs.version_type }}" + SPECIFIC_VERSION="${{ inputs.specific_version }}" + + FORGE_URL="${{ github.server_url }}/api/v1/repos/${{ github.repository }}" + echo "Fetching latest release..." + LATEST=$(curl -s "${FORGE_URL}/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 + + 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 + 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 + + if curl -sf "${FORGE_URL}/releases/tags/$VERSION" > /dev/null 2>&1; then + echo "Error: Release $VERSION already exists" + exit 1 + fi + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Building release: $VERSION" + + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + + - name: Build changelog + id: changelog + run: | + VERSION="${{ steps.version.outputs.version }}" + + 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" + + 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: Build docs + run: | + VERSION="${{ steps.version.outputs.version }}" + TARBALL="docs-${VERSION}.tar.gz" + echo "Building docs via Dagger..." + dagger call build-docs --src=. --version="$VERSION" \ + export --path="./$TARBALL" + echo "Build complete!" + ls -lh "$TARBALL" + + - name: Build project-specific release artifacts + run: | + rm -rf release-assets + mkdir -p release-assets + + if [ -x .forgejo/scripts/release ]; then + echo "Running project-specific release hook..." + ./.forgejo/scripts/release "${{ steps.version.outputs.version }}" + else + echo "No .forgejo/scripts/release hook found; docs-only release." + fi + + echo "Release asset inventory:" + find release-assets -maxdepth 1 -type f -print | sort || true + + - 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 }}" + FORGE_URL="${{ github.server_url }}/api/v1/repos/${{ github.repository }}" + + echo "Creating release $VERSION..." + + { + echo "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 "## Assets" + echo "" + echo "- \`$TARBALL\` contains the static docs site." + + EXTRA_ASSET_COUNT=$(find release-assets -maxdepth 1 -type f | wc -l | tr -d ' ') + if [ "$EXTRA_ASSET_COUNT" -gt 0 ]; then + echo "- Additional project-specific artifacts are attached to this release." + fi + } > /tmp/release_body.txt + + RELEASE_DATA=$(jq -n \ + --arg tag "$VERSION" \ + --arg name "Release $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" \ + "${FORGE_URL}/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" + + echo "Uploading $TARBALL..." + curl -s \ + -X POST \ + -H "Content-Type: application/gzip" \ + -H "Authorization: token $GITHUB_TOKEN" \ + --data-binary "@$TARBALL" \ + "${FORGE_URL}/releases/$RELEASE_ID/assets?name=$TARBALL" + + for artifact in release-assets/*; do + if [ ! -f "$artifact" ]; then + continue + fi + + FILENAME=$(basename "$artifact") + echo "Uploading $FILENAME..." + curl -s \ + -X POST \ + -H "Content-Type: application/octet-stream" \ + -H "Authorization: token $GITHUB_TOKEN" \ + --data-binary "@$artifact" \ + "${FORGE_URL}/releases/$RELEASE_ID/assets?name=$FILENAME" + done + + echo "" + echo "Release created successfully!" + + - name: Commit changelog changes + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ steps.version.outputs.version }}" + CHANGELOG_UPDATED="${{ steps.changelog.outputs.changelog_updated }}" + + if [ "$CHANGELOG_UPDATED" != "true" ]; then + echo "No changelog changes to commit" + exit 0 + fi + + git config user.name "Forgejo Actions" + git config user.email "actions@forge.eblu.me" + + git add CHANGELOG.md docs/changelog.d/ + + if git diff --cached --quiet; then + echo "No changes to commit" + else + git commit -m "Update changelog for $VERSION [skip ci]" + git push origin HEAD:main + echo "Changelog changes committed and pushed" + fi + + - name: Summary + run: | + VERSION="${{ steps.version.outputs.version }}" + EXTRA_ASSET_COUNT=$(find release-assets -maxdepth 1 -type f | wc -l | tr -d ' ') + + echo "================================================" + echo "Release: $VERSION" + echo "================================================" + echo "Docs bundle: docs-${VERSION}.tar.gz" + echo "Extra project assets: $EXTRA_ASSET_COUNT" diff --git a/docs/changelog.d/+forgejo-workflow-templates.infra.md b/docs/changelog.d/+forgejo-workflow-templates.infra.md new file mode 100644 index 0000000..a67b6f3 --- /dev/null +++ b/docs/changelog.d/+forgejo-workflow-templates.infra.md @@ -0,0 +1 @@ +Split the template's Forgejo workflows into a generic CI `build` workflow and a docs-first `release` workflow, with optional hooks for project-specific validation and release artifacts.