Skip to content

Commit c482309

Browse files
authored
KAN-196/redesign-release-workflow-defer-version-bump-to-master-merge (#163)
* fix: defer version bump to master merge The previous workflow bumped version on every develop merge, causing desync between Cargo.toml version and crates.io. Version 0.2.1 was bumped on develop but never published. Move version bumping into release.yml so bump, changelog, publish, and tag all happen atomically on master merge: - Delete aggregate-changesets.yml (no more auto-bump on develop) - Rewrite release.yml to run bump-version.sh on master merge - Replace ci.yml version-check with changeset-check for master PRs - Delete scripts/aggregate-changelog.sh (redundant with bump-version.sh) - Revert version 0.2.1 -> 0.2.0 to sync with master/crates.io - Remove broken 0.2.1 CHANGELOG entry KAN-196 * fix: exclude README.md from changeset detection in bump-version.sh The glob `ls -A .changeset/*.md` matched README.md, causing the script to think changesets exist when only the README is present. The for loop also lacked a skip guard, so README.md would be parsed as a changeset producing garbage changelog entries. Replace ls glob with find that excludes README.md, and add a basename check in the iteration loop. * refactor: extract changelog aggregation into standalone script bump-version.sh was handling version bumping, changelog aggregation, changeset cleanup, and git commit in a single script. Split these concerns: - bump-version.sh: only bumps versions in Cargo.toml files and runs cargo update - aggregate-changelog.sh: reads changesets, prepends entries to CHANGELOG.md with PR link, then deletes processed changeset files Also fix fragile bump-type parsing by using sed to properly extract the bump field from within frontmatter boundaries instead of grep -A1 which only matched immediately after the first ---. * fix: reorder release workflow to validate before push Previously the release workflow pushed the version bump commit to master before running validation. If validation failed, master would have a bumped version with no corresponding crates.io publish. Reorder steps: bump -> aggregate changelog -> commit -> get version -> validate -> push -> publish. Validation now runs locally before anything is pushed. Also split the workflow into separate visible steps for bump, changelog, and commit. And handle merge conflicts in the develop sync step with a warning instead of failing the entire release. * fix: use robust frontmatter parsing in changeset-check The grep -A1 approach only found the bump field if it appeared on the line immediately after ---. Use sed to extract from within the full frontmatter block, and fall back to "unknown" if parsing fails. * chore: wire all scripts into nix dev shell Add aggregate-changelog as a writeShellApplication in flake.nix and pass all script packages (changeset, aggregate-changelog, bump-version, publish-crates, validate-release) into shell.nix from the flake. Remove the duplicate writeShellScriptBin changeset definition from shell.nix — it now receives the flake's writeShellApplication version like the other scripts. Defaults to null so standalone nix-shell still works. Update CI workflows to use `nix develop --command <name>` consistently instead of `bash scripts/...` or `nix run .#...`. * fix: quote variable in parameter expansion to satisfy shellcheck SC2295 writeShellApplication runs shellcheck, which flags unquoted variables inside parameter expansion patterns. * fix: address PR review findings for release workflow - Make changeset-check block master PRs without changesets, with [hotfix] PR title bypass - Add ordering comment for bump-version/aggregate-changelog dependency - Consolidate duplicate version read into single release step - Reinstate cleanup trap in aggregate-changelog.sh - Filter shell.nix scripts individually instead of all-or-nothing guard * chore: add changeset * fix: address review findings for release workflow scripts - Strip leading "- " from changeset descriptions before prepending it in aggregate-changelog to prevent double-dash entries - Remove dead GITHUB_OUTPUT write from bump-version.sh (release.yml reads version from Cargo.toml directly) - Move PR title from inline ${{ }} interpolation to env block in changeset-check to prevent shell injection * fix: normalize changelog bullet prefix per-line in aggregate-changelog
1 parent c5a0b51 commit c482309

File tree

12 files changed

+157
-238
lines changed

12 files changed

+157
-238
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
bump: patch
3+
---
4+
5+
- fix: address PR review findings for release workflow
6+
- fix: quote variable in parameter expansion to satisfy shellcheck SC2295
7+
- chore: wire all scripts into nix dev shell
8+
- fix: use robust frontmatter parsing in changeset-check
9+
- fix: reorder release workflow to validate before push
10+
- refactor: extract changelog aggregation into standalone script
11+
- fix: exclude README.md from changeset detection in bump-version.sh
12+
- fix: defer version bump to master merge

.github/workflows/aggregate-changesets.yml

Lines changed: 0 additions & 72 deletions
This file was deleted.

.github/workflows/ci.yml

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -124,34 +124,43 @@ jobs:
124124
125125
echo "✅ Changeset validated successfully"
126126
127-
version-check:
128-
name: Version Check
127+
changeset-check:
128+
name: Changeset Check
129129
if: github.event_name == 'pull_request' && github.base_ref == 'master'
130130
runs-on: ubuntu-latest
131131
steps:
132132
- uses: actions/checkout@v4
133-
with:
134-
fetch-depth: 0
135133

136-
- name: Check version bump
134+
- name: Check for changesets
135+
env:
136+
PR_TITLE: ${{ github.event.pull_request.title }}
137137
run: |
138-
CURRENT_VERSION=$(grep -m1 'version = ' Cargo.toml | cut -d'"' -f2)
139-
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
140-
LAST_VERSION="${LAST_TAG#v}"
138+
if [[ "$PR_TITLE" == *"[hotfix]"* ]]; then
139+
echo "Hotfix PR, skipping changeset check"
140+
exit 0
141+
fi
141142
142-
echo "Current version: $CURRENT_VERSION"
143-
echo "Last released version: $LAST_VERSION"
144-
echo ""
143+
if [ ! -d ".changeset" ]; then
144+
echo "No .changeset directory found"
145+
echo "PRs to master require changesets to trigger a release."
146+
echo "Add [hotfix] to the PR title to bypass this check."
147+
exit 1
148+
fi
145149
146-
if [ "$CURRENT_VERSION" = "$LAST_VERSION" ]; then
147-
echo "❌ Version has not been bumped!"
148-
echo ""
149-
echo "The version in Cargo.toml ($CURRENT_VERSION) matches the last released version ($LAST_VERSION)."
150-
echo "Please bump the version in Cargo.toml before merging to master."
150+
CHANGESETS=$(find .changeset -maxdepth 1 -name "*.md" ! -name "README.md" 2>/dev/null || true)
151+
152+
if [ -z "$CHANGESETS" ]; then
153+
echo "No changesets found"
154+
echo "PRs to master require changesets to trigger a release."
155+
echo "Add [hotfix] to the PR title to bypass this check."
151156
exit 1
152157
fi
153158
154-
echo "✅ Version has been bumped: $LAST_VERSION → $CURRENT_VERSION"
159+
echo "Found changesets (release will be triggered on merge):"
160+
for changeset in $CHANGESETS; do
161+
bump=$(sed -n '/^---$/,/^---$/{ /^bump:/{ s/^bump: *//; p; } }' "$changeset" | tr -d '\r\n')
162+
echo " $changeset (bump: ${bump:-unknown})"
163+
done
155164
156165
release-validation:
157166
name: Release Validation
@@ -167,4 +176,4 @@ jobs:
167176
168177
- name: Validate release build
169178
run: |
170-
nix develop --command bash scripts/validate-release.sh
179+
nix develop --command validate-release

.github/workflows/release.yml

Lines changed: 64 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ on:
77
types:
88
- closed
99

10+
concurrency:
11+
group: release
12+
cancel-in-progress: false
13+
1014
jobs:
1115
release:
1216
name: Release to crates.io
@@ -20,114 +24,115 @@ jobs:
2024
fetch-depth: 0
2125
ref: master
2226

23-
- name: Get versions
24-
id: versions
27+
- name: Check for changesets
28+
id: check
2529
run: |
26-
CURRENT_VERSION=$(grep -m1 'version = ' Cargo.toml | cut -d'"' -f2)
27-
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
28-
LAST_VERSION="${LAST_TAG#v}"
29-
30-
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
31-
echo "last=$LAST_VERSION" >> $GITHUB_OUTPUT
32-
33-
# Check if current version is greater than last version using sort -V
34-
# Note: sort -V is a GNU extension, available on Ubuntu runners
35-
version_gt() {
36-
[ "$1" != "$2" ] && [ "$(printf '%s\n' "$1" "$2" | sort -V | head -n1)" = "$2" ]
37-
}
38-
39-
if version_gt "$CURRENT_VERSION" "$LAST_VERSION"; then
40-
echo "should_release=true" >> $GITHUB_OUTPUT
41-
echo "Version increased: $LAST_VERSION → $CURRENT_VERSION"
42-
elif [ "$CURRENT_VERSION" = "$LAST_VERSION" ]; then
43-
echo "should_release=false" >> $GITHUB_OUTPUT
44-
echo "Version unchanged ($CURRENT_VERSION), skipping release"
30+
if [ ! -d ".changeset" ]; then
31+
echo "has_changesets=false" >> "$GITHUB_OUTPUT"
32+
echo "No .changeset directory, skipping release"
33+
exit 0
34+
fi
35+
36+
CHANGESETS=$(find .changeset -maxdepth 1 -name "*.md" ! -name "README.md" 2>/dev/null || true)
37+
if [ -z "$CHANGESETS" ]; then
38+
echo "has_changesets=false" >> "$GITHUB_OUTPUT"
39+
echo "No changesets found, skipping release"
4540
else
46-
echo "should_release=false" >> $GITHUB_OUTPUT
47-
echo "::error::Version decreased: $LAST_VERSION → $CURRENT_VERSION (must be greater)"
48-
exit 1
41+
echo "has_changesets=true" >> "$GITHUB_OUTPUT"
42+
echo "Found changesets:"
43+
echo "$CHANGESETS"
4944
fi
5045
5146
- uses: cachix/install-nix-action@v27
52-
if: steps.versions.outputs.should_release == 'true'
47+
if: steps.check.outputs.has_changesets == 'true'
5348
with:
5449
github_access_token: ${{ secrets.GITHUB_TOKEN }}
5550
extra_nix_config: |
5651
experimental-features = nix-command flakes
5752
5853
- name: Setup SSH deploy key
59-
if: steps.versions.outputs.should_release == 'true'
54+
if: steps.check.outputs.has_changesets == 'true'
6055
run: |
6156
mkdir -p ~/.ssh
6257
echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key
6358
chmod 600 ~/.ssh/deploy_key
6459
ssh-keyscan github.com >> ~/.ssh/known_hosts
6560
6661
- name: Configure Git
67-
if: steps.versions.outputs.should_release == 'true'
62+
if: steps.check.outputs.has_changesets == 'true'
6863
run: |
6964
git config user.name "github-actions[bot]"
7065
git config user.email "github-actions[bot]@users.noreply.github.com"
7166
git remote set-url origin git@github.com:fulsomenko/kanban.git
7267
73-
- name: Aggregate changelog from changesets
74-
if: steps.versions.outputs.should_release == 'true'
68+
# bump-version reads changesets, aggregate-changelog reads then deletes them.
69+
# These two steps must run in this order.
70+
- name: Bump version from changesets
71+
if: steps.check.outputs.has_changesets == 'true'
7572
run: |
76-
bash scripts/aggregate-changelog.sh
73+
nix develop --command bump-version
7774
78-
- name: Commit changelog update
79-
if: steps.versions.outputs.should_release == 'true'
80-
env:
81-
GIT_SSH_COMMAND: "ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=no"
75+
- name: Aggregate changelog
76+
if: steps.check.outputs.has_changesets == 'true'
77+
run: |
78+
nix develop --command aggregate-changelog "${{ github.event.pull_request.number }}"
79+
80+
- name: Commit release changes
81+
if: steps.check.outputs.has_changesets == 'true'
82+
id: release
8283
run: |
83-
git add CHANGELOG.md
84-
git commit -m "docs: update changelog for v${{ steps.versions.outputs.current }}" || echo "No changelog changes to commit"
85-
git push origin master || echo "Nothing to push"
84+
NEW_VERSION=$(grep -m1 'version = ' Cargo.toml | cut -d'"' -f2)
85+
echo "new=$NEW_VERSION" >> "$GITHUB_OUTPUT"
86+
echo "Release version: $NEW_VERSION"
87+
git add .
88+
git commit -m "chore: release v${NEW_VERSION}"
8689
8790
- name: Validate release build
88-
if: steps.versions.outputs.should_release == 'true'
91+
if: steps.check.outputs.has_changesets == 'true'
8992
run: |
90-
nix develop --command bash scripts/validate-release.sh
93+
nix develop --command validate-release
94+
95+
- name: Push release commit
96+
if: steps.check.outputs.has_changesets == 'true'
97+
env:
98+
GIT_SSH_COMMAND: "ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=no"
99+
run: |
100+
git push origin master
91101
92102
- name: Publish to crates.io
93-
if: steps.versions.outputs.should_release == 'true'
103+
if: steps.check.outputs.has_changesets == 'true'
94104
env:
95105
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
96106
run: |
97-
echo "🚀 Publishing to crates.io..."
107+
echo "Publishing to crates.io..."
98108
nix run .#publish-crates
99-
echo "Published successfully"
109+
echo "Published successfully"
100110
101111
- name: Tag version
102-
if: steps.versions.outputs.should_release == 'true'
112+
if: steps.check.outputs.has_changesets == 'true'
103113
env:
104114
GIT_SSH_COMMAND: "ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=no"
105115
run: |
106-
git tag "v${{ steps.versions.outputs.current }}"
107-
git push origin "v${{ steps.versions.outputs.current }}"
116+
git tag "v${{ steps.release.outputs.new }}"
117+
git push origin "v${{ steps.release.outputs.new }}"
108118
109119
- name: Create GitHub Release
110-
if: steps.versions.outputs.should_release == 'true'
120+
if: steps.check.outputs.has_changesets == 'true'
111121
uses: softprops/action-gh-release@v2
112122
with:
113-
tag_name: v${{ steps.versions.outputs.current }}
123+
tag_name: v${{ steps.release.outputs.new }}
114124
generate_release_notes: true
115125

116-
- name: Clean up changesets and sync develop
117-
if: steps.versions.outputs.should_release == 'true'
126+
- name: Sync develop with master
127+
if: steps.check.outputs.has_changesets == 'true'
118128
env:
119129
GIT_SSH_COMMAND: "ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=no"
120130
run: |
121-
# Delete changesets on master (after successful publish)
122-
if [ -d ".changeset" ]; then
123-
find .changeset -maxdepth 1 -name "*.md" ! -name "README.md" -delete
124-
git add .changeset/
125-
fi
126-
git commit -m "chore: clean up changesets for v${{ steps.versions.outputs.current }}" || echo "No changesets to clean up"
127-
git push origin master || echo "Nothing to push"
128-
129-
# Sync develop with master (gets changelog + changeset cleanup)
130131
git checkout develop
131132
git pull origin develop
132-
git merge origin/master --no-edit
133+
if ! git merge origin/master --no-edit; then
134+
echo "::warning::Failed to auto-merge master into develop. Manual merge required."
135+
git merge --abort
136+
exit 0
137+
fi
133138
git push origin develop

CHANGELOG.md

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,3 @@
1-
## [0.2.1] - 2026-02-01 ([#162](https://github.com/fulsomenko/kanban/pull/162))
2-
3-
- # Changesets
4-
When creating a PR, add a changeset file to describe your changes.
5-
## Creating a Changeset
6-
```bash
7-
nix run .#changeset
8-
```
9-
Or create a file `.changeset/<descriptive-name>.md` manually:
10-
```md
11-
Brief description of changes for the changelog
12-
```
13-
## Bump Types
14-
- `patch` - Bug fixes, small changes (0.1.0 → 0.1.1)
15-
- `minor` - New features, backwards compatible (0.1.0 → 0.2.0)
16-
- `major` - Breaking changes (0.1.0 → 1.0.0)
17-
On merge to master, changesets are aggregated and the highest bump type determines the version increment.
18-
- - refactor: extract Sprint::assignable to deduplicate sprint filtering
19-
- fix: filter completed/cancelled sprints from assignment handlers
20-
21-
221
## [0.2.0] - 2026-02-01
232

243
- - feat(tui): register undo/redo keybindings in CardList provider

0 commit comments

Comments
 (0)