diff --git a/.github/workflows/release-pipeline.yml b/.github/workflows/release-pipeline.yml new file mode 100644 index 0000000..019590c --- /dev/null +++ b/.github/workflows/release-pipeline.yml @@ -0,0 +1,103 @@ +name: Release pipeline + +on: + workflow_call: + inputs: + version-command: + description: > + Override for version detection (e.g., `poetry version --short`). + Auto-detects from VERSION/package.json/pyproject.toml if empty. + type: string + default: "" + force_pr: + description: Force pull request creation + type: boolean + default: false + + outputs: + created-tag: + description: The tag if one was created (truthy means release shipped) + value: ${{ jobs.detect_release.outputs.created-tag }} + current-version: + description: The current version from detect_release + value: ${{ jobs.detect_release.outputs.current-version }} + +jobs: + detect_release: + name: Detect release + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - id: version-command + run: | + custom="${{ inputs.version-command }}" + if [ -n "$custom" ]; then + echo "cmd=$custom" >> $GITHUB_OUTPUT + elif [ -f package.json ]; then + echo "cmd=node -p \"require('./package.json').version\"" >> $GITHUB_OUTPUT + elif [ -f pyproject.toml ]; then + echo "cmd=grep -Po '(?<=^version = \")([^\"]+)' pyproject.toml" >> $GITHUB_OUTPUT + elif [ -f VERSION ]; then + echo "cmd=cat VERSION" >> $GITHUB_OUTPUT + else + echo "::error::No version source found. Set version-command input or add VERSION/package.json/pyproject.toml." + exit 1 + fi + shell: bash + + - id: tag + continue-on-error: true + uses: salsify/action-detect-and-tag-new-version@v2 + with: + version-command: ${{ steps.version-command.outputs.cmd }} + tag-annotation-template: | + chore(release): {VERSION} + + outputs: + created-tag: ${{ steps.tag.outputs.tag }} + current-version: ${{ steps.tag.outputs.current-version }} + + build_changelog: + name: Build changelog + runs-on: ubuntu-latest + needs: detect_release + if: "!needs.detect_release.outputs.created-tag" + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: changelog + uses: flowcanon/release-builder/build-changelog@v2 + + outputs: + has-prs: ${{ steps.changelog.outputs.has-prs }} + previous-version: ${{ steps.changelog.outputs.previous-version }} + next-version: ${{ steps.changelog.outputs.next-version }} + notes: ${{ steps.changelog.outputs.notes }} + release: ${{ steps.changelog.outputs.release }} + + create_pr: + if: inputs.force_pr || needs.build_changelog.outputs.has-prs + name: Create pull request + runs-on: ubuntu-latest + needs: build_changelog + + steps: + - uses: actions/checkout@v4 + + - uses: flowcanon/release-builder/package-version@v2 + with: + version: ${{ needs.build_changelog.outputs.next-version }} + + - uses: flowcanon/release-builder/pull-request@v2 + with: + next-version: ${{ needs.build_changelog.outputs.next-version }} + notes: ${{ needs.build_changelog.outputs.notes }} + previous-version: ${{ needs.build_changelog.outputs.previous-version }} + release: ${{ needs.build_changelog.outputs.release }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8005e55..9bd0e95 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,8 @@ jobs: build_changelog: name: Build changelog runs-on: ubuntu-latest + needs: detect_release + if: "!needs.detect_release.outputs.created-tag" steps: - uses: actions/checkout@v4 diff --git a/AGENTS.md b/AGENTS.md index 6f9149b..09d6bf0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,8 @@ Each directory contains an `action.yml` (the composite action definition) and su ## Release pipeline +**Downstream repos should use the reusable workflow** (`.github/workflows/release-pipeline.yml`) rather than assembling jobs from composite actions. The reusable workflow enforces correct job ordering (`detect_release → build_changelog → create_pr`) and prevents race conditions where `build_changelog` runs in parallel with `detect_release`. This repo's own dogfood workflow (`.github/workflows/release.yml`) can't use the reusable workflow because it references actions via `./` local paths, so it handles ordering with explicit `needs` and `if` conditions. + The actions are designed to run in this order: 1. **build-changelog** — Finds merged PRs since the last git tag, generates release notes, determines version bump type (major/minor/patch) from PR title keywords, calculates the next semantic version. diff --git a/README.md b/README.md index 19f3c8b..b617502 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,77 @@ Sends Slack notifications about release status. Can send an initial "pending" me --- +## Reusable Workflow (Recommended) + +The easiest way to use release-builder is with the reusable workflow. It encapsulates the full pipeline with correct job ordering, preventing race conditions between tag detection and changelog building. + +### Simple repo + +```yaml +name: Release + +on: + push: + branches: [main] + workflow_dispatch: + inputs: + force_pr: + description: Force pull request + type: boolean + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + release: + uses: flowcanon/release-builder/.github/workflows/release-pipeline.yml@v2 + with: + force_pr: ${{ inputs.force_pr || false }} +``` + +### With deploy step + +For repos that deploy on release, add a job that depends on the reusable workflow's outputs: + +```yaml +jobs: + release: + uses: flowcanon/release-builder/.github/workflows/release-pipeline.yml@v2 + with: + force_pr: ${{ inputs.force_pr || false }} + + deploy: + if: needs.release.outputs.created-tag + needs: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: ./script/deploy +``` + +### Inputs + +| Input | Type | Default | Description | +|-------|------|---------|-------------| +| `version-command` | string | `""` | Override for version detection (e.g., `poetry version --short`). Auto-detects from VERSION/package.json/pyproject.toml if empty. | +| `force_pr` | boolean | `false` | Force PR creation even if no PRs are found | + +### Outputs + +| Output | Description | +|--------|-------------| +| `created-tag` | The tag if one was created (truthy means a release shipped) | +| `current-version` | The current version from detect_release | + +### Why use the reusable workflow? + +When using composite actions directly, `detect_release` and `build_changelog` can run in parallel. If a PR title was renamed after the release PR was created, `build_changelog` may calculate a different version than what was tagged, opening a bogus release PR. The reusable workflow enforces `detect_release → build_changelog → create_pr` ordering, so `build_changelog` is skipped when a tag is created. + +## Composite Actions + +If you need more control (e.g., custom deploy steps between detection and changelog), you can use the composite actions directly. See the sections below. + ## Example This repository uses its own actions for releases. See [`.github/workflows/release.yml`](.github/workflows/release.yml) for a working implementation.