Skip to content

Commit a34edcf

Browse files
authored
Merge pull request #59 from rubenmarcus/feat/release-automation
feat(ci): Add automated release workflow with candidate-release labels
2 parents 364b31c + e32db1d commit a34edcf

File tree

3 files changed

+374
-2
lines changed

3 files changed

+374
-2
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
name: Prepare Release
2+
3+
on:
4+
pull_request:
5+
types: [labeled, closed]
6+
branches:
7+
- main
8+
9+
permissions:
10+
contents: write
11+
pull-requests: write
12+
13+
jobs:
14+
# Auto-add candidate-release label when src/ files change
15+
auto-label:
16+
name: Auto Label Release Candidate
17+
if: github.event.action != 'labeled' && github.event.action != 'closed'
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Check for package changes
24+
id: changes
25+
uses: dorny/paths-filter@v2
26+
with:
27+
filters: |
28+
package:
29+
- 'src/**'
30+
- 'package.json'
31+
- '!**/*.md'
32+
- '!docs/**'
33+
34+
- name: Add candidate-release label
35+
if: steps.changes.outputs.package == 'true'
36+
uses: actions/github-script@v7
37+
with:
38+
script: |
39+
const { owner, repo } = context.repo;
40+
const issue_number = context.payload.pull_request.number;
41+
42+
// Check if label already exists
43+
const labels = await github.rest.issues.listLabelsOnIssue({
44+
owner, repo, issue_number
45+
});
46+
47+
if (!labels.data.find(l => l.name === 'candidate-release')) {
48+
await github.rest.issues.addLabels({
49+
owner, repo, issue_number,
50+
labels: ['candidate-release']
51+
});
52+
console.log('Added candidate-release label');
53+
}
54+
55+
# Create release PR when candidate-release label is added
56+
create-release-pr:
57+
name: Create Release PR
58+
if: |
59+
github.event.action == 'labeled' &&
60+
github.event.label.name == 'candidate-release'
61+
runs-on: ubuntu-latest
62+
steps:
63+
- name: Checkout
64+
uses: actions/checkout@v4
65+
with:
66+
fetch-depth: 0
67+
68+
- name: Setup Node.js
69+
uses: actions/setup-node@v4
70+
with:
71+
node-version: '20'
72+
73+
- name: Determine version bump
74+
id: bump
75+
run: |
76+
# Get commits in this PR to determine bump type
77+
# feat: = minor, fix: = patch, BREAKING CHANGE = major
78+
PR_TITLE="${{ github.event.pull_request.title }}"
79+
80+
if [[ "$PR_TITLE" == *"BREAKING"* ]] || [[ "$PR_TITLE" == *"!"* ]]; then
81+
echo "bump=major" >> $GITHUB_OUTPUT
82+
elif [[ "$PR_TITLE" == feat* ]] || [[ "$PR_TITLE" == *"feat("* ]]; then
83+
echo "bump=minor" >> $GITHUB_OUTPUT
84+
else
85+
echo "bump=patch" >> $GITHUB_OUTPUT
86+
fi
87+
88+
CURRENT_VERSION=$(node -p "require('./package.json').version")
89+
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
90+
91+
- name: Calculate new version
92+
id: version
93+
run: |
94+
CURRENT="${{ steps.bump.outputs.current }}"
95+
BUMP="${{ steps.bump.outputs.bump }}"
96+
97+
# Parse version (handle prerelease)
98+
if [[ "$CURRENT" == *"-"* ]]; then
99+
# It's a prerelease, just bump the prerelease number
100+
BASE=$(echo "$CURRENT" | sed 's/-.*$//')
101+
PRE_TYPE=$(echo "$CURRENT" | sed 's/.*-\([a-z]*\).*/\1/')
102+
PRE_NUM=$(echo "$CURRENT" | sed 's/.*\.\([0-9]*\)$/\1/')
103+
NEW_PRE_NUM=$((PRE_NUM + 1))
104+
NEW_VERSION="${BASE}-${PRE_TYPE}.${NEW_PRE_NUM}"
105+
else
106+
# Parse semver
107+
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
108+
109+
case "$BUMP" in
110+
major)
111+
NEW_VERSION="$((MAJOR + 1)).0.0"
112+
;;
113+
minor)
114+
NEW_VERSION="${MAJOR}.$((MINOR + 1)).0"
115+
;;
116+
patch)
117+
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
118+
;;
119+
esac
120+
fi
121+
122+
echo "new=$NEW_VERSION" >> $GITHUB_OUTPUT
123+
echo "New version will be: $NEW_VERSION (bump: $BUMP)"
124+
125+
- name: Check for existing release PR
126+
id: check
127+
uses: actions/github-script@v7
128+
with:
129+
script: |
130+
const { owner, repo } = context.repo;
131+
const prs = await github.rest.pulls.list({
132+
owner, repo,
133+
state: 'open',
134+
head: `${owner}:release/v${{ steps.version.outputs.new }}`
135+
});
136+
return prs.data.length > 0;
137+
result-encoding: string
138+
139+
- name: Create release branch and PR
140+
if: steps.check.outputs.result != 'true'
141+
run: |
142+
NEW_VERSION="${{ steps.version.outputs.new }}"
143+
CURRENT_VERSION="${{ steps.bump.outputs.current }}"
144+
SOURCE_PR="${{ github.event.pull_request.number }}"
145+
SOURCE_TITLE="${{ github.event.pull_request.title }}"
146+
147+
# Configure git
148+
git config user.name "github-actions[bot]"
149+
git config user.email "github-actions[bot]@users.noreply.github.com"
150+
151+
# Create release branch from main
152+
git checkout main
153+
git pull origin main
154+
git checkout -b "release/v${NEW_VERSION}"
155+
156+
# Update package.json version
157+
npm version "$NEW_VERSION" --no-git-tag-version
158+
159+
# Commit changes
160+
git add package.json package-lock.json
161+
git commit -m "chore(release): bump version to ${NEW_VERSION}"
162+
163+
# Push branch
164+
git push origin "release/v${NEW_VERSION}"
165+
166+
# Create PR
167+
gh pr create \
168+
--title "chore(release): v${NEW_VERSION}" \
169+
--body "$(cat <<EOF
170+
## Release v${NEW_VERSION}
171+
172+
This PR was automatically created to prepare release v${NEW_VERSION}.
173+
174+
### Changes included
175+
- From PR #${SOURCE_PR}: ${SOURCE_TITLE}
176+
177+
### Version bump
178+
- Previous: ${CURRENT_VERSION}
179+
- New: ${NEW_VERSION}
180+
- Type: ${{ steps.bump.outputs.bump }}
181+
182+
### What happens when this PR is merged
183+
1. Git tag \`v${NEW_VERSION}\` will be created
184+
2. GitHub Release will be published
185+
3. Package will be published to npm
186+
187+
---
188+
🤖 This PR was auto-generated by the release workflow.
189+
EOF
190+
)" \
191+
--label "release" \
192+
--base main
193+
env:
194+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
195+
196+
- name: Comment on source PR
197+
uses: actions/github-script@v7
198+
with:
199+
script: |
200+
const { owner, repo } = context.repo;
201+
const issue_number = context.payload.pull_request.number;
202+
const newVersion = '${{ steps.version.outputs.new }}';
203+
204+
await github.rest.issues.createComment({
205+
owner, repo, issue_number,
206+
body: `🚀 Release PR created for **v${newVersion}**!\n\nOnce this PR is merged, a release PR will be ready. Merge the release PR to publish.`
207+
});

.github/workflows/release.yml

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
name: Release
2+
3+
on:
4+
pull_request:
5+
types: [closed]
6+
branches:
7+
- main
8+
9+
permissions:
10+
contents: write
11+
12+
jobs:
13+
release:
14+
name: Create Release
15+
# Only run when a release PR is merged
16+
if: |
17+
github.event.pull_request.merged == true &&
18+
startsWith(github.event.pull_request.head.ref, 'release/v')
19+
runs-on: ubuntu-latest
20+
outputs:
21+
version: ${{ steps.version.outputs.version }}
22+
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
with:
27+
fetch-depth: 0
28+
29+
- name: Setup Node.js
30+
uses: actions/setup-node@v4
31+
with:
32+
node-version: '20'
33+
cache: 'npm'
34+
35+
- name: Get version from package.json
36+
id: version
37+
run: |
38+
VERSION=$(node -p "require('./package.json').version")
39+
echo "version=$VERSION" >> $GITHUB_OUTPUT
40+
41+
if [[ "$VERSION" == *"-"* ]]; then
42+
echo "is_prerelease=true" >> $GITHUB_OUTPUT
43+
else
44+
echo "is_prerelease=false" >> $GITHUB_OUTPUT
45+
fi
46+
47+
echo "Version: $VERSION"
48+
49+
- name: Install dependencies
50+
run: npm ci
51+
52+
- name: Build
53+
run: npm run build
54+
55+
- name: Run tests
56+
run: npm test
57+
58+
- name: Create and push tag
59+
run: |
60+
VERSION=${{ steps.version.outputs.version }}
61+
git config user.name "github-actions[bot]"
62+
git config user.email "github-actions[bot]@users.noreply.github.com"
63+
64+
# Create tag if it doesn't exist
65+
if ! git rev-parse "v$VERSION" >/dev/null 2>&1; then
66+
git tag -a "v$VERSION" -m "Release v$VERSION"
67+
git push origin "v$VERSION"
68+
echo "Created tag v$VERSION"
69+
else
70+
echo "Tag v$VERSION already exists"
71+
fi
72+
73+
- name: Generate release notes
74+
run: |
75+
VERSION=${{ steps.version.outputs.version }}
76+
IS_PRERELEASE=${{ steps.version.outputs.is_prerelease }}
77+
78+
# Get previous tag
79+
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
80+
81+
# Build release notes
82+
NOTES="## What's Changed\n\n"
83+
84+
if [ -n "$PREV_TAG" ]; then
85+
COMMITS=$(git log --pretty=format:"- %s (%h)" $PREV_TAG..HEAD --no-merges 2>/dev/null | grep -v "chore(release)" || echo "")
86+
if [ -n "$COMMITS" ]; then
87+
NOTES+="$COMMITS\n\n"
88+
fi
89+
fi
90+
91+
# Add installation instructions
92+
if [ "$IS_PRERELEASE" == "true" ]; then
93+
PRERELEASE_TYPE=$(echo "$VERSION" | sed 's/.*-\([a-z]*\).*/\1/')
94+
NOTES+="## Installation\n\n"
95+
NOTES+="\`\`\`bash\n"
96+
NOTES+="npm install -g ralph-starter@$PRERELEASE_TYPE\n"
97+
NOTES+="\`\`\`\n\n"
98+
NOTES+="Or install this specific version:\n\n"
99+
NOTES+="\`\`\`bash\n"
100+
NOTES+="npm install -g ralph-starter@$VERSION\n"
101+
NOTES+="\`\`\`\n"
102+
else
103+
NOTES+="## Installation\n\n"
104+
NOTES+="\`\`\`bash\n"
105+
NOTES+="npm install -g ralph-starter\n"
106+
NOTES+="\`\`\`\n"
107+
fi
108+
109+
echo -e "$NOTES" > release_notes.md
110+
111+
- name: Create GitHub Release
112+
uses: softprops/action-gh-release@v1
113+
with:
114+
tag_name: v${{ steps.version.outputs.version }}
115+
name: v${{ steps.version.outputs.version }}
116+
body_path: release_notes.md
117+
prerelease: ${{ steps.version.outputs.is_prerelease }}
118+
generate_release_notes: true
119+
env:
120+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
121+
122+
publish:
123+
name: Publish to npm
124+
needs: release
125+
runs-on: ubuntu-latest
126+
steps:
127+
- name: Checkout
128+
uses: actions/checkout@v4
129+
130+
- name: Setup Node.js
131+
uses: actions/setup-node@v4
132+
with:
133+
node-version: '20'
134+
cache: 'npm'
135+
registry-url: 'https://registry.npmjs.org'
136+
137+
- name: Install dependencies
138+
run: npm ci
139+
140+
- name: Build
141+
run: npm run build
142+
143+
- name: Publish to npm
144+
run: |
145+
VERSION=${{ needs.release.outputs.version }}
146+
if [[ "$VERSION" == *"-"* ]]; then
147+
npm publish --tag beta
148+
else
149+
npm publish --tag latest
150+
fi
151+
env:
152+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
153+
154+
- name: Comment on PR
155+
uses: actions/github-script@v7
156+
with:
157+
script: |
158+
const { owner, repo } = context.repo;
159+
const version = '${{ needs.release.outputs.version }}';
160+
161+
await github.rest.issues.createComment({
162+
owner, repo,
163+
issue_number: context.payload.pull_request.number,
164+
body: `✅ **v${version}** has been released!\n\n- 📦 [npm](https://www.npmjs.com/package/ralph-starter/v/${version})\n- 🏷️ [GitHub Release](https://github.com/${owner}/${repo}/releases/tag/v${version})`
165+
});

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)