Skip to content

Release: Semantic Release #15

Release: Semantic Release

Release: Semantic Release #15

Workflow file for this run

name: Pre-release Tag
on:
pull_request:
types: [closed]
branches: [main]
concurrency:
group: pre-release
cancel-in-progress: false
jobs:
pre-release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
name: Pre-Release
permissions:
contents: write
issues: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }}
fetch-depth: 0
- name: Get PR type from description
id: pr-type
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const body = context.payload.pull_request.body || '';
const typeMatch = body.match(/type:\s*(feat|fix|chore|docs|refactor)/i);
if (!typeMatch) {
core.setOutput('type', 'fix');
return;
}
const type = typeMatch[1].toLowerCase();
core.setOutput('type', type);
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install semantic-release
run: |
npm install -g \
semantic-release \
@semantic-release/commit-analyzer \
@semantic-release/release-notes-generator \
@semantic-release/github
- name: Create conventional commit
env:
COMMIT_TYPE: ${{ steps.pr-type.outputs.type }}
run: |
git config user.email "ci@github.com"
git config user.name "GitHub CI"
ORIGINAL_MSG=$(git log -1 --pretty=%s)
git commit --allow-empty -m "${COMMIT_TYPE}: ${ORIGINAL_MSG}"
- name: Run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
- name: Rewrite notes (cumulative, grouped, since last stable)
if: success()
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const cmp = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
const releases = await github.paginate(github.rest.repos.listReleases, {
owner, repo, per_page: 100,
});
const latest = releases
.filter(r => r.prerelease && /-main\.\d+$/.test(r.tag_name))
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0];
if (!latest) {
core.info('No pre-release found to update.');
return;
}
const tags = await github.paginate(github.rest.repos.listTags, {
owner, repo, per_page: 100,
});
const stableRe = /^v(\d+)\.(\d+)\.(\d+)$/;
let stableTag = null;
let stableVer = [-1, -1, -1];
for (const t of tags) {
const m = stableRe.exec(t.name);
if (m) {
const v = [+m[1], +m[2], +m[3]];
if (cmp(v, stableVer) > 0) { stableVer = v; stableTag = t.name; }
}
}
let sinceMs = 0;
if (stableTag) {
const ref = await github.rest.git.getRef({ owner, repo, ref: `tags/${stableTag}` });
let sha = ref.data.object.sha;
if (ref.data.object.type === 'tag') {
const t = await github.rest.git.getTag({ owner, repo, tag_sha: sha });
sha = t.data.object.sha;
}
const c = await github.rest.repos.getCommit({ owner, repo, ref: sha });
sinceMs = new Date(c.data.commit.committer.date).getTime();
}
const merged = [];
for (let page = 1; page <= 20; page++) {
const { data } = await github.rest.pulls.list({
owner, repo, state: 'closed', base: 'main',
sort: 'updated', direction: 'desc', per_page: 100, page,
});
if (!data.length) break;
for (const pr of data) {
if (pr.merged_at && new Date(pr.merged_at).getTime() > sinceMs) merged.push(pr);
}
if (new Date(data[data.length - 1].updated_at).getTime() < sinceMs) break;
}
if (!merged.length) {
core.info('No merged PRs since last stable; leaving notes as-is.');
return;
}
const cats = [
{ title: '🚀 Features', types: ['feat'] },
{ title: '🐛 Fixes', types: ['fix'] },
{ title: '🧹 Chores', types: ['chore'] },
{ title: '📚 Documentation', types: ['docs'] },
{ title: '♻️ Refactors', types: ['refactor'] },
{ title: 'Other Changes', types: ['*'] },
].map(c => ({ ...c, items: [] }));
const typeOf = (pr) => {
const m = (pr.body || '').match(/type:\s*(feat|fix|chore|docs|refactor)/i);
return m ? m[1].toLowerCase() : 'fix';
};
const seen = new Set();
for (const pr of merged) {
if (seen.has(pr.number)) continue;
seen.add(pr.number);
const type = typeOf(pr);
const bucket = cats.find(c => c.types.includes(type)) || cats.find(c => c.types.includes('*'));
bucket.items.push(pr);
}
let body = `## What's Changed\n`;
for (const c of cats) {
if (!c.items.length) continue;
body += `\n### ${c.title}\n`;
for (const pr of c.items) {
body += `* ${pr.title} by @${pr.user.login} in #${pr.number}\n`;
}
}
if (stableTag) {
body += `\n**Full Changelog**: https://github.com/${owner}/${repo}/compare/${stableTag}...${latest.tag_name}\n`;
}
await github.rest.repos.updateRelease({
owner, repo, release_id: latest.id, body,
});
core.info(`Rewrote ${latest.tag_name} notes: ${merged.length} PR(s) since ${stableTag || 'repo start'}.`);
- name: Remove previous pre-releases
if: success()
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const releases = await github.paginate(
github.rest.repos.listReleases,
{ owner, repo, per_page: 100 }
);
const preReleases = releases
.filter(r => r.prerelease && /-main\.\d+$/.test(r.tag_name))
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
const stale = preReleases.slice(1);
for (const r of stale) {
core.info(`Deleting stale pre-release ${r.tag_name}`);
await github.rest.repos.deleteRelease({ owner, repo, release_id: r.id });
try {
await github.rest.git.deleteRef({
owner,
repo,
ref: `tags/${r.tag_name}`,
});
} catch (e) {
core.warning(`Could not delete tag ${r.tag_name}: ${e.message}`);
}
}
- name: Update PR labels
if: success()
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = context.payload.pull_request.number;
const labelsToCreate = [
{ name: 'released', color: '0075ca' },
{ name: 'released on @main', color: '0052cc' }
];
for (const label of labelsToCreate) {
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label.name
});
} catch {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label.name,
color: label.color
});
}
}
await github.rest.issues.removeAllLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber
});
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: ['released', 'released on @main']
});