name: Release Plan
on:
workflow_dispatch:
inputs:
mode:
description: "Plan (preview only) or Release (execute bumps)"
required: true
default: "plan"
type: choice
options:
- plan
- release
package:
description: "Package to release (empty = all with changes)"
required: false
type: choice
options:
- ""
- mmdflux
- mmds-core
- mmds-excalidraw
- mmds-tldraw
permissions:
contents: write
actions: write
concurrency:
group: release-plan
cancel-in-progress: false
jobs:
plan:
name: Release Plan
if: inputs.mode == 'plan'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install cargo-binstall
uses: cargo-bins/cargo-binstall@main
- name: Install cocogitto
run: cargo binstall --no-confirm cocogitto@6.5.0
- name: Generate release plan
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
PLAN_FILE="${RUNNER_TEMP}/release-plan.md"
# Write to both step summary and artifact file
out() { tee -a "$PLAN_FILE" >> "$GITHUB_STEP_SUMMARY"; }
# Package definitions: name:tag_prefix
PACKAGES="mmdflux:mmdflux-v mmds-core:mmds-core-v mmds-excalidraw:mmds-excalidraw-v mmds-tldraw:mmds-tldraw-v"
HEAD_SHA=$(git rev-parse HEAD)
HEAD_SHORT=$(git rev-parse --short HEAD)
REPO="${GITHUB_REPOSITORY}"
BRANCH="${GITHUB_REF_NAME}"
# --- Header ---
{
echo "# Release Plan"
echo ""
echo "## Branch Status"
echo ""
echo "**Commit:** [\`${HEAD_SHORT}\`](https://github.com/${REPO}/commit/${HEAD_SHA}) on \`${BRANCH}\`"
echo ""
echo "### CI Workflows"
echo ""
echo "| Workflow | Status | Commit |"
echo "|----------|--------|--------|"
} | out
# Show only the most recent run per workflow name
gh run list --branch "${BRANCH}" --limit 20 \
--json name,conclusion,headSha,status \
--jq '[group_by(.name)[] | sort_by(.headSha) | last] | sort_by(.name)[] | "| \(.name) | \(if .conclusion != "" and .conclusion != null then .conclusion else .status end) | `\(.headSha[0:7])` |"' \
| out
# --- Package Reports ---
{
echo ""
echo "## Packages"
echo ""
} | out
bump_commands=""
for entry in $PACKAGES; do
pkg="${entry%%:*}"
tag_prefix="${entry##*:}"
{
echo "### ${pkg}"
echo ""
} | out
# Find latest tag for this package that is an ancestor of HEAD
latest_tag=$(git describe --tags --match "${tag_prefix}*" --abbrev=0 2>/dev/null || echo "")
if [ -z "$latest_tag" ]; then
{
echo "No previous release tag found."
echo ""
} | out
continue
fi
current_version="${latest_tag#"${tag_prefix}"}"
# Dry-run to check for a pending bump
dry_output=$(cog bump --package "$pkg" --auto --dry-run --skip-untracked 2>&1 || true)
if echo "$dry_output" | grep -q "No conventional commits"; then
{
echo "**Current version:** ${current_version}"
echo ""
echo "No unreleased changes."
echo ""
} | out
continue
fi
# Extract next version from dry-run output
next_version=$(echo "$dry_output" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | tail -1 || echo "unknown")
{
echo "**Current version:** ${current_version} **-->** ${next_version}"
echo ""
} | out
bump_commands="${bump_commands}cog bump --package ${pkg} --auto\n"
# Changelog preview
changelog=$(cog changelog "${latest_tag}..HEAD" 2>&1 || echo "(changelog generation failed)")
if [ -n "$changelog" ]; then
{
echo "<details>"
echo "<summary>Changelog preview</summary>"
echo ""
echo "$changelog"
echo ""
echo "</details>"
echo ""
} | out
fi
done
# --- Next Steps ---
{
echo "---"
echo ""
} | out
if [ -n "$bump_commands" ]; then
{
echo "## Next Steps"
echo ""
echo "To execute this release, re-run this workflow with **mode: release**."
echo ""
echo "Or run locally:"
echo ""
echo '```bash'
echo -e "$bump_commands"
echo '```'
} | out
else
echo "No packages have unreleased changes." | out
fi
- name: Upload release plan
uses: actions/upload-artifact@v5
with:
name: release-plan
path: ${{ runner.temp }}/release-plan.md
retention-days: 90
release:
name: Execute Release
if: inputs.mode == 'release'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-binstall
uses: cargo-bins/cargo-binstall@main
- name: Install tools
run: cargo binstall --no-confirm cocogitto@6.5.0 cargo-edit
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
git_user_signingkey: true
git_commit_gpgsign: true
git_config_global: true
git_tag_gpgsign: true
git_committer_name: "Kevin Swiber"
git_committer_email: "kswiber@gmail.com"
- name: Execute release
env:
GH_TOKEN: ${{ github.token }}
INPUT_PACKAGE: ${{ inputs.package }}
run: |
set -euo pipefail
PLAN_FILE="${RUNNER_TEMP}/release-plan.md"
out() { tee -a "$PLAN_FILE" >> "$GITHUB_STEP_SUMMARY"; }
# The CI bump profile (defined in cog.toml) skips lint/test and
# handles git push + workflow dispatch via per-package post_bump_hooks.
if [ -n "${INPUT_PACKAGE}" ]; then
cog bump --package "${INPUT_PACKAGE}" --auto --hook-profile ci
else
cog bump --auto --hook-profile ci
fi
{
echo "# Release Complete"
echo ""
echo "Release executed via \`cog bump --auto --hook-profile ci\`."
echo "See workflow logs for details on which packages were bumped and which workflows were dispatched."
} | out
- name: Upload release report
if: always()
uses: actions/upload-artifact@v5
with:
name: release-plan
path: ${{ runner.temp }}/release-plan.md
retention-days: 90