-
Notifications
You must be signed in to change notification settings - Fork 9
102 lines (91 loc) · 4.96 KB
/
Copy pathchangeset-from-pr.yml
File metadata and controls
102 lines (91 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
name: Changeset from PR template
# Auto-generates the changeset for a PR from its "## Release" section (the same
# section the ready-for-review gate enforces), so engineers never hand-run
# `npx changeset`. Reads the ticked version bump (minor/patch) + the release notes
# and commits `.changeset/pr-<number>.md` to the PR branch. Idempotent: re-runs on
# every body edit and only commits when the derived changeset actually changes, so
# the bot's own push (which re-fires `synchronize`) settles in one no-op pass.
#
# Scope: skips forks (no write token — they add a changeset manually), the
# changesets "Version Packages" PR (head `changeset-release/*`), and PRs labelled
# `skip-changeset` (CI/docs/chore). The ready-for-review gate still independently
# REQUIRES the bump + internal notes; this workflow only GENERATES when they're valid.
on:
pull_request:
types: [opened, edited, synchronize, reopened, labeled, unlabeled]
concurrency:
group: changeset-from-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: write
jobs:
generate:
if: >-
github.event.pull_request.state == 'open' &&
github.event.pull_request.head.repo.full_name == github.repository &&
!startsWith(github.event.pull_request.head.ref, 'changeset-release/') &&
!contains(github.event.pull_request.labels.*.name, 'skip-changeset')
runs-on: ubuntu-latest
steps:
- name: Derive + commit the changeset from the Release section
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const PKG = '@wdio/browserstack-service';
const pr = context.payload.pull_request;
const body = pr.body || '';
const head = pr.head.ref;
// --- isolate the "## Release" block (mirrors ready-for-review-label.yml) ---
const releaseBlock = body.split(/(?=^## )/m)
.find(s => /^##[ \t]*Release[ \t]*(\(|$)/m.test(s)) || '';
if (!releaseBlock) {
core.info('No "## Release" section in the PR body — the ready-for-review gate flags this. Skipping changeset generation.');
return;
}
// Count bump ticks only BEFORE the first "Release notes (" header, so a
// checkbox-looking bullet inside the notes can never be mistaken for a bump.
const bumpRegion = releaseBlock.split(/^\*\*Release notes \(/m)[0];
const bumps = [...bumpRegion.matchAll(/- \[[xX]\]\s*(minor|patch)(?=\s|\(|$)/g)].map(m => m[1].toLowerCase());
if (bumps.length !== 1) {
core.info(`Expected exactly one version-bump tick (minor|patch), found ${bumps.length}. Skipping (ready-for-review gate flags this).`);
return;
}
const bump = bumps[0];
// Extract non-empty bullets under a "**Release notes (<label>):**" header,
// stopping at the next bold header; strip the header-line remainder + HTML comments.
const extractNotes = (label) => {
const parts = releaseBlock.split(new RegExp('^\\*\\*Release notes \\(' + label + '\\):\\*\\*', 'm'));
if (parts.length < 2) return '';
const region = parts[1].replace(/^[^\n]*\n?/, '').split(/^\*\*/m)[0].replace(/<!--[\s\S]*?-->/g, '');
return region.split('\n').map(l => l.trim())
.filter(l => /^[-*]\s+\S/.test(l))
.map(l => l.replace(/^[-*]\s+/, '- '))
.join('\n');
};
// Public CHANGELOG prefers the customer-facing notes; fall back to the
// (required) internal notes, then the PR title as a last resort.
const summary = extractNotes('customer-facing') || extractNotes('internal') || `- ${pr.title}`;
const content = `---\n"${PKG}": ${bump}\n---\n\n${summary}\n`;
const filePath = `.changeset/pr-${pr.number}.md`;
// Idempotent: only write when the derived changeset differs from what's on the branch.
let sha;
try {
const cur = await github.rest.repos.getContent({ ...context.repo, path: filePath, ref: head });
sha = cur.data.sha;
const existing = Buffer.from(cur.data.content, 'base64').toString('utf8');
if (existing === content) {
core.info('Changeset already up to date — no commit.');
return;
}
} catch (e) {
if (e.status !== 404) throw e; // 404 = file not yet present
}
await github.rest.repos.createOrUpdateFileContents({
...context.repo,
path: filePath,
branch: head,
message: `chore(changeset): auto-generate from PR template (${bump})`,
content: Buffer.from(content).toString('base64'),
...(sha ? { sha } : {}),
});
core.notice(`Wrote ${filePath} — bump: ${bump}\n${summary}`);