Repository navigation
Scheduled monthly patch release #532
Description
Follow-up to #514: a scheduled job that cuts a patch release on the 1st of each month if main is green and anything has merged since the last tag.
How it could work
A new monthly-release.yml on schedule (06:00 UTC on the 1st) plus workflow_dispatch with a dry_run input:
- Find the last
v*tag and stop if nothing has merged since. - Stop if any check on
main's HEAD isn't finished and green. - Stop if a PR labelled
breakingwas merged since the last tag. That needs a minor release by hand. - Otherwise create the next patch tag through the API and start
release.ymlandpublish.ymlon it.
The catch: a tag created with GITHUB_TOKEN doesn't trigger other workflows, so pushing the tag alone wouldn't start the release. workflow_dispatch is exempt from that, and gh workflow run release.yml --ref v0.9.2 runs with github.ref set to the tag, so GoReleaser and docker/metadata-action behave as on a tag push. The only change to existing workflows would be adding workflow_dispatch to release.yml (publish.yml already has it). No extra secret needed. The alternative is pushing the tag with a GitHub App token, which leaves the workflows untouched but adds a secret.
Sketch
name: Monthly patch release
on:
schedule:
- cron: "0 6 1 * *"
workflow_dispatch:
inputs:
dry_run:
type: boolean
default: true
permissions: {}
concurrency:
group: monthly-release
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
actions: write
checks: read
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
DRY_RUN: ${{ inputs.dry_run || 'false' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
fetch-depth: 0
persist-credentials: false
- name: Decide whether to release
id: plan
run: |
set -euo pipefail
sha=$(git rev-parse HEAD)
last=$(git describe --tags --abbrev=0 --match 'v*')
if [ "$(git rev-list --count "$last..$sha")" -eq 0 ]; then
echo "Nothing merged since $last"; exit 0
fi
bad=$(gh api "repos/$REPO/commits/$sha/check-runs?per_page=100" \
--jq '[.check_runs[] | select(.status != "completed"
or (.conclusion | IN("success","skipped","neutral") | not))] | length')
if [ "$bad" -ne 0 ]; then
echo "::warning::main ($sha) is not green, skipping"; exit 0
fi
prs=$(gh pr list -R "$REPO" --state merged --base main \
--search "merged:>$(git log -1 --format=%cI "$last") label:breaking" \
--json number --jq 'length')
if [ "$prs" -ne 0 ]; then
echo "::warning::breaking change merged since $last, needs a manual minor release"; exit 0
fi
IFS=. read -r major minor patch <<<"${last#v}"
echo "tag=v$major.$minor.$((patch + 1))" >> "$GITHUB_OUTPUT"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Tag and start the release workflows
if: steps.plan.outputs.tag != ''
env:
TAG: ${{ steps.plan.outputs.tag }}
SHA: ${{ steps.plan.outputs.sha }}
run: |
set -euo pipefail
echo "Releasing $TAG at $SHA (dry run: $DRY_RUN)"
[ "$DRY_RUN" = "true" ] && exit 0
gh api "repos/$REPO/git/refs" -f ref="refs/tags/$TAG" -f sha="$SHA"
gh workflow run release.yml -R "$REPO" --ref "$TAG"
gh workflow run publish.yml -R "$REPO" --ref "$TAG"Open questions
- Is a
breakinglabel workable? If someone forgets it, a breaking change goes out as a patch. Make the Gradle build cache read-only by default #509 would have been one. - Should "green" mean every check on
main, or only theci.ymlrun? - Should a month with only Dependabot bumps get a release?
workflow_dispatchonrelease.ymlor a GitHub App token?
If this sounds right I'll open a PR, starting in dry-run mode.
Activity
Cut a patch release on the 1st of each month