Release #82
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*.*.*' | |
| - 'v*.*.*-*' | |
| - 'extensions/*/v*.*.*' | |
| - 'extensions/*/v*.*.*-*' | |
| workflow_dispatch: | |
| inputs: | |
| module: | |
| description: 'Module to release' | |
| required: true | |
| type: choice | |
| options: | |
| - forge | |
| - cli | |
| - all | |
| - auth | |
| - cache | |
| - consensus | |
| - discovery | |
| - events | |
| - features | |
| - graphql | |
| - grpc | |
| - hls | |
| - kafka | |
| - mcp | |
| - mqtt | |
| - orpc | |
| - queue | |
| - search | |
| - security | |
| - streaming | |
| - webrtc | |
| version: | |
| description: 'Semantic version to release (e.g. 1.2.3 or 1.0.0-beta.1)' | |
| required: true | |
| type: string | |
| dry_run: | |
| description: 'Dry run — validate everything but do not publish' | |
| required: false | |
| type: boolean | |
| default: false | |
| skip_tests: | |
| description: 'Skip tests (use for hotfixes when tests already passed)' | |
| required: false | |
| type: boolean | |
| default: false | |
| prerelease: | |
| description: 'Mark as a pre-release' | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| defaults: | |
| run: | |
| shell: bash | |
| # ------------------------------------------------------------------- | |
| # Job: detect | |
| # Normalises inputs from both tag-push and workflow_dispatch triggers | |
| # into a single set of outputs consumed by downstream jobs. | |
| # ------------------------------------------------------------------- | |
| jobs: | |
| detect: | |
| name: Detect release parameters | |
| runs-on: ubuntu-latest | |
| outputs: | |
| module_type: ${{ steps.resolve.outputs.module_type }} | |
| module_name: ${{ steps.resolve.outputs.module_name }} | |
| module_path: ${{ steps.resolve.outputs.module_path }} | |
| module_import: ${{ steps.resolve.outputs.module_import }} | |
| version: ${{ steps.resolve.outputs.version }} | |
| tag: ${{ steps.resolve.outputs.tag }} | |
| is_prerelease: ${{ steps.resolve.outputs.is_prerelease }} | |
| dry_run: ${{ steps.resolve.outputs.dry_run }} | |
| skip_tests: ${{ steps.resolve.outputs.skip_tests }} | |
| is_all: ${{ steps.resolve.outputs.is_all }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Resolve release parameters | |
| id: resolve | |
| run: | | |
| DRY_RUN="false" | |
| SKIP_TESTS="false" | |
| IS_ALL="false" | |
| # ---- workflow_dispatch path ---- | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| MODULE="${{ inputs.module }}" | |
| VERSION="${{ inputs.version }}" | |
| DRY_RUN="${{ inputs.dry_run }}" | |
| SKIP_TESTS="${{ inputs.skip_tests }}" | |
| IS_PRERELEASE="${{ inputs.prerelease }}" | |
| # Validate semver | |
| if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then | |
| echo "::error::Invalid semantic version: $VERSION (expected N.N.N or N.N.N-pre)" | |
| exit 1 | |
| fi | |
| if [ "$MODULE" = "all" ]; then | |
| # "all" releases the main module + CLI + every extension at the same version | |
| IS_ALL="true" | |
| MODULE_TYPE="main" | |
| MODULE_NAME="forge" | |
| MODULE_PATH="." | |
| MODULE_IMPORT="github.com/xraph/forge" | |
| TAG="v${VERSION}" | |
| elif [ "$MODULE" = "forge" ]; then | |
| MODULE_TYPE="main" | |
| MODULE_NAME="forge" | |
| MODULE_PATH="." | |
| MODULE_IMPORT="github.com/xraph/forge" | |
| TAG="v${VERSION}" | |
| elif [ "$MODULE" = "cli" ]; then | |
| # "cli" releases only the CLI binary (GoReleaser) — no Go module publish | |
| MODULE_TYPE="cli" | |
| MODULE_NAME="forge-cli" | |
| MODULE_PATH="cmd/forge" | |
| MODULE_IMPORT="github.com/xraph/forge/cmd/forge" | |
| TAG="v${VERSION}" | |
| else | |
| MODULE_TYPE="extension" | |
| MODULE_NAME="$MODULE" | |
| MODULE_PATH="extensions/$MODULE" | |
| MODULE_IMPORT="github.com/xraph/forge/extensions/$MODULE" | |
| TAG="extensions/${MODULE}/v${VERSION}" | |
| fi | |
| # Auto-detect prerelease from version string | |
| if [[ "$VERSION" == *"-"* ]]; then | |
| IS_PRERELEASE="true" | |
| fi | |
| # ---- tag push path ---- | |
| else | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| if [[ "$TAG" =~ ^extensions/([^/]+)/v(.+)$ ]]; then | |
| MODULE_TYPE="extension" | |
| MODULE_NAME="${BASH_REMATCH[1]}" | |
| VERSION="${BASH_REMATCH[2]}" | |
| MODULE_PATH="extensions/$MODULE_NAME" | |
| MODULE_IMPORT="github.com/xraph/forge/extensions/$MODULE_NAME" | |
| elif [[ "$TAG" =~ ^v(.+)$ ]]; then | |
| MODULE_TYPE="main" | |
| MODULE_NAME="forge" | |
| VERSION="${BASH_REMATCH[1]}" | |
| MODULE_PATH="." | |
| MODULE_IMPORT="github.com/xraph/forge" | |
| else | |
| echo "::error::Unrecognised tag format: $TAG" | |
| exit 1 | |
| fi | |
| if [[ "$VERSION" == *"-"* ]]; then | |
| IS_PRERELEASE="true" | |
| else | |
| IS_PRERELEASE="false" | |
| fi | |
| fi | |
| # Verify module directory exists | |
| if [ ! -f "$MODULE_PATH/go.mod" ]; then | |
| echo "::error::go.mod not found at $MODULE_PATH" | |
| exit 1 | |
| fi | |
| # Emit outputs | |
| { | |
| echo "module_type=$MODULE_TYPE" | |
| echo "module_name=$MODULE_NAME" | |
| echo "module_path=$MODULE_PATH" | |
| echo "module_import=$MODULE_IMPORT" | |
| echo "version=$VERSION" | |
| echo "tag=$TAG" | |
| echo "is_prerelease=$IS_PRERELEASE" | |
| echo "dry_run=$DRY_RUN" | |
| echo "skip_tests=$SKIP_TESTS" | |
| echo "is_all=$IS_ALL" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "### Release Parameters" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Key | Value |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|-----|-------|" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Module | \`$MODULE_NAME\` ($MODULE_TYPE) |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Version | \`$VERSION\` |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Tag | \`$TAG\` |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Import | \`$MODULE_IMPORT\` |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Pre-release | $IS_PRERELEASE |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Dry run | $DRY_RUN |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Skip tests | $SKIP_TESTS |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Release all | $IS_ALL |" >> "$GITHUB_STEP_SUMMARY" | |
| # For manual dispatch, create & push the main tag if it doesn't exist | |
| - name: Create tag (manual dispatch) | |
| if: github.event_name == 'workflow_dispatch' && steps.resolve.outputs.dry_run == 'false' | |
| run: | | |
| TAG="${{ steps.resolve.outputs.tag }}" | |
| if git rev-parse "$TAG" >/dev/null 2>&1; then | |
| echo "Tag $TAG already exists — skipping creation" | |
| else | |
| git tag "$TAG" | |
| git push origin "$TAG" | |
| echo "Created and pushed tag $TAG" | |
| fi | |
| # For "all" mode, also create tags for every extension module | |
| - name: Create extension tags (all mode) | |
| if: github.event_name == 'workflow_dispatch' && steps.resolve.outputs.is_all == 'true' && steps.resolve.outputs.dry_run == 'false' | |
| run: | | |
| VERSION="${{ steps.resolve.outputs.version }}" | |
| # Removed extensions are not listed: ai, cron, database, gateway and | |
| # storage are migration notes now, with no go.mod to tag. | |
| EXTENSIONS=(auth cache consensus discovery events features graphql grpc hls kafka mcp mqtt orpc queue search security streaming webrtc) | |
| for ext in "${EXTENSIONS[@]}"; do | |
| EXT_TAG="extensions/${ext}/v${VERSION}" | |
| if git rev-parse "$EXT_TAG" >/dev/null 2>&1; then | |
| echo "Tag $EXT_TAG already exists — skipping" | |
| else | |
| git tag "$EXT_TAG" | |
| echo "Created tag $EXT_TAG" | |
| fi | |
| done | |
| # Push all new tags in one go | |
| git push origin --tags | |
| echo "Pushed all extension tags" | |
| # ------------------------------------------------------------------- | |
| # Job: test | |
| # Runs tests for the module being released (skippable). | |
| # ------------------------------------------------------------------- | |
| test: | |
| name: Test (${{ matrix.os }}) | |
| needs: detect | |
| if: needs.detect.outputs.skip_tests == 'false' | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: ${{ needs.detect.outputs.module_path }}/go.mod | |
| check-latest: true | |
| - name: Cache Go modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: ${{ runner.os }}-go-${{ needs.detect.outputs.module_name }}-${{ hashFiles(format('{0}/go.sum', needs.detect.outputs.module_path)) }} | |
| restore-keys: | | |
| ${{ runner.os }}-go-${{ needs.detect.outputs.module_name }}- | |
| - name: Run tests | |
| run: | | |
| cd ${{ needs.detect.outputs.module_path }} | |
| go mod download | |
| PKGS=$(go list ./... | grep -v '/bk/') | |
| go test -v -race -timeout=15m $PKGS | |
| # ------------------------------------------------------------------- | |
| # Job: release-cli | |
| # Runs GoReleaser for main module releases (CLI binaries + Docker). | |
| # Uses the existing .goreleaser.yml without modifications. | |
| # ------------------------------------------------------------------- | |
| release-cli: | |
| name: Release CLI (GoReleaser) | |
| needs: [detect, test] | |
| if: | | |
| always() && | |
| (needs.detect.outputs.module_type == 'main' || needs.detect.outputs.module_type == 'cli') && | |
| needs.detect.outputs.dry_run == 'false' && | |
| needs.detect.result == 'success' && | |
| (needs.test.result == 'success' || needs.test.result == 'skipped') | |
| permissions: | |
| contents: write | |
| packages: write | |
| uses: xraph/workflows/.github/workflows/go-binary-release.yml@v1 | |
| with: | |
| # Read the version from go.mod rather than duplicating it here. | |
| go-version-file: go.mod | |
| # TEMPORARY: container images are skipped, so the CLI release can ship. | |
| # | |
| # go-binary-release.yml's `goreleaser` job declares `permissions: contents: | |
| # write` and nothing else, and once a permissions block exists everything | |
| # unlisted is `none` -- so its GITHUB_TOKEN has `packages: none`. The | |
| # called workflow's block is the authoritative one for its own jobs, so the | |
| # `packages: write` this job declares below cannot widen it. The push then | |
| # fails with | |
| # | |
| # denied: installation not allowed to Write organization package | |
| # | |
| # after a full six-minute build. Granting the repo write access on the | |
| # GHCR package does not help either: the token has no packages scope to | |
| # exercise in the first place. | |
| # | |
| # `docker: false` alone would make it worse, not better -- that input only | |
| # skips QEMU, Buildx and the registry *login*, while .goreleaser.yml still | |
| # has dockers/docker_manifests, so GoReleaser would build the images and | |
| # push them unauthenticated. The pipe has to be skipped in GoReleaser | |
| # itself, and then the setup steps are simply unnecessary. | |
| # | |
| # Revert both lines once xraph/workflows grants `packages: write` and its | |
| # v1 tag moves: https://github.com/xraph/workflows/pull/1 | |
| docker: false | |
| registry: ghcr.io | |
| goreleaser-version: '~> v2' | |
| goreleaser-args: 'release --clean --skip=docker' | |
| secrets: | |
| GORELEASER_TOKEN: ${{ secrets.GORELEASER_TOKEN }} | |
| FURY_TOKEN: ${{ secrets.FURY_TOKEN }} | |
| AUR_KEY: ${{ secrets.AUR_KEY }} | |
| # ------------------------------------------------------------------- | |
| # Job: release-module | |
| # Creates a GitHub release and notifies the Go proxy. | |
| # Runs for ALL release types (main & extension). | |
| # For main, this runs in addition to release-cli. | |
| # For extensions, this is the only release step. | |
| # ------------------------------------------------------------------- | |
| release-module: | |
| name: Publish Go module | |
| needs: [detect, test] | |
| if: | | |
| always() && | |
| needs.detect.outputs.module_type != 'cli' && | |
| needs.detect.outputs.dry_run == 'false' && | |
| needs.detect.result == 'success' && | |
| (needs.test.result == 'success' || needs.test.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| # Only create a GH release for extensions — GoReleaser handles main | |
| - name: Create GitHub Release (extensions) | |
| if: needs.detect.outputs.module_type == 'extension' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.detect.outputs.tag }} | |
| name: "${{ needs.detect.outputs.module_name }} v${{ needs.detect.outputs.version }}" | |
| body: | | |
| ## ${{ needs.detect.outputs.module_name }} v${{ needs.detect.outputs.version }} | |
| **Module**: `${{ needs.detect.outputs.module_import }}` | |
| ### Installation | |
| ```bash | |
| go get ${{ needs.detect.outputs.module_import }}@${{ needs.detect.outputs.tag }} | |
| ``` | |
| ### Documentation | |
| - [pkg.go.dev](https://pkg.go.dev/${{ needs.detect.outputs.module_import }}@${{ needs.detect.outputs.tag }}) | |
| draft: false | |
| prerelease: ${{ needs.detect.outputs.is_prerelease == 'true' }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Notify Go proxy | |
| run: | | |
| MODULE="${{ needs.detect.outputs.module_import }}" | |
| TAG="${{ needs.detect.outputs.tag }}" | |
| echo "Requesting Go proxy to cache $MODULE@$TAG" | |
| sleep 5 | |
| curl -sf "https://proxy.golang.org/${MODULE}/@v/${TAG}.info" || \ | |
| echo "Go proxy notification failed (normal for private repos)" | |
| # ------------------------------------------------------------------- | |
| # Job: dry-run | |
| # Validates the release pipeline without publishing anything. | |
| # ------------------------------------------------------------------- | |
| dry-run: | |
| name: Dry run | |
| needs: [detect, test] | |
| if: | | |
| always() && | |
| needs.detect.outputs.dry_run == 'true' && | |
| needs.detect.result == 'success' && | |
| (needs.test.result == 'success' || needs.test.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: ${{ needs.detect.outputs.module_path }}/go.mod | |
| - name: Validate module builds | |
| run: | | |
| set -euo pipefail | |
| MODULE_PATH="${{ needs.detect.outputs.module_path }}" | |
| # cmd/forge pins the last PUBLISHED github.com/xraph/forge, because its | |
| # go.mod has to stay free of replace directives for | |
| # `go install ...@latest` to accept it. Built standalone it therefore | |
| # compiles against the released root module rather than this tree, and | |
| # any root API this tag adds but has not published yet fails here as | |
| # "undefined" -- a dry run reporting a break the real release does not | |
| # have, since .goreleaser.yml's first before-hook sets up the same | |
| # workspace for the build itself. Every other module carries its own | |
| # replace directive and resolves the root locally without help. | |
| # | |
| # The workfile lives OUTSIDE the repo on purpose: a go.work at the root | |
| # would apply to every module, and the ones it does not list would fail | |
| # with "directory prefix ... does not contain modules listed in | |
| # go.work". Everything else is built with GOWORK=off so each module is | |
| # still checked standalone. | |
| case "$MODULE_PATH" in | |
| cmd/forge) | |
| WORKFILE="${RUNNER_TEMP:-/tmp}/forge-release.work" | |
| rm -f "$WORKFILE" | |
| GOWORK="$WORKFILE" go work init \ | |
| "$GITHUB_WORKSPACE" \ | |
| "$GITHUB_WORKSPACE/cmd/forge" | |
| cd "$MODULE_PATH" | |
| GOWORK="$WORKFILE" go build ./... | |
| ;; | |
| *) | |
| cd "$MODULE_PATH" | |
| GOWORK=off go build ./... | |
| ;; | |
| esac | |
| - name: GoReleaser dry run (main/cli only) | |
| if: needs.detect.outputs.module_type == 'main' || needs.detect.outputs.module_type == 'cli' | |
| uses: goreleaser/goreleaser-action@v6 | |
| with: | |
| distribution: goreleaser | |
| version: '~> v2' | |
| args: release --snapshot --clean | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Dry run summary | |
| run: | | |
| echo "### Dry Run Complete" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Module **${{ needs.detect.outputs.module_name }}** v${{ needs.detect.outputs.version }} validated successfully." >> "$GITHUB_STEP_SUMMARY" | |
| echo "No artifacts were published." >> "$GITHUB_STEP_SUMMARY" | |
| # ------------------------------------------------------------------- | |
| # Job: summary | |
| # Provides a unified release summary. | |
| # ------------------------------------------------------------------- | |
| summary: | |
| name: Release summary | |
| needs: [detect, test, release-cli, release-module, dry-run] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Generate summary | |
| run: | | |
| MODULE="${{ needs.detect.outputs.module_name }}" | |
| VERSION="${{ needs.detect.outputs.version }}" | |
| TAG="${{ needs.detect.outputs.tag }}" | |
| TYPE="${{ needs.detect.outputs.module_type }}" | |
| IMPORT="${{ needs.detect.outputs.module_import }}" | |
| DRY="${{ needs.detect.outputs.dry_run }}" | |
| { | |
| echo "# Release Summary" | |
| echo "" | |
| if [ "$DRY" = "true" ]; then | |
| echo "> **Dry run** — nothing was published." | |
| echo "" | |
| fi | |
| echo "| Key | Value |" | |
| echo "|-----|-------|" | |
| echo "| Module | \`$MODULE\` ($TYPE) |" | |
| echo "| Version | \`$VERSION\` |" | |
| echo "| Tag | \`$TAG\` |" | |
| if [ "$DRY" != "true" ]; then | |
| echo "" | |
| echo "## Installation" | |
| echo "" | |
| if [ "$TYPE" = "main" ] || [ "$TYPE" = "cli" ]; then | |
| echo '```bash' | |
| echo "go install github.com/xraph/forge/cmd/forge@${TAG}" | |
| echo '```' | |
| else | |
| echo '```bash' | |
| echo "go get ${IMPORT}@${TAG}" | |
| echo '```' | |
| fi | |
| fi | |
| echo "" | |
| echo "## Job Results" | |
| echo "" | |
| echo "| Job | Status |" | |
| echo "|-----|--------|" | |
| echo "| Detect | ${{ needs.detect.result }} |" | |
| echo "| Test | ${{ needs.test.result }} |" | |
| echo "| CLI Release | ${{ needs.release-cli.result }} |" | |
| echo "| Module Release | ${{ needs.release-module.result }} |" | |
| echo "| Dry Run | ${{ needs.dry-run.result }} |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # ------------------------------------------------------------------- | |
| # Job: update-changelog | |
| # Generates changelog from git log and commits to main branch. | |
| # Only runs for main module releases (not extensions or dry runs). | |
| # ------------------------------------------------------------------- | |
| update-changelog: | |
| name: Update CHANGELOG.md | |
| needs: [detect, test, release-cli, release-module] | |
| if: | | |
| always() && | |
| needs.detect.outputs.module_type == 'main' && | |
| needs.detect.outputs.dry_run == 'false' && | |
| needs.detect.result == 'success' && | |
| (needs.release-cli.result == 'success' || needs.release-cli.result == 'skipped') && | |
| (needs.release-module.result == 'success' || needs.release-module.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| ref: main | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate changelog entry | |
| run: | | |
| VERSION="${{ needs.detect.outputs.version }}" | |
| TAG="${{ needs.detect.outputs.tag }}" | |
| DATE=$(date -u +%Y-%m-%d) | |
| # Find previous tag for this module | |
| PREV_TAG=$(git tag --sort=-v:refname | grep -E '^v[0-9]' | awk -v t="$TAG" 'found{print;exit} $0==t{found=1}') | |
| if [ -z "$PREV_TAG" ]; then | |
| echo "No previous tag found — skipping changelog generation" | |
| echo "SKIP_CHANGELOG=true" >> "$GITHUB_ENV" | |
| exit 0 | |
| fi | |
| echo "Generating changelog for $TAG (since $PREV_TAG)" | |
| # Collect commits between tags (exclude extensions/ and docs/ directories) | |
| COMMITS=$(git log --oneline --no-merges "$PREV_TAG..$TAG" -- . ':!extensions' ':!docs' 2>/dev/null || true) | |
| if [ -z "$COMMITS" ]; then | |
| echo "No commits found — creating minimal entry" | |
| COMMITS="* Minor updates and improvements" | |
| fi | |
| # Categorise commits by conventional commit type | |
| FEATURES="" | |
| FIXES="" | |
| REFACTORS="" | |
| BREAKING="" | |
| MAINTENANCE="" | |
| while IFS= read -r line; do | |
| [ -z "$line" ] && continue | |
| HASH=$(echo "$line" | awk '{print $1}') | |
| MSG=$(echo "$line" | cut -d' ' -f2-) | |
| case "$MSG" in | |
| feat*) | |
| SCOPE=$(echo "$MSG" | sed -n 's/^feat(\([^)]*\)).*/\1/p') | |
| DESC=$(echo "$MSG" | sed 's/^feat([^)]*): //' | sed 's/^feat: //') | |
| if [ -n "$SCOPE" ]; then | |
| FEATURES="${FEATURES}* **${SCOPE}:** ${DESC} ([${HASH}](https://github.com/xraph/forge/commit/${HASH}))\n" | |
| else | |
| FEATURES="${FEATURES}* ${DESC} ([${HASH}](https://github.com/xraph/forge/commit/${HASH}))\n" | |
| fi | |
| ;; | |
| fix*) | |
| SCOPE=$(echo "$MSG" | sed -n 's/^fix(\([^)]*\)).*/\1/p') | |
| DESC=$(echo "$MSG" | sed 's/^fix([^)]*): //' | sed 's/^fix: //') | |
| if [ -n "$SCOPE" ]; then | |
| FIXES="${FIXES}* **${SCOPE}:** ${DESC} ([${HASH}](https://github.com/xraph/forge/commit/${HASH}))\n" | |
| else | |
| FIXES="${FIXES}* ${DESC} ([${HASH}](https://github.com/xraph/forge/commit/${HASH}))\n" | |
| fi | |
| ;; | |
| refactor*) | |
| SCOPE=$(echo "$MSG" | sed -n 's/^refactor(\([^)]*\)).*/\1/p') | |
| DESC=$(echo "$MSG" | sed 's/^refactor([^)]*): //' | sed 's/^refactor: //') | |
| if [ -n "$SCOPE" ]; then | |
| REFACTORS="${REFACTORS}* **${SCOPE}:** ${DESC} ([${HASH}](https://github.com/xraph/forge/commit/${HASH}))\n" | |
| else | |
| REFACTORS="${REFACTORS}* ${DESC} ([${HASH}](https://github.com/xraph/forge/commit/${HASH}))\n" | |
| fi | |
| ;; | |
| *"BREAKING"*|*"breaking"*) | |
| BREAKING="${BREAKING}* ${MSG}\n" | |
| ;; | |
| *) | |
| SCOPE=$(echo "$MSG" | sed -n 's/^[a-z]*(\([^)]*\)).*/\1/p') | |
| DESC=$(echo "$MSG" | sed 's/^[a-z]*([^)]*): //' | sed 's/^[a-z]*: //') | |
| if [ -n "$SCOPE" ]; then | |
| MAINTENANCE="${MAINTENANCE}* **${SCOPE}:** ${DESC} ([${HASH}](https://github.com/xraph/forge/commit/${HASH}))\n" | |
| else | |
| MAINTENANCE="${MAINTENANCE}* ${DESC} ([${HASH}](https://github.com/xraph/forge/commit/${HASH}))\n" | |
| fi | |
| ;; | |
| esac | |
| done <<< "$COMMITS" | |
| # Build the changelog entry | |
| ENTRY="## [${VERSION}](https://github.com/xraph/forge/compare/${PREV_TAG}...v${VERSION}) (${DATE})\n" | |
| if [ -n "$BREAKING" ]; then | |
| ENTRY="${ENTRY}\n\n### ⚠ BREAKING CHANGES\n\n${BREAKING}" | |
| fi | |
| if [ -n "$FEATURES" ]; then | |
| ENTRY="${ENTRY}\n\n### Features\n\n${FEATURES}" | |
| fi | |
| if [ -n "$FIXES" ]; then | |
| ENTRY="${ENTRY}\n\n### Bug Fixes\n\n${FIXES}" | |
| fi | |
| if [ -n "$REFACTORS" ]; then | |
| ENTRY="${ENTRY}\n\n### Refactoring\n\n${REFACTORS}" | |
| fi | |
| if [ -n "$MAINTENANCE" ]; then | |
| ENTRY="${ENTRY}\n\n### Maintenance\n\n${MAINTENANCE}" | |
| fi | |
| # Check if this version already exists in the changelog | |
| if grep -q "## \[${VERSION}\]" CHANGELOG.md; then | |
| echo "Version ${VERSION} already in CHANGELOG.md — skipping" | |
| echo "SKIP_CHANGELOG=true" >> "$GITHUB_ENV" | |
| exit 0 | |
| fi | |
| # Prepend to CHANGELOG.md (after the # Changelog header) | |
| TMPFILE=$(mktemp) | |
| echo "# Changelog" > "$TMPFILE" | |
| echo "" >> "$TMPFILE" | |
| printf "${ENTRY}\n" >> "$TMPFILE" | |
| # Append existing content without the header | |
| tail -n +3 CHANGELOG.md >> "$TMPFILE" | |
| mv "$TMPFILE" CHANGELOG.md | |
| echo "SKIP_CHANGELOG=false" >> "$GITHUB_ENV" | |
| echo "Changelog entry generated for v${VERSION}" | |
| - name: Commit and push changelog | |
| if: env.SKIP_CHANGELOG != 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add CHANGELOG.md | |
| git commit -m "docs(changelog): update CHANGELOG.md for v${{ needs.detect.outputs.version }}" | |
| git push origin main | |