Skip to content

Commit a6dcf5a

Browse files
authored
Merge pull request #87 from delegateas/chore/proces-change-to-github-workflows-release
Changed version bump/release process
2 parents f62ff79 + 7038fd4 commit a6dcf5a

7 files changed

Lines changed: 286 additions & 177 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
name: Release (Prepare)
2+
3+
# Opt-in release preparation that runs DURING a pull request.
4+
#
5+
# Add one of these labels to a PR to prepare a release:
6+
# release:patch | release:minor | release:major
7+
#
8+
# The workflow bumps Website/package.json + .release-please-manifest.json,
9+
# regenerates the changelog, and commits the result to the PR branch.
10+
# No direct push to main happens here (branch protection is respected) and
11+
# no tag is created. Tagging is handled by release-publish.yml after merge.
12+
13+
on:
14+
pull_request:
15+
types: [labeled]
16+
17+
permissions:
18+
contents: write
19+
pull-requests: write
20+
21+
concurrency:
22+
# Serialize per-PR so re-labeling doesn't race itself.
23+
group: release-prepare-${{ github.event.pull_request.number }}
24+
cancel-in-progress: false
25+
26+
jobs:
27+
prepare:
28+
runs-on: ubuntu-latest
29+
# Only react to release:* labels.
30+
if: startsWith(github.event.label.name, 'release:')
31+
steps:
32+
- name: Parse release type from label
33+
id: parse
34+
run: |
35+
LABEL="${{ github.event.label.name }}"
36+
TYPE="${LABEL#release:}"
37+
case "$TYPE" in
38+
patch|minor|major) ;;
39+
*)
40+
echo "::error::Unsupported release label '$LABEL'. Use release:patch, release:minor or release:major."
41+
exit 1
42+
;;
43+
esac
44+
echo "release_type=$TYPE" >> "$GITHUB_OUTPUT"
45+
46+
- name: Checkout PR branch
47+
uses: actions/checkout@v4
48+
with:
49+
ref: ${{ github.event.pull_request.head.ref }}
50+
repository: ${{ github.event.pull_request.head.repo.full_name }}
51+
fetch-depth: 0
52+
token: ${{ secrets.GITHUB_TOKEN }}
53+
54+
- name: Skip if release already prepared on this branch
55+
id: guard
56+
run: |
57+
LAST_MSG=$(git log -1 --pretty=format:%s)
58+
if echo "$LAST_MSG" | grep -q '^chore(release):'; then
59+
echo "Release commit already present on branch head — skipping."
60+
echo "skip=true" >> "$GITHUB_OUTPUT"
61+
else
62+
echo "skip=false" >> "$GITHUB_OUTPUT"
63+
fi
64+
65+
- name: Setup Node.js
66+
if: steps.guard.outputs.skip != 'true'
67+
uses: actions/setup-node@v4
68+
with:
69+
node-version: '20'
70+
71+
- name: Prepare release commit
72+
if: steps.guard.outputs.skip != 'true'
73+
id: prepare
74+
env:
75+
RELEASE_TYPE: ${{ steps.parse.outputs.release_type }}
76+
REPO: ${{ github.repository }}
77+
run: |
78+
npm install semver
79+
80+
git config user.name "github-actions[bot]"
81+
git config user.email "github-actions[bot]@users.noreply.github.com"
82+
83+
CURRENT_VERSION=$(jq -r '.Website' .release-please-manifest.json)
84+
echo "Current version: $CURRENT_VERSION"
85+
86+
NEXT_VERSION=$(node -e "
87+
const semver = require('semver');
88+
console.log(semver.inc('$CURRENT_VERSION', process.env.RELEASE_TYPE));
89+
")
90+
echo "Next version: $NEXT_VERSION"
91+
echo "version=$NEXT_VERSION" >> "$GITHUB_OUTPUT"
92+
93+
# Bump package + manifest
94+
(cd Website && npm version "$NEXT_VERSION" --no-git-tag-version)
95+
jq --arg version "$NEXT_VERSION" '.Website = $version' .release-please-manifest.json > temp.json && mv temp.json .release-please-manifest.json
96+
97+
# Generate enhanced changelog from commits since the last release tag
98+
cat > generate_changelog.js << 'EOF'
99+
const { execSync } = require('child_process');
100+
101+
function generateChangelog(lastTag, nextVersion, repo) {
102+
let commits = [];
103+
try {
104+
const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
105+
const output = execSync(`git log ${range} --pretty=format:"%h|%s"`).toString();
106+
commits = output.split('\n').filter(line => line.trim()).map(line => {
107+
const idx = line.indexOf('|');
108+
return { hash: line.slice(0, idx), message: line.slice(idx + 1) };
109+
});
110+
} catch (error) {
111+
return `## [${nextVersion}] - ${new Date().toISOString().split('T')[0]}\n\n### Changed\n- Manual release\n\n`;
112+
}
113+
114+
const categories = {
115+
'Features': [],
116+
'Bug Fixes': [],
117+
'Performance Improvements': [],
118+
'UI/UX Improvements': [],
119+
'Code Refactoring': [],
120+
'Other Changes': []
121+
};
122+
123+
commits.forEach(commit => {
124+
const { hash, message } = commit;
125+
const link = `([${hash}](https://github.com/${repo}/commit/${hash}))`;
126+
let cleanMessage = message
127+
.replace(/^(feat|feature):\s*/i, '')
128+
.replace(/^(fix|bugfix):\s*/i, '')
129+
.replace(/^(perf|performance):\s*/i, '')
130+
.replace(/^(style|ui|ux):\s*/i, '')
131+
.replace(/^(refactor|refact):\s*/i, '')
132+
.replace(/^chore:\s*/i, '');
133+
134+
if (message.match(/^feat|feature|add|implement|new/i) || message.includes('PBI')) {
135+
categories['Features'].push(`* ${cleanMessage} ${link}`);
136+
} else if (message.match(/^fix|bug|resolve|correct/i)) {
137+
categories['Bug Fixes'].push(`* ${cleanMessage} ${link}`);
138+
} else if (message.match(/perf|performance|optim|speed|fast/i)) {
139+
categories['Performance Improvements'].push(`* ${cleanMessage} ${link}`);
140+
} else if (message.match(/ui|ux|style|design|visual|appearance/i)) {
141+
categories['UI/UX Improvements'].push(`* ${cleanMessage} ${link}`);
142+
} else if (message.match(/refactor|restructure|reorganize|clean/i)) {
143+
categories['Code Refactoring'].push(`* ${cleanMessage} ${link}`);
144+
} else if (!message.match(/^merge|^chore\(release\)/i)) {
145+
categories['Other Changes'].push(`* ${cleanMessage} ${link}`);
146+
}
147+
});
148+
149+
let changelog = `## [${nextVersion}] - ${new Date().toISOString().split('T')[0]}\n\n`;
150+
Object.entries(categories).forEach(([category, items]) => {
151+
if (items.length > 0) {
152+
changelog += `### ${category}\n\n`;
153+
items.forEach(item => { changelog += `${item}\n`; });
154+
changelog += '\n';
155+
}
156+
});
157+
158+
if (Object.values(categories).every(cat => cat.length === 0)) {
159+
changelog += `### Changed\n\n* Release ${nextVersion}\n\n`;
160+
}
161+
162+
return changelog;
163+
}
164+
165+
console.log(generateChangelog(process.argv[2], process.argv[3], process.argv[4]));
166+
EOF
167+
168+
git fetch --tags
169+
LAST_TAG="website-v$CURRENT_VERSION"
170+
if ! git rev-parse -q --verify "refs/tags/$LAST_TAG" >/dev/null; then
171+
LAST_TAG=""
172+
fi
173+
174+
node generate_changelog.js "$LAST_TAG" "$NEXT_VERSION" "$REPO" > temp_changelog.md
175+
176+
if [ -f "Website/CHANGELOG.md" ]; then
177+
cat temp_changelog.md Website/CHANGELOG.md > temp_full_changelog.md
178+
mv temp_full_changelog.md Website/CHANGELOG.md
179+
else
180+
mv temp_changelog.md Website/CHANGELOG.md
181+
fi
182+
183+
rm -f generate_changelog.js temp_changelog.md
184+
185+
git add Website/package.json Website/package-lock.json Website/CHANGELOG.md .release-please-manifest.json
186+
git commit -m "chore(release): release $NEXT_VERSION
187+
188+
Release type: $RELEASE_TYPE
189+
Previous version: $CURRENT_VERSION
190+
New version: $NEXT_VERSION"
191+
192+
git push origin "HEAD:${{ github.event.pull_request.head.ref }}"
193+
194+
- name: Comment on PR
195+
if: steps.guard.outputs.skip != 'true'
196+
uses: actions/github-script@v7
197+
with:
198+
script: |
199+
const version = '${{ steps.prepare.outputs.version }}';
200+
await github.rest.issues.createComment({
201+
owner: context.repo.owner,
202+
repo: context.repo.repo,
203+
issue_number: context.payload.pull_request.number,
204+
body: `🚀 **Release \`v${version}\` prepared** on this branch.\n\nThe version bump and changelog have been committed. When this PR is merged, tag \`website-v${version}\` and a GitHub Release will be created automatically.`
205+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Release (Publish)
2+
3+
# Runs AFTER a release-labeled PR is merged into main.
4+
#
5+
# Detects that the merged PR carried a release:* label, then creates the
6+
# annotated tag (website-vX.Y.Z) and a GitHub Release. The version is read
7+
# from .release-please-manifest.json on main, so it works regardless of
8+
# whether the PR was merged with a merge commit or squashed.
9+
10+
on:
11+
pull_request:
12+
types: [closed]
13+
14+
permissions:
15+
contents: write
16+
17+
jobs:
18+
publish:
19+
runs-on: ubuntu-latest
20+
if: >
21+
github.event.pull_request.merged == true &&
22+
contains(join(github.event.pull_request.labels.*.name, ','), 'release:')
23+
steps:
24+
- name: Checkout main
25+
uses: actions/checkout@v4
26+
with:
27+
ref: ${{ github.event.pull_request.base.ref }}
28+
fetch-depth: 0
29+
30+
- name: Read version and prepare tag
31+
id: version
32+
run: |
33+
VERSION=$(jq -r '.Website' .release-please-manifest.json)
34+
TAG="website-v$VERSION"
35+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
36+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
37+
38+
git fetch --tags
39+
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
40+
echo "Tag $TAG already exists — nothing to publish."
41+
echo "exists=true" >> "$GITHUB_OUTPUT"
42+
else
43+
echo "exists=false" >> "$GITHUB_OUTPUT"
44+
fi
45+
46+
- name: Extract latest changelog section
47+
if: steps.version.outputs.exists != 'true'
48+
id: notes
49+
run: |
50+
# Grab everything from the first "## [" heading up to the next one.
51+
awk '/^## \[/{c++} c==1' Website/CHANGELOG.md > release_notes.md
52+
echo "Release notes:"
53+
cat release_notes.md
54+
55+
- name: Create tag and GitHub Release
56+
if: steps.version.outputs.exists != 'true'
57+
env:
58+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59+
TAG: ${{ steps.version.outputs.tag }}
60+
VERSION: ${{ steps.version.outputs.version }}
61+
run: |
62+
git config user.name "github-actions[bot]"
63+
git config user.email "github-actions[bot]@users.noreply.github.com"
64+
65+
git tag -a "$TAG" -m "Release $VERSION"
66+
git push origin "refs/tags/$TAG"
67+
68+
gh release create "$TAG" \
69+
--title "Release $VERSION" \
70+
--notes-file release_notes.md

0 commit comments

Comments
 (0)