Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ on:
push:
tags:
- "v*"
# version-bump.yml pushes its `v*` tag using GITHUB_TOKEN, which GitHub
# deliberately excludes from triggering the `push: tags:` rule above (to
# prevent bot-authored pushes from cascading into unbounded workflow
# runs) — so it dispatches this workflow directly instead. `--ref` on that
# dispatch call points this run at the tag itself, so `github.ref_name`
# below still resolves to the same `vX.Y.Z` a manually-pushed tag would.
workflow_dispatch:

jobs:
release:
Expand Down
131 changes: 131 additions & 0 deletions .github/workflows/version-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: Version Bump

# Runs after every push to main and decides, from Conventional Commits since
# the last tag, whether this merge deserves a new version: fix: -> patch,
# feat: -> minor, a `!` after the type/scope or a `BREAKING CHANGE:` footer
# -> major. Anything else (chore/docs/refactor/test/ci/style, or a push with
# no new commits since the last tag) bumps nothing and the job exits early.
#
# When a bump is due, it writes the new version into all 4 files the
# project's manual convention already touched (package.json,
# package-lock.json, src-tauri/Cargo.toml, src-tauri/Cargo.lock,
# src-tauri/tauri.conf.json), commits, tags `vX.Y.Z`, and pushes both.
#
# The push uses the workflow's own GITHUB_TOKEN. GitHub explicitly does not
# let a GITHUB_TOKEN-authored push trigger *other* workflow runs (this is
# what stops an infinite loop here too, on top of the guard below) — so the
# `v*` tag this job pushes will NOT auto-fire release.yml's `on: push:
# tags:` trigger. The final step compensates by explicitly dispatching
# release.yml via the API (`gh workflow run`), which release.yml has to
# opt into with `workflow_dispatch:`.
on:
push:
branches: [main]

permissions:
contents: write
actions: write

jobs:
bump:
runs-on: ubuntu-latest
# Without this, the bump commit's own push to main would re-trigger this
# same workflow. (In practice GITHUB_TOKEN pushes already don't trigger
# further runs — see the note above — but this keeps the job correct
# even if that ever changes, e.g. a maintainer force-pushing a manual
# bump commit under their own account.)
if: ${{ !startsWith(github.event.head_commit.message, 'chore: bump version to') }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Determine bump level from commits since the last tag
id: bump
run: |
set -e
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$LAST_TAG" ]; then
RANGE="$LAST_TAG..HEAD"
echo "Scanning commits since $LAST_TAG"
else
RANGE="HEAD"
echo "No prior tag found; scanning full history"
fi
SUBJECTS=$(git log "$RANGE" --pretty=%s)
BODIES=$(git log "$RANGE" --pretty=%b)
LEVEL="none"
if echo "$SUBJECTS" | grep -qE '^[a-zA-Z]+(\([^)]*\))?!:' || echo "$BODIES" | grep -q 'BREAKING CHANGE'; then
LEVEL="major"
elif echo "$SUBJECTS" | grep -qE '^feat(\([^)]*\))?:'; then
LEVEL="minor"
elif echo "$SUBJECTS" | grep -qE '^fix(\([^)]*\))?:'; then
LEVEL="patch"
fi
echo "level=$LEVEL" >> "$GITHUB_OUTPUT"
echo "Bump level: $LEVEL"

- name: Compute next version
id: version
if: steps.bump.outputs.level != 'none'
run: |
set -e
CURRENT=$(node -p "require('./package.json').version")
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
case "${{ steps.bump.outputs.level }}" in
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
patch) PATCH=$((PATCH + 1)) ;;
esac
NEW="$MAJOR.$MINOR.$PATCH"
echo "new=$NEW" >> "$GITHUB_OUTPUT"
echo "Bumping $CURRENT -> $NEW"

- uses: actions/setup-node@v4
if: steps.bump.outputs.level != 'none'
with:
node-version: 22

- uses: dtolnay/rust-toolchain@stable
if: steps.bump.outputs.level != 'none'

- uses: swatinem/rust-cache@v2
if: steps.bump.outputs.level != 'none'
with:
workspaces: src-tauri

- name: Write the new version into every version file
if: steps.bump.outputs.level != 'none'
run: |
set -e
NEW="${{ steps.version.outputs.new }}"
# Updates package.json and package-lock.json together, the same
# way the project's manual bump convention did.
npm version "$NEW" --no-git-tag-version --allow-same-version
# Cargo.toml's version line is the package's own — the first
# `version = "..."` in the file, right under [package].
sed -i "0,/^version = \"[^\"]*\"/s//version = \"$NEW\"/" src-tauri/Cargo.toml
sed -i "s/\"version\": \"[0-9.]*\"/\"version\": \"$NEW\"/" src-tauri/tauri.conf.json
# `cargo check` is the same mechanism the manual convention relied
# on to sync Cargo.lock's afkode entry to the new Cargo.toml
# version without touching any dependency's resolved version.
(cd src-tauri && cargo check)

- name: Commit, tag, and push
if: steps.bump.outputs.level != 'none'
run: |
set -e
NEW="${{ steps.version.outputs.new }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json package-lock.json src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.conf.json
git commit -m "chore: bump version to $NEW"
git tag "v$NEW"
git push origin HEAD:main
git push origin "v$NEW"

- name: Trigger the release build
if: steps.bump.outputs.level != 'none'
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run release.yml --ref "v${{ steps.version.outputs.new }}"
52 changes: 52 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# AFKode

## Commit messages: Conventional Commits (required)

Every commit message on `main` — and therefore every PR that gets merged —
must follow [Conventional Commits](https://www.conventionalcommits.org/):

```
<type>(<optional scope>): <description>
```

`.github/workflows/version-bump.yml` reads these on every push to `main` and
decides the version bump automatically:

- `fix: ...` → **patch** bump (`0.8.19` → `0.8.20`)
- `feat: ...` → **minor** bump (`0.8.19` → `0.9.0`)
- A `!` right after the type/scope (`feat!:`, `fix(pty)!:`) or a
`BREAKING CHANGE:` footer in the commit body → **major** bump
(`0.8.19` → `1.0.0`)
- `chore:`, `docs:`, `refactor:`, `test:`, `style:`, `ci:`, `build:` and
anything else that isn't `fix`/`feat`/breaking → **no bump**

The workflow scans every commit since the last `v*` tag, takes the highest
level found (major beats minor beats patch), writes the new version into
`package.json`, `package-lock.json`, `src-tauri/Cargo.toml`,
`src-tauri/Cargo.lock`, and `src-tauri/tauri.conf.json`, commits it as
`chore: bump version to X.Y.Z`, tags it `vX.Y.Z`, and dispatches
`release.yml` — which builds, signs/notarizes, and publishes installers for
Windows/macOS/Linux, and opens the winget-pkgs manifest PR. **A `fix:` or
`feat:` commit landing on `main` becomes a public release with no further
human step.**

Practical implications:

- **Do not hand-write `chore: bump version to ...` commits anymore** — the
version files are the automation's responsibility now, not the
contributor's or Claude's. Editing them by hand ahead of it just makes the
automation's diff noisier (it still runs its own bump on top).
- **Pick the commit type deliberately.** A `fix:`/`feat:` on `main` is not
cosmetic — it ships a signed installer to real users and opens a winget PR.
If a change on `main` should NOT trigger a release (a docs tweak framed as
`fix:` by habit, a revert, an internal-only change), use `chore:`,
`docs:`, `refactor:`, etc. instead.
- **Squash-merge PRs with a properly-typed final message.** The workflow
reads commit subjects on `main`, not PR titles — if a PR merges as a merge
commit with all its intermediate WIP messages, every one of those subjects
is scanned, so a stray `feat:` typo in an intermediate commit still forces
a minor bump. Prefer squash merges with one clean Conventional Commit
subject.
- **Multiple `fix`/`feat` commits in one push only bump once** — the
workflow computes a single new version per run, taking the highest level
among everything since the last tag, not one bump per commit.
Loading