name: "Loop: Release" # Generated by `micro loop init`. Cuts the next tag when the default branch has # new commits since the latest one, and pushes it with a PAT (<< .TokenSecret >>) # so any tag-triggered release workflow fires. The bump reflects what shipped, # read from the CHANGELOG [Unreleased] section: new features (Added/Changed) cut # a MINOR; fixes/docs only cut a PATCH; breaking changes are skipped so a MAJOR # stays a human decision. # # The tag MUST be pushed with a PAT, not the default GITHUB_TOKEN: a tag pushed # by GITHUB_TOKEN does not trigger other workflows (Actions blocks that recursion). on: workflow_dispatch: {} schedule: - cron: "<< .ReleaseCron >>" permissions: contents: read concurrency: group: loop-release cancel-in-progress: false jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # need full history + all tags # Do NOT persist the default GITHUB_TOKEN as a git credential: it would # be sent on the PAT push below and override it, so the tag push would # authenticate as github-actions[bot] and 403. Letting the PAT in the # push URL be the only credential is the whole point. persist-credentials: false - name: Cut the next patch tag if there are new commits env: RELEASE_TOKEN: ${{ secrets.<< .TokenSecret >> }} REPO: ${{ github.repository }} run: | if [ -z "$RELEASE_TOKEN" ]; then echo "<< .TokenSecret >> is not set — skipping." exit 0 fi git fetch --tags --force LATEST=$(git tag --list '<< .TagPrefix >>*.*.*' --sort=-v:refname | head -1) if [ -z "$LATEST" ]; then echo "no << .TagPrefix >>MAJOR.MINOR.PATCH tag found — aborting so nothing weird gets tagged." exit 1 fi echo "latest tag: $LATEST" COUNT=$(git rev-list --count "$LATEST"..HEAD) echo "commits since $LATEST: $COUNT" if [ "$COUNT" -eq 0 ]; then echo "no new commits since $LATEST — no release." exit 0 fi ver="${LATEST#<< .TagPrefix >>}" major="${ver%%.*}" rest="${ver#*.}" minor="${rest%%.*}" patch="${rest#*.}" case "$major.$minor.$patch" in [0-9]*.[0-9]*.[0-9]*) ;; *) echo "unexpected tag shape: $LATEST" ; exit 1 ;; esac # Choose the bump from what actually shipped, read from the CHANGELOG # [Unreleased] section (kept current by the coherence role): # new features (### Added / ### Changed) -> MINOR # fixes/docs only -> PATCH # breaking (### Removed / "(breaking)") -> skip; a major is a human call UNRELEASED="" if [ -f CHANGELOG.md ]; then UNRELEASED=$(awk '/^## \[Unreleased\]/{f=1; next} /^## \[/{f=0} f' CHANGELOG.md) fi if printf '%s\n' "$UNRELEASED" | grep -qiE '^### Removed|^### Changed \(breaking\)|BREAKING'; then echo "CHANGELOG [Unreleased] contains breaking changes — a major release is a human decision. Skipping." exit 0 elif printf '%s\n' "$UNRELEASED" | grep -qE '^### (Added|Changed)'; then NEXT="<< .TagPrefix >>${major}.$((minor + 1)).0" KIND="minor (new features)" else NEXT="<< .TagPrefix >>${major}.${minor}.$((patch + 1))" KIND="patch (fixes/docs only)" fi echo "cutting: $NEXT — $KIND ($COUNT commits since $LATEST)" git config user.name "loop release bot" git config user.email "noreply@users.noreply.github.com" git tag -a "$NEXT" -m "Release $NEXT — automated $KIND ($COUNT commits since $LATEST)" git push "https://x-access-token:${RELEASE_TOKEN}@github.com/${REPO}.git" "$NEXT" echo "Pushed $NEXT."