Repository navigation
fix(ci): run every documented example falsifier in smoke (#443) #431
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: {} | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| jobs: | |
| gate: | |
| name: Gate on CI evidence | |
| # A push to main must not publish unless Validate passed on this exact SHA and, | |
| # when it was merged from a PR, every check on that PR passed. A direct push | |
| # also waits for its own Blender Smoke push run, unless it changed only | |
| # smoke-ignored paths (see release-gate.sh). | |
| runs-on: ubuntu-latest | |
| if: "!contains(github.event.head_commit.message, '[skip ci]')" | |
| permissions: | |
| contents: read | |
| actions: read | |
| checks: read | |
| pull-requests: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| sparse-checkout: .github/scripts | |
| - name: Require green Validate and PR checks | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| SHA: ${{ github.sha }} | |
| BEFORE: ${{ github.event.before }} | |
| run: bash .github/scripts/release-gate.sh | |
| version-and-release: | |
| name: Bump version, tag, and release | |
| needs: gate | |
| runs-on: ubuntu-latest | |
| if: "!contains(github.event.head_commit.message, '[skip ci]')" | |
| permissions: | |
| contents: write | |
| actions: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Get current version | |
| id: current | |
| run: | | |
| version=$(cat VERSION | tr -d '[:space:]') | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "Current version: $version" | |
| - name: Determine bump type from commits | |
| id: bump | |
| run: | | |
| last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| range="${last_tag:+$last_tag..}HEAD" | |
| echo "Commits since last release:" | |
| git log "$range" --format="%s" | |
| # Release ONLY when a commit since the last tag is release-worthy: | |
| # feat/fix subjects, `!` breaking subjects, or a `BREAKING CHANGE:` body | |
| # footer. docs/chore/ci/refactor/test/style/build/perf land WITHOUT | |
| # cutting a release; a mixed push (docs + a real fix) still releases. | |
| # .github/scripts/bump-kind.sh owns the patterns (pages.yml shares it). | |
| # [skip ci] (guarded on the job) remains an optional belt-and-suspenders override. | |
| kind=$(git log "$range" --format='%B%x00' | bash .github/scripts/bump-kind.sh) | |
| bump="patch" | |
| release="false" | |
| if [ -z "$last_tag" ]; then | |
| # Initial release: no prior tag -> cut the first release at the current VERSION. | |
| release="true" | |
| elif [ "$kind" != "none" ]; then | |
| bump="$kind"; release="true" | |
| elif bash .github/scripts/plugin-content-changed.sh "$last_tag"; then | |
| # No feat/fix subject, but skills/rules/snippets/templates/claude | |
| # changed: ship a patch so a docs: correction still reaches plugin | |
| # users instead of waiting for the next feat/fix (#350). | |
| echo "Plugin content changed since $last_tag with no feat/fix subject; releasing a patch" | |
| bump="patch"; release="true" | |
| fi | |
| echo "bump=$bump" >> "$GITHUB_OUTPUT" | |
| echo "release=$release" >> "$GITHUB_OUTPUT" | |
| if [ "$release" = "true" ]; then | |
| echo "Release decision: RELEASE (bump=$bump)" | |
| else | |
| echo "Release decision: SKIP - no feat:/fix:/feat!: commit since ${last_tag:-<none>}; docs/chore/ci do not cut a release" | |
| fi | |
| - name: Compute new version | |
| id: new | |
| run: | | |
| current="${{ steps.current.outputs.version }}" | |
| bump="${{ steps.bump.outputs.bump }}" | |
| IFS='.' read -r major minor patch <<< "$current" | |
| # Initial release: VERSION already at 0.1.0 with no prior tag. | |
| # If there is no last tag and the bump type would otherwise advance | |
| # past 0.1.0, hold the version at 0.1.0 so the first release is | |
| # cut at the value that's already in VERSION. | |
| last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| if [ -z "$last_tag" ]; then | |
| new_version="$current" | |
| else | |
| case "$bump" in | |
| major) major=$((major + 1)); minor=0; patch=0 ;; | |
| minor) minor=$((minor + 1)); patch=0 ;; | |
| patch) patch=$((patch + 1)) ;; | |
| esac | |
| new_version="$major.$minor.$patch" | |
| fi | |
| echo "version=$new_version" >> "$GITHUB_OUTPUT" | |
| echo "New version: $new_version" | |
| - name: Check if tag already exists | |
| id: check | |
| run: | | |
| new_version="${{ steps.new.outputs.version }}" | |
| if git rev-parse "v$new_version" >/dev/null 2>&1; then | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| echo "Tag v$new_version already exists, skipping" | |
| else | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Update VERSION file | |
| if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' | |
| env: | |
| NEW_VERSION: ${{ steps.new.outputs.version }} | |
| run: | | |
| echo "$NEW_VERSION" > VERSION | |
| - name: Sync release docs | |
| if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' | |
| uses: TMHSDigital/Developer-Tools-Directory/.github/actions/release-doc-sync@7886cbe6ef93d57cc73c020b98268fa0dc0bdc98 # v1 | |
| with: | |
| plugin-version: ${{ steps.new.outputs.version }} | |
| previous-version: ${{ steps.current.outputs.version }} | |
| # A SHA, not a tag: this job holds contents:write and runs the | |
| # meta-repo's Python at this ref. 9be79799 is tag v1.15.1. | |
| meta-repo-ref: 9be79799df00ed42a0c4cff39e74a383a493296c | |
| - name: Write release notes (CHANGELOG entry and release body) | |
| # release-doc-sync prepends a "See release notes ... for details." stub; | |
| # replace it with the feat/fix/other subjects since the previous tag, | |
| # and keep the same list for the GitHub release body. | |
| if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' | |
| env: | |
| NEW_VERSION: ${{ steps.new.outputs.version }} | |
| run: | | |
| last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| python3 .github/scripts/release_notes.py ${last_tag:+--since "$last_tag"} \ | |
| --version "$NEW_VERSION" --notes "$RUNNER_TEMP/release-notes.md" \ | |
| --changelog CHANGELOG.md | |
| - name: Sync plugin manifest version | |
| if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' | |
| env: | |
| NEW_VERSION: ${{ steps.new.outputs.version }} | |
| run: | | |
| python3 - << 'PYEOF' | |
| import os | |
| import re | |
| path = '.cursor-plugin/plugin.json' | |
| text = open(path).read() | |
| text = re.sub( | |
| r'^( "version": ")[^"]+(",)$', | |
| rf'\g<1>{os.environ["NEW_VERSION"]}\g<2>', | |
| text, | |
| count=1, | |
| flags=re.M, | |
| ) | |
| open(path, 'w').write(text) | |
| # Claude Code manifests: same version, rewritten via JSON. | |
| import json | |
| for path in ('.claude-plugin/plugin.json', '.claude-plugin/marketplace.json'): | |
| data = json.load(open(path, encoding='utf-8')) | |
| if 'version' in data: | |
| data['version'] = os.environ['NEW_VERSION'] | |
| for entry in data.get('plugins', []): | |
| entry['version'] = os.environ['NEW_VERSION'] | |
| with open(path, 'w', encoding='utf-8', newline='\n') as fh: | |
| fh.write(json.dumps(data, indent=2) + '\n') | |
| PYEOF | |
| - name: Commit version bump | |
| id: commit | |
| if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| # Another PR may have merged after this run checked out (#226). The | |
| # queued run for the newer SHA scans the whole range since the last | |
| # tag and releases everything, so stand down cleanly instead of | |
| # failing with a non-fast-forward push. | |
| git fetch -q origin main | |
| if [ "$(git rev-parse origin/main)" != "$(git rev-parse HEAD)" ]; then | |
| echo "::notice::origin/main moved past $GITHUB_SHA; the newer release run will publish this range" | |
| echo "stale=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Explicit paths only: the files this workflow owns, never `git add -A`. | |
| git add -- VERSION CHANGELOG.md CLAUDE.md ROADMAP.md \ | |
| .cursor-plugin/plugin.json .claude-plugin/plugin.json .claude-plugin/marketplace.json | |
| if [ -n "$(git status --porcelain)" ]; then | |
| echo "::warning::files changed that the release commit does not own:" | |
| git status --porcelain | |
| fi | |
| if git diff --cached --quiet; then | |
| echo "No changes to commit" | |
| else | |
| git commit -s -m "chore: bump version to ${{ steps.new.outputs.version }} [skip ci]" | |
| if ! git push origin main; then | |
| echo "::notice::push rejected (main moved); the newer release run will publish this range" | |
| echo "stale=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| fi | |
| - name: Create and push tag | |
| if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' && steps.commit.outputs.stale != 'true' | |
| run: | | |
| new_version="${{ steps.new.outputs.version }}" | |
| IFS='.' read -r major minor _patch <<< "$new_version" | |
| git tag "v$new_version" | |
| git tag -f "v$major" | |
| git tag -f "v$major.$minor" | |
| git push origin "v$new_version" | |
| git push origin "v$major" --force | |
| git push origin "v$major.$minor" --force | |
| - name: Create GitHub Release | |
| if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' && steps.commit.outputs.stale != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release create "v${{ steps.new.outputs.version }}" \ | |
| --title "v${{ steps.new.outputs.version }}" \ | |
| --notes-file "$RUNNER_TEMP/release-notes.md" | |
| - name: Publish the slim plugin build to plugin-dist | |
| # Claude Code installs from this branch (marketplace.json on main points | |
| # its plugin source here). One orphan commit, force-pushed, so the | |
| # branch never grows history; only main is protected by the ruleset. | |
| if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' && steps.commit.outputs.stale != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| NEW_VERSION: ${{ steps.new.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| dist="$RUNNER_TEMP/plugin-dist" | |
| python3 scripts/build_plugin_dist.py --out "$dist" | |
| # Republish only when what plugin users load changed. A release cut for | |
| # the gallery or site would otherwise bump the plugin version with | |
| # identical content and prompt every user to update (#350). The | |
| # fingerprint ignores the manifest "version" values. | |
| new_fp=$(python3 scripts/build_plugin_dist.py --fingerprint "$dist") | |
| old_fp="" | |
| if git fetch -q --depth 1 origin plugin-dist; then | |
| published="$RUNNER_TEMP/plugin-dist-published" | |
| mkdir -p "$published" | |
| git archive FETCH_HEAD | tar -x -C "$published" | |
| old_fp=$(python3 scripts/build_plugin_dist.py --fingerprint "$published") | |
| fi | |
| echo "plugin-dist fingerprint: built=$new_fp published=${old_fp:-<none>}" | |
| if [ "$new_fp" = "$old_fp" ]; then | |
| echo "Plugin content unchanged; plugin-dist stays at its published version." | |
| exit 0 | |
| fi | |
| cd "$dist" | |
| git init -q -b plugin-dist | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add -A | |
| git commit -q -s -m "plugin build v$NEW_VERSION from $GITHUB_SHA" | |
| git push -q --force "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:plugin-dist | |
| - name: Dispatch Pages | |
| if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' && steps.commit.outputs.stale != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: gh workflow run pages.yml --ref main |