diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ab4b4b8..d8e6c30 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -115,6 +115,7 @@ jobs:
filters: |
code:
- 'template-files/**'
+
- name: Comment on PR
if: github.event_name == 'pull_request' && steps.filter.outputs.code == 'true'
uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # v2.9.0
@@ -130,6 +131,43 @@ jobs:
${{ steps.templates-error.outputs.summary }}
GITHUB_TOKEN: ${{ secrets.SANDBOX_TEMPLATE_TOKEN }}
+ prepare-release:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Filter Changes
+ uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
+ id: filter
+ with:
+ filters: |
+ code:
+ - 'prepare-release/**'
+
+ - name: Checkout Source
+ if: steps.filter.outputs.code == 'true'
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Prepare Major Release
+ if: steps.filter.outputs.code == 'true'
+ uses: ./prepare-release
+ with:
+ repository: conda-sandbox/releases
+ version: ${{ github.event.pull_request.number }}.0.0
+ branch: ${{ github.event.pull_request.number }}.0.x
+ branch-sha: main
+ token: ${{ secrets.SANDBOX_RELEASES_TOKEN }}
+ fork-token: ${{ secrets.FORK_TOKEN }}
+
+ - name: Prepare Patch Release
+ if: steps.filter.outputs.code == 'true'
+ uses: ./prepare-release
+ with:
+ repository: conda-sandbox/releases
+ version: 0.0.${{ github.event.pull_request.number }}
+ branch: 0.0.x
+ branch-sha: main
+ token: ${{ secrets.SANDBOX_RELEASES_TOKEN }}
+ fork-token: ${{ secrets.FORK_TOKEN }}
+
# required check
analyze:
needs: [pytest, read-file, template-files]
diff --git a/prepare-release/README.md b/prepare-release/README.md
new file mode 100644
index 0000000..d3e5ee9
--- /dev/null
+++ b/prepare-release/README.md
@@ -0,0 +1,79 @@
+# Prepare Release
+
+This action prepares a release on GitHub:
+1. Running `towncrier` to update the changelog.
+
+## Action Inputs
+
+| Name | Description | Default |
+| ---- | ----------- | ------- |
+| `version` | Version to release. | **Required** |
+| `branch` | Target branch to use for the release. | `${{ github.even.repository.default_branch` |
+| `changelog-author` | Git-format author to use for the changelog commits. | @conda-bot |
+| `token` | GitHub token to create release branch if missing and the pull request.
Fine-grained PAT: `pull-request: write; contents: write` | `${{ github.token }}` |
+| `fork-token` | GitHub token to create and push to the fork/branch.
Fine-grained PAT: `administration: write; contents: write` | `${{ github.token }}` |
+
+## Sample Workflows
+
+### Basic Workflow
+
+```yaml
+name: Prepare Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: The version to release.
+ required: true
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ prepare:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Prepare Release
+ uses: conda/actions/prepare-release
+ with:
+ version: ${{ inputs.version }}
+```
+
+### Dynamic Branch Workflow
+
+```yaml
+name: Prepare Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: The version to release.
+ required: true
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ prepare:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Get Branch
+ shell: python
+ run: |
+ from os import environ
+ from pathlib import Path
+
+ # derive the branch from the version by dropping the `PATCH` and using `.x`
+ branch = "${{ inputs.version }}".rsplit(".", 1)[0]
+ Path(environ["GITHUB_ENV"]).write_text(f"BRANCH={branch}.x")
+
+ - name: Prepare Release
+ uses: conda/actions/prepare-release
+ with:
+ version: ${{ inputs.version }}
+ branch: ${{ env.BRANCH }}
+```
diff --git a/prepare-release/action.yml b/prepare-release/action.yml
new file mode 100644
index 0000000..1bcabf2
--- /dev/null
+++ b/prepare-release/action.yml
@@ -0,0 +1,129 @@
+name: Prepare Release
+description: Prepares a release by running towncrier and creating a PR with the changes.
+inputs:
+ version:
+ description: The version to release.
+ required: true
+ branch:
+ description: The target branch to use for the release.
+ default: ${{ github.event.repository.default_branch }}
+ branch-sha:
+ description: The SHA from which to create the target branch if missing.
+ default: ${{ github.event.repository.default_branch }}
+ changelog-author:
+ description: Git-format author to use for the changelog commits.
+ default: Conda Bot <18747875+conda-bot@users.noreply.github.com>
+ token:
+ description: |
+ GitHub token to create release branch if missing and the pull request.
+ Fine-grained PAT: `pull-request: write; contents: write`
+ default: ${{ github.token }}
+ fork-token:
+ description: |
+ GitHub token to create and push to the fork.
+ Fine-grained PAT: `administration: write; contents: write`
+ # default: ${{ inputs.token }}
+ comment-blurb:
+ description: Comment to add to the PR.
+ default: ✂️ snip snip ✂️ the making of a new release.
+ repository:
+ description: The repository to use for the release.
+ default: ${{ github.repository }}
+
+runs:
+ using: composite
+ steps:
+ - name: Checkout Source
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ fetch-depth: 0
+ path: .github_cache/actions/prepare-release/${{ inputs.repository }}
+ repository: ${{ inputs.repository }}
+ token: ${{ inputs.token }}
+
+ - name: Create Branch
+ shell: bash
+ run: |
+ if ! git -C ${SOURCE} fetch origin "${{ inputs.branch }}"; then
+ # if branch doesn't exist, create it from the default branch
+ gh api \
+ -X POST \
+ -H "Accept: application/vnd.github+json" \
+ "/repos/${{ inputs.repository }}/git/refs" \
+ -f ref="refs/heads/${{ inputs.branch }}" \
+ -f sha="$(git -C ${SOURCE} rev-parse ${{ inputs.branch-sha }})"
+ fi
+ git -C ${SOURCE} fetch origin
+ git -C ${SOURCE} checkout "${{ inputs.branch }}"
+ git -C ${SOURCE} pull origin "${{ inputs.branch }}"
+ env:
+ GH_TOKEN: ${{ inputs.token }}
+ SOURCE: .github_cache/actions/prepare-release/${{ inputs.repository }}
+
+ # `hashFiles` only works on files within the working directory, since `requirements.txt`
+ # is not in the working directory we need to manually compute the SHA256 hash
+ - name: Compute Hash
+ id: hash
+ shell: bash
+ run: echo hash=$(sha256sum ${{ github.action_path }}/requirements.txt | awk '{print $1}') >> $GITHUB_OUTPUT
+
+ - name: Pip Cache
+ uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
+ with:
+ path: ~/.cache/pip
+ key: ${{ github.workflow }}-prepare-release-${{ steps.hash.outputs.hash }}
+
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '>=3.9'
+
+ - name: Pip Install
+ shell: bash
+ run: pip install --quiet -r ${{ github.action_path }}/requirements.txt
+
+ - name: Pip List
+ shell: bash
+ run: pip list
+
+ - name: Run Towncrier
+ shell: bash
+ run: >-
+ towncrier build
+ --version=${{ inputs.version }}
+ --dir=.github_cache/actions/prepare-release/${{ inputs.repository }}
+
+ # - name: Detect Contributors
+
+ # - name: Detect First-Time Contributors
+
+ - name: Create Fork
+ id: create-fork
+ uses: conda/actions/create-fork@7873f9d7c90877290866eb893b8f6eff2e88429a # v25.1.2
+ with:
+ token: ${{ inputs.fork-token || inputs.token }}
+
+ - name: Create PR
+ uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
+ with:
+ # push to the fork
+ branch-token: ${{ inputs.fork-token || inputs.token }}
+ push-to-fork: ${{ steps.create-fork.outputs.fork }}
+ branch: changelog-${{ inputs.version }}
+ commit-message: Changelog ${{ inputs.version }}
+ author: ${{ inputs.changelog-author }}
+ committer: ${{ inputs.changelog-author }}
+ # create PR
+ token: ${{ inputs.token }}
+ base: ${{ inputs.branch }}
+ delete-branch: true
+ title: Changelog ${{ inputs.version }}
+ # GitHub flavored markdown reinvents how paragraphs work, adjoined lines of text are not
+ # concatenated so instead we rely on YAML multi-line + extra newlines
+ body: >-
+ ${{ inputs.comment-blurb }}
+
+
+ PR generated by ${{ github.html_url }}/actions/runs/${{ github.run_id }},
+ triggered by ${{ github.event_name == 'schedule' && 'cron' || format('@{0}', github.triggering_actor) }}
+ via ${{ github.event_name }}.
diff --git a/prepare-release/requirements.txt b/prepare-release/requirements.txt
new file mode 100644
index 0000000..0d2d2d7
--- /dev/null
+++ b/prepare-release/requirements.txt
@@ -0,0 +1 @@
+towncrier