Skip to content

Changelog release stubs #3

Changelog release stubs

Changelog release stubs #3

name: Changelog release stubs
# Watches each source repo for a new GitHub release and opens a draft PR
# pre-filling a changelog <Update> stub for a human to turn into real copy.
# Poll-shaped like the sibling update-flox-version.yml / sync-man-pages.yml
# workflows: no changes or secrets needed in the source repos.
#
# Lifecycle of a stub PR:
# - merge it -> the changelog-id marker lands on a changelog page
# and the release is never stubbed again
# - close unmerged -> the release is declined; the PR-exists guard below
# stops it from being recreated
# - leave it open -> reruns skip it entirely (a human may be editing the
# branch, so it is never force-reset)
on:
schedule:
- cron: '0 8 * * *' # daily at 08:00 UTC (offset from the 06:00/07:00 sibling jobs)
workflow_dispatch:
jobs:
stub:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
repo:
- flox/flox
- flox/floxhub
- flox/floxenvs
- flox/flox-plugins
- flox/flox-skills
- flox/flox-vscode
env:
# FloxBot token, not GITHUB_TOKEN: it can read private source repos
# (e.g. floxhub) and lets the created PR trigger CI.
GH_TOKEN: ${{ secrets.MANAGED_FLOXBOT_GITHUB_ACCESS_TOKEN_REPO_SCOPE }}
steps:
- name: Checkout docs
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Check for a new release
id: release
env:
REPO: ${{ matrix.repo }}
run: |
err="$RUNNER_TEMP/gh-err.txt"
if ! release=$(gh api "repos/${REPO}/releases/latest" 2> "$err"); then
# Only "no releases yet" is a normal no-op; anything else (bad
# token, rate limit, outage) must fail loudly, or the automation
# dies silently while the workflow stays green. GitHub returns
# 404 for repos the token can't see too, so a 404 only counts as
# "no releases" when the repo itself is visible.
if grep -q "HTTP 404" "$err" && gh api "repos/${REPO}" --silent; then
echo "No releases in ${REPO}; nothing to do."
echo "new=false" >> "$GITHUB_OUTPUT"
exit 0
fi
cat "$err" >&2
exit 1
fi
tag=$(jq -r '.tag_name' <<< "$release")
# Each published entry is followed by an
# {/* changelog-id: owner/repo@tag */} marker, so the changelog
# pages themselves are the state — no state file.
# -F plus the closing " */" keep prefix tags (v1.0 vs v1.0.0) and
# regex dots from matching the wrong marker.
if grep -rqF "changelog-id: ${REPO}@${tag} */" changelog.mdx changelog/; then
echo "Entry for ${REPO}@${tag} already exists; nothing to do."
echo "new=false" >> "$GITHUB_OUTPUT"
exit 0
fi
{
echo "new=true"
echo "tag=${tag}"
echo "name=$(jq -r '.name // .tag_name' <<< "$release" | tr '\n' ' ' | sed 's/ *$//')"
echo "url=$(jq -r '.html_url' <<< "$release")"
echo "date=$(jq -r '.published_at[:10]' <<< "$release")"
} >> "$GITHUB_OUTPUT"
- name: Slugify branch name
if: steps.release.outputs.new == 'true'
id: slug
env:
# Passed via env, not inline ${{ }}, so tag names can't inject into
# the shell (tags come from the source repo's release API).
REPO: ${{ matrix.repo }}
TAG: ${{ steps.release.outputs.tag }}
run: |
slug=$(printf '%s' "${REPO}-${TAG}" | tr '/' '-' | tr -cd '[:alnum:]._-')
echo "slug=${slug}" >> "$GITHUB_OUTPUT"
- name: Determine mode
if: steps.release.outputs.new == 'true'
id: mode
env:
DATE: ${{ steps.release.outputs.date }}
SLUG: ${{ steps.slug.outputs.slug }}
run: |
# A release dated after changelog.mdx's year needs a year rollover
# first. Rollover ships as its own mechanical PR on a fixed branch:
# every new-year release produces the identical rollover, so
# concurrent matrix jobs converge on one PR instead of conflicting,
# and the release stubs follow on the next run after it merges.
page_year=$(sed -n 's/^title: "\([0-9][0-9][0-9][0-9]\)"$/\1/p' changelog.mdx | head -1)
if [ -z "$page_year" ]; then
echo 'error: could not read the year from the changelog.mdx title' >&2
exit 1
fi
rel_year=${DATE%%-*}
if [ "$rel_year" -gt "$page_year" ]; then
echo "mode=rollover" >> "$GITHUB_OUTPUT"
echo "branch=changelog-rollover-${rel_year}" >> "$GITHUB_OUTPUT"
echo "year=${rel_year}" >> "$GITHUB_OUTPUT"
else
echo "mode=stub" >> "$GITHUB_OUTPUT"
echo "branch=changelog-${SLUG}" >> "$GITHUB_OUTPUT"
fi
- name: Skip if handled by an existing PR
if: steps.release.outputs.new == 'true'
id: existing
env:
BRANCH: ${{ steps.mode.outputs.branch }}
MODE: ${{ steps.mode.outputs.mode }}
run: |
# Stub PRs: any prior PR stops recreation (open = a human may have
# pushed real copy, never regenerate over it; closed-unmerged = the
# release was declined). Rollover PRs carry no hand-written copy,
# so an OPEN one is refreshed daily from main — keeping its archive
# snapshot current — and only a closed-unmerged one (an explicit
# decline) blocks.
state="all"
[ "$MODE" = "rollover" ] && state="closed"
prs=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$BRANCH" \
--state "$state" --json number,title,state)
count=$(jq 'length' <<< "$prs")
echo "count=${count}" >> "$GITHUB_OUTPUT"
if [ "$count" -gt 0 ]; then
echo "A ${state} PR for ${BRANCH} already exists; skipping:"
jq -r '.[] | " #\(.number) [\(.state)] \(.title)"' <<< "$prs"
fi
- name: Insert changelog stub
if: steps.release.outputs.new == 'true' && steps.existing.outputs.count == '0'
env:
REPO: ${{ matrix.repo }}
TAG: ${{ steps.release.outputs.tag }}
NAME: ${{ steps.release.outputs.name }}
URL: ${{ steps.release.outputs.url }}
DATE: ${{ steps.release.outputs.date }}
run: |
./scripts/changelog-stub.sh "$REPO" "$TAG" "$NAME" "$URL" "$DATE"
- name: Create draft pull request
if: steps.release.outputs.new == 'true' && steps.existing.outputs.count == '0' && steps.mode.outputs.mode == 'stub'
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
with:
token: "${{ secrets.MANAGED_FLOXBOT_GITHUB_ACCESS_TOKEN_REPO_SCOPE }}"
add-paths: |
changelog.mdx
changelog
docs.json
commit-message: "docs(changelog): stub for ${{ matrix.repo }} ${{ steps.release.outputs.tag }}"
committer: "FloxBot <bot@flox.dev>"
author: "FloxBot <bot@flox.dev>"
branch: ${{ steps.mode.outputs.branch }}
delete-branch: true
draft: true
title: "docs(changelog): ${{ matrix.repo }} ${{ steps.release.outputs.tag }}"
body: |
[${{ matrix.repo }} ${{ steps.release.outputs.tag }}](${{ steps.release.outputs.url }}) was published on ${{ steps.release.outputs.date }}.
This draft PR pre-fills a changelog stub — replace the placeholder
with real release copy, then mark the PR ready for review. If this
release doesn't merit a changelog entry, close the PR and it won't
be recreated.
Created automatically by the [Changelog release stubs workflow](https://github.com/flox/docs/actions/workflows/changelog-stubs.yml).
labels: "team-developer-support"
- name: Create rollover pull request
if: steps.release.outputs.new == 'true' && steps.existing.outputs.count == '0' && steps.mode.outputs.mode == 'rollover'
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
with:
token: "${{ secrets.MANAGED_FLOXBOT_GITHUB_ACCESS_TOKEN_REPO_SCOPE }}"
add-paths: |
changelog.mdx
changelog
docs.json
commit-message: "docs(changelog): open the ${{ steps.mode.outputs.year }} changelog"
committer: "FloxBot <bot@flox.dev>"
author: "FloxBot <bot@flox.dev>"
branch: ${{ steps.mode.outputs.branch }}
delete-branch: true
title: "docs(changelog): open the ${{ steps.mode.outputs.year }} changelog"
body: |
The first release of ${{ steps.mode.outputs.year }} landed
([${{ matrix.repo }} ${{ steps.release.outputs.tag }}](${{ steps.release.outputs.url }})),
so the changelog rolls over: the previous year's entries move to
their archive page and `changelog.mdx` — the stable `/changelog`
URL and its RSS feed — resets for the new year.
This PR is fully mechanical and refreshed from `main` daily while
open, so don't push manual edits here. Draft PRs for the new
year's releases follow automatically once it merges. Closing it
unmerged blocks automatic rollover until the workflow is run by
hand.
Created automatically by the [Changelog release stubs workflow](https://github.com/flox/docs/actions/workflows/changelog-stubs.yml).
labels: "team-developer-support"