Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions .github/workflows/propose-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
name: Propose the next release

# Writes the version bump, the lock sync, and the changelog section, then opens
# a pull request with them. It never merges, tags, or publishes.
#
# The release workflow refuses to guess a version: it requires pyproject.toml
# and CHANGELOG.md to already carry the exact version being published. This
# workflow produces that state as a reviewable diff, so the only manual step
# left is reading the proposal and merging it.
#
# The branch push uses the built-in token. The pull request is opened with the
# lifecycle App token, because a pull request opened with the built-in token
# does not start any further workflow run, and this proposal has to be tested
# before anybody merges it. The lifecycle App is never granted contents access,
# so it cannot land the change it proposes.

on:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: false

jobs:
reject-lifecycle-app:
permissions: {}
runs-on: ubuntu-latest
steps:
- name: Reject the lifecycle App
env:
ACTOR: ${{ github.actor }}
TRIGGERING_ACTOR: ${{ github.triggering_actor }}
run: |
test "$ACTOR" != 'openadapt-lifecycle[bot]'
test "$TRIGGERING_ACTOR" != 'openadapt-lifecycle[bot]'

propose-release:
needs: reject-lifecycle-app
if: >-
github.repository == 'OpenAdaptAI/openadapt-evals' &&
github.ref == 'refs/heads/main' &&
github.actor != 'openadapt-lifecycle[bot]' &&
github.triggering_actor != 'openadapt-lifecycle[bot]'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
steps:
- name: Checkout exact main
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ github.token }}

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.12'

- name: Plan the release
id: plan
run: |
set -euo pipefail
python scripts/plan_release.py | tee "$GITHUB_OUTPUT"

- name: Write the bump, the lock, and the changelog
if: steps.plan.outputs.released == 'true'
run: |
set -euo pipefail
python scripts/plan_release.py --write
python -m pip install --quiet uv==0.11.29
python scripts/verify_release_lock.py --write
python scripts/verify_release_lock.py
git diff --stat

- name: Push the proposal branch
if: steps.plan.outputs.released == 'true'
env:
NEXT: ${{ steps.plan.outputs.next }}
run: |
set -euo pipefail
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git checkout -B release/next
git add pyproject.toml uv.lock CHANGELOG.md
git commit -m "chore(release): prepare ${NEXT}"
git push --force-with-lease origin release/next

- name: Create the lifecycle App pull-request token
if: steps.plan.outputs.released == 'true'
id: lifecycle-app
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.OPENADAPT_LIFECYCLE_APP_ID }}
private-key: ${{ secrets.OPENADAPT_LIFECYCLE_APP_PRIVATE_KEY }}
owner: OpenAdaptAI
repositories: openadapt-evals
permission-pull-requests: write

- name: Verify the lifecycle App identity
if: steps.plan.outputs.released == 'true'
env:
ACTUAL_APP_SLUG: ${{ steps.lifecycle-app.outputs.app-slug }}
ACTUAL_INSTALLATION_ID: ${{ steps.lifecycle-app.outputs.installation-id }}
EXPECTED_INSTALLATION_ID: ${{ vars.OPENADAPT_LIFECYCLE_INSTALLATION_ID }}
run: |
set -euo pipefail
test "$ACTUAL_APP_SLUG" = 'openadapt-lifecycle'
test "$ACTUAL_INSTALLATION_ID" = "$EXPECTED_INSTALLATION_ID"

- name: Open or update the release pull request
if: steps.plan.outputs.released == 'true'
env:
GH_TOKEN: ${{ steps.lifecycle-app.outputs.token }}
NEXT: ${{ steps.plan.outputs.next }}
PREVIOUS: ${{ steps.plan.outputs.previous }}
COUNT: ${{ steps.plan.outputs.changes }}
run: |
set -euo pipefail
existing=$(gh pr list --head release/next --state open --json number --jq '.[0].number // empty')
body=$(printf '%s\n' \
"Prepares \`${NEXT}\`, up from \`${PREVIOUS}\`, across ${COUNT} commits." \
"" \
"Written by \`scripts/plan_release.py\` from the conventional commits since the last tag. Merging this does not publish anything. After it lands, dispatch **Release and publish** with version \`${NEXT}\` and the merge commit SHA." \
"" \
"Read the changelog section before merging. That is the whole point of this pull request.")
if [ -n "$existing" ]; then
gh pr edit "$existing" --title "chore(release): prepare ${NEXT}" --body "$body"
echo "updated PR #${existing}"
else
gh pr create \
--base main \
--head release/next \
--title "chore(release): prepare ${NEXT}" \
--body "$body"
fi

- name: Report a quiet run
if: steps.plan.outputs.released != 'true'
env:
STAGED: ${{ steps.plan.outputs.staged }}
run: |
set -euo pipefail
if [ "$STAGED" = 'true' ]; then
echo 'A release is already staged on main and is waiting to be published.'
else
echo 'No releasable change since the last tag.'
fi
Loading
Loading