dsebastien_obsidian-graph-e.../.github/workflows/release.yml

219 lines
9.2 KiB
YAML

name: Release
# Two-phase release so the provenance attestation is bound to the tag commit:
# 1. prepare (workflow_dispatch without publish, on a branch): version bump +
# changelog + format, build as a gate, commit, tag that commit (lightweight,
# so the tag ref points directly at the commit), then re-dispatch this
# workflow at the tag ref with publish=true.
# 2. publish (workflow_dispatch with publish=true at a tag ref, or a manually
# pushed tag): github.sha IS the tagged commit, so the Sigstore certificate
# matches the commit the release tag points to. Builds, attests, releases.
#
# Recovery: if prepare fails after the tag was pushed (e.g. the re-dispatch
# call failed), run: gh workflow run release.yml --ref <version> -f version=<version> -f publish=true
# If prepare fails after the release commit was pushed but before tagging,
# just re-run prepare with the same version: it detects the existing
# "chore(release): <version>" commit at HEAD and skips straight to tagging.
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 1.2.3)'
required: true
type: string
publish:
description: 'Internal: build and publish at the current (tag) ref. Leave unchecked to start a release.'
required: false
type: boolean
default: false
push:
tags:
- '[0-9]*.[0-9]*.[0-9]*'
permissions:
contents: write
id-token: write
attestations: write
actions: write
concurrency:
group: release
cancel-in-progress: false
jobs:
prepare:
if: github.event_name == 'workflow_dispatch' && !inputs.publish && startsWith(github.ref, 'refs/heads/')
runs-on: ubuntu-slim
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Setup Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Determine version
id: version
env:
RAW_VERSION: ${{ inputs.version }}
run: |
# Strip 'v' prefix if present (Obsidian tags have no 'v' prefix)
VERSION="${RAW_VERSION#v}"
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid version '$VERSION' — expected SemVer without 'v' prefix (e.g., 1.2.3)" >&2
exit 1
fi
if git rev-parse "refs/tags/$VERSION" >/dev/null 2>&1 || git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
echo "Tag $VERSION already exists" >&2
exit 1
fi
# Idempotent re-run: a previous prepare may have pushed the
# release commit but failed before tagging. Skip the bump in
# that case to avoid duplicating the changelog entry.
if [ "$(git log -1 --format=%s)" = "chore(release): $VERSION" ]; then
echo "HEAD is already the release commit for $VERSION; skipping bump steps."
echo "prepared=true" >> $GITHUB_OUTPUT
else
echo "prepared=false" >> $GITHUB_OUTPUT
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Update package.json version
if: steps.version.outputs.prepared != 'true'
run: bun run release:update-version ${{ steps.version.outputs.version }}
- name: Update manifest and versions.json
if: steps.version.outputs.prepared != 'true'
run: |
export npm_package_version=${{ steps.version.outputs.version }}
bun run release:version-bump
- name: Generate changelog
if: steps.version.outputs.prepared != 'true'
run: bun run release:changelog
- name: Log changelog
run: |
echo "=== Changelog for ${{ steps.version.outputs.version }} ==="
head -100 CHANGELOG.md
- name: Format code
if: steps.version.outputs.prepared != 'true'
run: bun run format
- name: Build (release gate — do not tag a commit that cannot build)
run: |
bun run build
bun test
- name: Commit release changes
if: steps.version.outputs.prepared != 'true'
run: |
git add package.json versions.json manifest.json CHANGELOG.md docs/release-notes.md
git commit -m "chore(release): ${{ steps.version.outputs.version }}"
git push origin HEAD:${{ github.ref_name }}
- name: Tag release commit
# Lightweight tag on purpose: the tag ref then points directly at
# the commit, so GITHUB_SHA and the attestation subject are
# unambiguous on both publish trigger paths.
run: |
git tag ${{ steps.version.outputs.version }}
git push origin ${{ steps.version.outputs.version }}
- name: Dispatch publish run at the tag
# A tag pushed with GITHUB_TOKEN does not trigger the tag-push
# event (recursion guard), but workflow_dispatch is exempt.
run: |
VERSION=${{ steps.version.outputs.version }}
curl --fail-with-body -X POST \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/${{ github.repository }}/actions/workflows/release.yml/dispatches \
-d "{\"ref\":\"refs/tags/$VERSION\",\"inputs\":{\"version\":\"$VERSION\",\"publish\":\"true\"}}" \
|| {
echo "Dispatch failed. The tag is already pushed; recover with:" >&2
echo " gh workflow run release.yml --ref $VERSION -f version=$VERSION -f publish=true" >&2
exit 1
}
publish:
if: >-
(github.event_name == 'workflow_dispatch' && inputs.publish && startsWith(github.ref, 'refs/tags/')) ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-slim
steps:
- name: Checkout tag
uses: actions/checkout@v6
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Verify tag matches manifest version
id: version
run: |
VERSION="${GITHUB_REF_NAME#v}"
if [ "$GITHUB_REF_NAME" != "$VERSION" ]; then
echo "Tag '$GITHUB_REF_NAME' has a 'v' prefix — Obsidian release tags must be bare SemVer." >&2
exit 1
fi
MANIFEST_VERSION=$(bun -e 'console.log(require("./manifest.json").version)')
if [ "$VERSION" != "$MANIFEST_VERSION" ]; then
echo "Tag $VERSION does not match manifest.json version $MANIFEST_VERSION." >&2
echo "Tags must point at a release commit (run the prepare phase first)." >&2
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build for production
run: bun run build
- name: Extract changelog for release notes
id: changelog
run: |
# Extract the latest version section from CHANGELOG.md
NOTES=$(awk '/^## \[?[0-9]/{if(p) exit; p=1} p' CHANGELOG.md)
# Handle multi-line output
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Generate artifact attestations
uses: actions/attest-build-provenance@v3
with:
subject-path: |
dist/main.js
dist/manifest.json
dist/styles.css
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.version.outputs.version }}
name: ${{ steps.version.outputs.version }}
body: ${{ steps.changelog.outputs.notes }}
files: |
dist/main.js
dist/manifest.json
dist/styles.css
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}