@npmcli/template-oss 4.20.0 → 4.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,184 @@
1
+ const { link, code, wrapSpecs, list, formatDate, makeGitHubUrl } = require('./util')
2
+
3
+ class Changelog {
4
+ static BREAKING = 'breaking'
5
+
6
+ #title
7
+ #entries = {}
8
+ #types = new Set([Changelog.BREAKING])
9
+ #titles = {
10
+ [Changelog.BREAKING]: '⚠️ BREAKING CHANGES',
11
+ }
12
+
13
+ constructor ({ version, url, sections }) {
14
+ this.#title = `## ${url ? link(version, url) : version} (${formatDate()})`
15
+ for (const section of sections) {
16
+ this.#types.add(section.type)
17
+ this.#titles[section.type] = section.section
18
+ }
19
+ }
20
+
21
+ add (type, ...entries) {
22
+ if (!this.#types.has(type) || !entries.length) {
23
+ return
24
+ }
25
+ this.#entries[type] ??= []
26
+ this.#entries[type].push(...entries)
27
+ }
28
+
29
+ toString () {
30
+ const body = [this.#title]
31
+ for (const type of this.#types) {
32
+ const title = this.#titles[type]
33
+ if (this.#entries[type]?.length) {
34
+ body.push(
35
+ `### ${title}`,
36
+ this.#entries[type].map(list).join('\n')
37
+ )
38
+ }
39
+ }
40
+ return body.join('\n\n').trim()
41
+ }
42
+ }
43
+
44
+ class ChangelogNotes {
45
+ #owner
46
+ #repo
47
+ #rest
48
+ #graphql
49
+ #ghUrl
50
+
51
+ constructor (github) {
52
+ this.#owner = github.repository.owner
53
+ this.#repo = github.repository.repo
54
+ this.#rest = github.octokit.rest
55
+ this.#graphql = github.graphql
56
+ this.#ghUrl = makeGitHubUrl(this.#owner, this.#repo)
57
+ }
58
+
59
+ async #getAuthorsForCommits (commits) {
60
+ const shas = commits
61
+ .filter(c => c.type !== 'deps')
62
+ .map(c => c.sha)
63
+ .filter(Boolean)
64
+
65
+ if (!shas.length) {
66
+ return {}
67
+ }
68
+
69
+ const authorsByCommit = {}
70
+ const { repository } = await this.#graphql(
71
+ `fragment CommitAuthors on GitObject {
72
+ ... on Commit {
73
+ authors (first:10) {
74
+ nodes {
75
+ user { login }
76
+ name
77
+ }
78
+ }
79
+ }
80
+ }
81
+ query {
82
+ repository (owner:"${this.#owner}", name:"${this.#repo}") {
83
+ ${shas.map((s) => {
84
+ return `_${s}: object (expression: "${s}") { ...CommitAuthors }`
85
+ })}
86
+ }
87
+ }`
88
+ )
89
+ for (const [key, commit] of Object.entries(repository)) {
90
+ if (commit) {
91
+ authorsByCommit[key.slice(1)] = commit.authors.nodes
92
+ .map((a) => a.user && a.user.login ? `@${a.user.login}` : a.name)
93
+ .filter(Boolean)
94
+ }
95
+ }
96
+ return authorsByCommit
97
+ }
98
+
99
+ async #getPullRequestForCommits (commits) {
100
+ const shas = commits
101
+ .filter(c => !c.pullRequest?.number)
102
+ .map(c => c.sha)
103
+ .filter(Boolean)
104
+
105
+ if (!shas.length) {
106
+ return {}
107
+ }
108
+
109
+ const pullRequestsByCommit = {}
110
+ for (const sha of shas) {
111
+ pullRequestsByCommit[sha] = await this.#rest.repos.listPullRequestsAssociatedWithCommit({
112
+ owner: this.#owner,
113
+ repo: this.#repo,
114
+ commit_sha: sha,
115
+ per_page: 1,
116
+ })
117
+ .then((r) => r.data[0].number)
118
+ .catch(() => null)
119
+ }
120
+ return pullRequestsByCommit
121
+ }
122
+
123
+ #buildEntry (commit, { authors = [], pullRequest }) {
124
+ const entry = []
125
+
126
+ if (commit.sha) {
127
+ // A link to the commit
128
+ entry.push(link(code(commit.sha.slice(0, 7)), this.#ghUrl('commit', commit.sha)))
129
+ }
130
+
131
+ // A link to the pull request if the commit has one
132
+ const commitPullRequest = commit.pullRequest?.number ?? pullRequest
133
+ if (commitPullRequest) {
134
+ entry.push(link(`#${commitPullRequest}`, this.#ghUrl('pull', commitPullRequest)))
135
+ }
136
+
137
+ // The title of the commit, with the optional scope as a prefix
138
+ const scope = commit.scope && `${commit.scope}:`
139
+ const subject = wrapSpecs(commit.bareMessage)
140
+ entry.push([scope, subject].filter(Boolean).join(' '))
141
+
142
+ // A list og the authors github handles or names
143
+ if (authors.length) {
144
+ entry.push(`(${authors.join(', ')})`)
145
+ }
146
+
147
+ return entry.join(' ')
148
+ }
149
+
150
+ async buildNotes (commits, { version, previousTag, currentTag, changelogSections }) {
151
+ // get authors for commits for each sha
152
+ const authorsByCommit = await this.#getAuthorsForCommits(commits)
153
+
154
+ // when rebase merging multiple commits with a single PR, only the first commit
155
+ // will have a pr number when coming from release-please. this check will manually
156
+ // lookup commits without a pr number and find one if it exists
157
+ const pullRequestByCommit = await this.#getPullRequestForCommits(commits)
158
+
159
+ const changelog = new Changelog({
160
+ version,
161
+ url: previousTag
162
+ ? this.#ghUrl('compare', `${previousTag.toString()}...${currentTag.toString()}`)
163
+ : null,
164
+ sections: changelogSections,
165
+ })
166
+
167
+ for (const commit of commits) {
168
+ // Collect commits by type
169
+ changelog.add(commit.type, this.#buildEntry(commit, {
170
+ authors: authorsByCommit[commit.sha],
171
+ pullRequest: pullRequestByCommit[commit.sha],
172
+ }))
173
+
174
+ // And breaking changes to its own section
175
+ changelog.add(Changelog.BREAKING, ...commit.notes
176
+ .filter(n => n.title === 'BREAKING CHANGE')
177
+ .map(n => n.text))
178
+ }
179
+
180
+ return changelog.toString()
181
+ }
182
+ }
183
+
184
+ module.exports = ChangelogNotes
@@ -0,0 +1,107 @@
1
+ const localeCompare = require('@isaacs/string-locale-compare')('en')
2
+ const { ManifestPlugin } = require('release-please/build/src/plugin.js')
3
+ const { addPath } = require('release-please/build/src/plugins/workspace.js')
4
+ const { TagName } = require('release-please/build/src/util/tag-name.js')
5
+ const { ROOT_PROJECT_PATH } = require('release-please/build/src/manifest.js')
6
+ const { DEPS, link, wrapSpecs } = require('./util.js')
7
+
8
+ const WORKSPACE_MESSAGE = (name, version) => `${DEPS}(workspace): ${name}@${version}`
9
+ const WORKSPACE_SCOPE = /(?<scope>workspace): `?(?<name>\S+?)[@\s](?<version>\S+?)`?$/gm
10
+
11
+ module.exports = class extends ManifestPlugin {
12
+ static WORKSPACE_MESSAGE = WORKSPACE_MESSAGE
13
+
14
+ #releasesByPackage = new Map()
15
+ #pathsByComponent = new Map()
16
+
17
+ async preconfigure (strategiesByPath) {
18
+ // First build a list of all releases that will happen based on
19
+ // the conventional commits
20
+ for (const path in strategiesByPath) {
21
+ const component = await strategiesByPath[path].getComponent()
22
+ const packageName = await await strategiesByPath[path].getDefaultPackageName()
23
+ this.#pathsByComponent.set(component, path)
24
+ this.#releasesByPackage.set(packageName, { path, component })
25
+ }
26
+
27
+ return strategiesByPath
28
+ }
29
+
30
+ run (candidates) {
31
+ this.#rewriteWorkspaceChangelogItems(candidates)
32
+ this.#sortReleases(candidates)
33
+ return candidates
34
+ }
35
+
36
+ // I don't like how release-please formats workspace changelog entries
37
+ // so this rewrites them to look like the rest of our changelog. This can't
38
+ // be part of the changelog plugin since they are written after that by the
39
+ // node-workspace plugin. A possible PR to release-please could add an option
40
+ // to customize these or run them through the changelog notes generator.
41
+ #rewriteWorkspaceChangelogItems (candidates) {
42
+ for (const candidate of candidates) {
43
+ for (const release of candidate.pullRequest.body.releaseData) {
44
+ // Update notes with a link to each workspaces release notes
45
+ // now that we have all of the releases in a single pull request
46
+ release.notes =
47
+ release.notes.replace(WORKSPACE_SCOPE, (...args) => {
48
+ const { scope, name, version } = args.pop()
49
+ const { path, component } = this.#releasesByPackage.get(name)
50
+ const { tagSeparator, includeVInTag } = this.repositoryConfig[path]
51
+ const { repository: { owner, repo } } = this.github
52
+ const tag = new TagName(version, component, tagSeparator, includeVInTag).toString()
53
+ const url = `https://github.com/${owner}/${repo}/releases/tag/${tag}`
54
+ return `${link(scope, url)}: ${wrapSpecs(`${name}@${version}`)}`
55
+ })
56
+
57
+ // remove the other release please dependencies list which always starts with
58
+ // the following line and then each line is indented. so we search for the line
59
+ // and remove and indented lines until the next non indented line.
60
+ let foundRemoveStart = false
61
+ let foundRemoveEnd = false
62
+ release.notes = release.notes
63
+ .split('\n')
64
+ .filter((line) => {
65
+ if (line === '* The following workspace dependencies were updated') {
66
+ foundRemoveStart = true
67
+ } else if (foundRemoveStart && !foundRemoveEnd) {
68
+ // TODO: test when inserted dependencies is not the last thing in the changelog
69
+ /* istanbul ignore next */
70
+ if (!line || !line.startsWith(' ')) {
71
+ foundRemoveEnd = true
72
+ }
73
+ }
74
+ // If we found the start, remove all lines until we've found the end
75
+ return foundRemoveStart ? foundRemoveEnd : true
76
+ })
77
+ .join('\n')
78
+
79
+ // Find the associated changelog and update that too
80
+ const path = this.#pathsByComponent.get(release.component)
81
+ for (const update of candidate.pullRequest.updates) {
82
+ if (update.path === addPath(path, 'CHANGELOG.md')) {
83
+ update.updater.changelogEntry = release.notes
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ // Sort root release to the top of the pull request
91
+ // release please pre sorts based on graph order so
92
+ #sortReleases (candidates) {
93
+ for (const candidate of candidates) {
94
+ candidate.pullRequest.body.releaseData.sort((a, b) => {
95
+ const aPath = this.#pathsByComponent.get(a.component)
96
+ const bPath = this.#pathsByComponent.get(b.component)
97
+ if (aPath === ROOT_PROJECT_PATH) {
98
+ return -1
99
+ }
100
+ if (bPath === ROOT_PROJECT_PATH) {
101
+ return 1
102
+ }
103
+ return localeCompare(aPath, bPath)
104
+ })
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,95 @@
1
+ const { NodeWorkspace } = require('release-please/build/src/plugins/node-workspace')
2
+ const { parseConventionalCommits } = require('release-please/build/src/commit')
3
+ const { DEPS } = require('./util')
4
+ const { WORKSPACE_MESSAGE } = require('./node-workspace-format')
5
+
6
+ // This adds a preconfigure method to the release-please node-workspace plugin
7
+ // which fixes https://github.com/googleapis/release-please/issues/2089 for our
8
+ // use case. We should attempt to upstream this to release-please but it
9
+ // fundamentally changes the way the node-workspace plugin behaves so it might
10
+ // not be easy to land. For now we extend the base plugin and add one method
11
+ // which is much better than previously when we needed to fork and maintain
12
+ // release-please ourselves.
13
+ class NpmNodeWorkspace extends NodeWorkspace {
14
+ async preconfigure (strategiesByPath, commitsByPath, releasesByPath) {
15
+ // First build a list of all releases that will happen based on the
16
+ // conventional commits
17
+ const candidates = []
18
+ for (const path in strategiesByPath) {
19
+ const pullRequest = await strategiesByPath[path].buildReleasePullRequest(
20
+ parseConventionalCommits(commitsByPath[path]),
21
+ releasesByPath[path]
22
+ )
23
+ // Release please types say this sometimes will return undefined but I could not
24
+ // get any scenario where that was the case. If it was undefined we would want to
25
+ // just ignore it anyway.
26
+ /* istanbul ignore else */
27
+ if (pullRequest) {
28
+ candidates.push({
29
+ path,
30
+ pullRequest,
31
+ config: this.repositoryConfig[path],
32
+ })
33
+ }
34
+ }
35
+
36
+ // Then build the graph of all those releases + any other connected workspaces
37
+ const { allPackages, candidatesByPackage } = await this.buildAllPackages(candidates)
38
+ const graph = await this.buildGraph(allPackages)
39
+ const packageNamesToUpdate = this.packageNamesToUpdate(graph, candidatesByPackage)
40
+ const graphPackages = this.buildGraphOrder(graph, packageNamesToUpdate)
41
+
42
+ // Then build a list of all the updated versions
43
+ const updatedVersions = {}
44
+ for (const pkg of graphPackages) {
45
+ const path = this.pathFromPackage(pkg)
46
+ const packageName = this.packageNameFromPackage(pkg)
47
+ const existingCandidate = candidatesByPackage[packageName]
48
+
49
+ if (existingCandidate) {
50
+ // If there is an existing pull request use that version
51
+ updatedVersions[packageName] = existingCandidate.pullRequest?.version
52
+ } else {
53
+ // Otherwise build another pull request (that will be discarded) just
54
+ // to see what the version would be if it only contained a deps commit.
55
+ // This is to make sure we use any custom versioning or release strategy.
56
+ const releasePullRequest = await strategiesByPath[path].buildReleasePullRequest(
57
+ parseConventionalCommits([{ message: `${DEPS}: ${Math.random()}` }]),
58
+ releasesByPath[path]
59
+ )
60
+ updatedVersions[packageName] = releasePullRequest?.version
61
+ }
62
+ }
63
+
64
+ // Then go through all the packages again and add deps commits for each
65
+ // updated workspace
66
+ for (const pkg of graphPackages) {
67
+ const path = this.pathFromPackage(pkg)
68
+ const packageName = this.packageNameFromPackage(pkg)
69
+ const graphPackage = graph.get(packageName)
70
+
71
+ // Update dependency versions add a deps commit to each path so it gets
72
+ // processed later. This else never happens in our cases because our
73
+ // packages always have deps, but keeping this around to make it easier to
74
+ // upstream in the future.
75
+ /* istanbul ignore else */
76
+ if (graphPackage.deps) {
77
+ for (const depName of graphPackage.deps) {
78
+ const depVersion = updatedVersions[depName]
79
+ // Same as the above check, we always have a version here but technically
80
+ // we could not in which it would be safe to ignore it.
81
+ /* istanbul ignore else */
82
+ if (depVersion) {
83
+ commitsByPath[path].push({
84
+ message: WORKSPACE_MESSAGE(depName, depVersion.toString()),
85
+ })
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ return strategiesByPath
92
+ }
93
+ }
94
+
95
+ module.exports = NpmNodeWorkspace
@@ -0,0 +1,287 @@
1
+ const { Octokit } = require('@octokit/rest')
2
+ const core = require('@actions/core')
3
+ const { join } = require('path')
4
+ const semver = require('semver')
5
+ const assert = require('assert')
6
+ const dedent = require('dedent')
7
+ const mapWorkspaces = require('@npmcli/map-workspaces')
8
+ const { request: fetch } = require('undici')
9
+ const { getPublishTag, block, noop } = require('./util')
10
+
11
+ class ReleaseManager {
12
+ #octokit
13
+ #owner
14
+ #repo
15
+ #cwd
16
+ #pr
17
+ #backport
18
+ #defaultTag
19
+ #lockfile
20
+ #publish
21
+
22
+ #info
23
+
24
+ constructor ({
25
+ token,
26
+ repo,
27
+ cwd = process.cwd(),
28
+ pr,
29
+ backport,
30
+ defaultTag,
31
+ lockfile,
32
+ publish,
33
+ silent,
34
+ }) {
35
+ assert(token, 'GITHUB_TOKEN is required')
36
+ assert(repo, 'GITHUB_REPOSITORY is required')
37
+ assert(cwd, 'cwd is required')
38
+ assert(pr, 'pr is required')
39
+ assert(defaultTag, 'defaultTag is required')
40
+
41
+ this.#octokit = new Octokit({ auth: token })
42
+ this.#owner = repo.split('/')[0]
43
+ this.#repo = repo.split('/')[1]
44
+ this.#cwd = cwd
45
+ this.#pr = pr
46
+ this.#backport = backport
47
+ this.#defaultTag = defaultTag
48
+ this.#lockfile = lockfile
49
+ this.#publish = publish
50
+
51
+ this.#info = silent ? noop : core.info
52
+ }
53
+
54
+ static async run (options) {
55
+ const manager = new ReleaseManager(options)
56
+ return manager.run()
57
+ }
58
+
59
+ async run () {
60
+ const { data: pullRequest } = await this.#octokit.rest.pulls.get({
61
+ owner: this.#owner,
62
+ repo: this.#repo,
63
+ pull_number: this.#pr,
64
+ })
65
+
66
+ const [release, workspaces] = await this.#getPrReleases({ pullRequest })
67
+ const releaseItems = await this.#getReleaseProcess({ release, workspaces })
68
+
69
+ this.#info(`Filtered ${releaseItems.length} release process items:`)
70
+ this.#info(releaseItems.map(r => r.split('\n')[0].replace('- [ ] ', '')).join(', '))
71
+
72
+ const checklist = releaseItems
73
+ .join('\n\n')
74
+ .replace(/<PR-NUMBER>/g, pullRequest.number)
75
+ .replace(/<RELEASE-BRANCH>/g, pullRequest.head.ref)
76
+ .replace(/<BASE-BRANCH>/g, pullRequest.base.ref)
77
+ .replace(/<MAJOR>/g, release.major)
78
+ .replace(/<X\.Y\.Z>/g, release.version)
79
+ .replace(/<GITHUB-RELEASE-LINK>/g, release.url)
80
+ .replace(/<PUBLISH-FLAGS>/g, release.flags)
81
+ .replace(/^(\s*\S.*)(-w <WS-PKG-N>)$/gm, workspaces.map(w => `$1${w.flags}`).join('\n'))
82
+ .trim()
83
+
84
+ return `### Release Checklist for ${release.tag}\n\n${checklist}`
85
+ }
86
+
87
+ async #getPrReleases ({ pullRequest }) {
88
+ return /<details><summary>.*<\/summary>/.test(pullRequest.body)
89
+ ? await this.#getPrMonoRepoReleases({ pullRequest })
90
+ : [this.#getPrRootRelease({ pullRequest }), []]
91
+ }
92
+
93
+ async #getPrMonoRepoReleases ({ pullRequest }) {
94
+ const releases = pullRequest.body.match(/<details><summary>.*<\/summary>/g)
95
+ this.#info(`Found ${releases.length} releases`)
96
+
97
+ const workspacesComponents = [...await mapWorkspaces({
98
+ cwd: this.#cwd,
99
+ pkg: require(join(this.#cwd, 'package.json')),
100
+ })]
101
+ .reduce((acc, [k]) => {
102
+ const wsComponentName = k.startsWith('@') ? k.split('/')[1] : k
103
+ acc[wsComponentName] = k
104
+ return acc
105
+ }, {})
106
+
107
+ const MONO_VERSIONS = /<details><summary>(?:(.*?):\s)?(.*?)<\/summary>/
108
+
109
+ return releases.reduce((acc, r) => {
110
+ const [, name, version] = r.match(MONO_VERSIONS)
111
+
112
+ const release = this.#getPrReleaseInfo({
113
+ pullRequest,
114
+ name,
115
+ version,
116
+ workspaces: workspacesComponents,
117
+ })
118
+
119
+ if (release.isRoot) {
120
+ this.#info(`Found root: ${JSON.stringify(release)}`)
121
+ acc[0] = release
122
+ } else {
123
+ this.#info(`Found workspace: ${JSON.stringify(release)}`)
124
+ acc[1].push(release)
125
+ }
126
+
127
+ return acc
128
+ }, [null, []])
129
+ }
130
+
131
+ #getPrRootRelease ({ pullRequest }) {
132
+ this.#info('Found no monorepo, checking for single root version')
133
+
134
+ const match = pullRequest.body.match(/\n##\s\[(.*?)\]/)
135
+ assert(match, 'Could not find single root version in body')
136
+
137
+ const version = match[1]
138
+ this.#info(`Found version: ${version}`)
139
+
140
+ return this.#getPrReleaseInfo({ pullRequest, version })
141
+ }
142
+
143
+ #getPrReleaseInfo ({ pullRequest, workspaces = {}, name = null, version: rawVersion }) {
144
+ const version = semver.parse(rawVersion)
145
+ const prerelease = !!version.prerelease.length
146
+ const tag = `${name ? `${name}-` : ''}v${rawVersion}`
147
+ const publishTag = getPublishTag(rawVersion, {
148
+ backport: this.#backport,
149
+ defaultTag: this.#defaultTag,
150
+ })
151
+
152
+ return {
153
+ isRoot: !name,
154
+ tag,
155
+ prerelease,
156
+ version: rawVersion,
157
+ major: version.major,
158
+ url: `https://github.com/${pullRequest.base.repo.full_name}/releases/tag/${tag}`,
159
+ flags: [
160
+ workspaces[name] ? `-w ${workspaces[name]}` : null,
161
+ `--tag=${publishTag}`,
162
+ ].filter(Boolean).join(' '),
163
+ }
164
+ }
165
+
166
+ async #getReleaseProcess ({ release, workspaces }) {
167
+ const RELEASE_LIST_ITEM = /^\d+\.\s/gm
168
+
169
+ this.#info(`Fetching release process from repo wiki: ${this.#owner}/${this.#repo}`)
170
+
171
+ const releaseProcess = await fetch(
172
+ `https://raw.githubusercontent.com/wiki/${this.#owner}/${this.#repo}/Release-Process.md`
173
+ )
174
+ .then(r => {
175
+ // If the url fails with anything but a 404 we want the process to blow
176
+ // up because that means something is very wrong. This is a rare edge
177
+ // case that isn't worth testing.
178
+ /* istanbul ignore else */
179
+ if (r.statusCode === 200) {
180
+ this.#info('Found release process from wiki')
181
+ return r.body.text()
182
+ } else if (r.statusCode === 404) {
183
+ this.#info('No release process found in wiki, falling back to default process')
184
+ return this.#getReleaseSteps()
185
+ } else {
186
+ throw new Error(`Release process fetch failed with status: ${r.statusCode}`)
187
+ }
188
+ })
189
+
190
+ // XXX: the release steps need to always be the last thing in the doc for this to work
191
+ const releaseLines = releaseProcess.split('\n')
192
+ const releaseStartLine = releaseLines.reduce((acc, l, i) => l.match(/^#+\s/) ? i : acc, 0)
193
+ const section = releaseLines.slice(releaseStartLine).join('\n')
194
+
195
+ return section
196
+ .split({
197
+ [Symbol.split]: (str) => {
198
+ const [, ...matches] = str.split(RELEASE_LIST_ITEM)
199
+ this.#info(`Found ${matches.length} release items`)
200
+ return matches.map((m) => `- [ ] <STEP_INDEX>. ${m}`.trim())
201
+ },
202
+ })
203
+ .filter((item) => {
204
+ if (release.prerelease && item.includes('> NOT FOR PRERELEASE')) {
205
+ return false
206
+ }
207
+
208
+ if (!workspaces.length && item.includes('Publish workspaces')) {
209
+ return false
210
+ }
211
+
212
+ return true
213
+ })
214
+ .map((item, index) => item.replace('<STEP_INDEX>', index + 1))
215
+ }
216
+
217
+ #getReleaseSteps () {
218
+ const R = `-R ${this.#owner}/${this.#repo}`
219
+
220
+ const manualSteps = `
221
+ 1. Checkout the release branch and test
222
+
223
+ ${block('sh')}
224
+ gh pr checkout <PR-NUMBER> --force
225
+ npm ${this.#lockfile ? 'ci' : 'update'}
226
+ npm test
227
+ gh pr checks <PR-NUMBER> ${R} --watch
228
+ ${block()}
229
+
230
+ 1. Publish workspaces
231
+
232
+ ${block('sh')}
233
+ npm publish -w <WS-PKG-N>
234
+ ${block()}
235
+
236
+ 1. Publish
237
+
238
+ ${block('sh')}
239
+ npm publish <PUBLISH-FLAGS>
240
+ ${block()}
241
+
242
+ 1. Merge release PR
243
+
244
+ ${block('sh')}
245
+ gh pr merge <PR-NUMBER> ${R} --squash
246
+ git checkout <BASE-BRANCH>
247
+ git fetch
248
+ git reset --hard origin/<BASE-BRANCH>
249
+ ${block()}
250
+ `
251
+
252
+ const autoSteps = `
253
+ 1. Approve this PR
254
+
255
+ ${block('sh')}
256
+ gh pr review <PR-NUMBER> ${R} --approve
257
+ ${block()}
258
+
259
+ 1. Merge release PR :rotating_light: Merging this will auto publish :rotating_light:
260
+
261
+ ${block('sh')}
262
+ gh pr merge <PR-NUMBER> ${R} --squash
263
+ ${block()}
264
+ `
265
+
266
+ /* eslint-disable max-len */
267
+ const alwaysSteps = `
268
+ 1. Check For Release Tags
269
+
270
+ Release Please will run on the just pushed release commit and create GitHub releases and tags for each package.
271
+
272
+ ${block('sh')}
273
+ gh run watch ${R} $(gh run list ${R} -w release -b <BASE-BRANCH> -L 1 --json databaseId -q ".[0].databaseId")
274
+ ${block()}
275
+ `
276
+ /* eslint-enable max-len */
277
+
278
+ return [
279
+ this.#publish ? autoSteps : manualSteps,
280
+ alwaysSteps,
281
+ ]
282
+ .map(v => dedent(v))
283
+ .join('\n\n')
284
+ }
285
+ }
286
+
287
+ module.exports = ReleaseManager