@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.
- package/README.md +20 -0
- package/bin/release-manager.js +23 -277
- package/bin/release-please.js +20 -55
- package/lib/config.js +6 -5
- package/lib/content/_job-release-integration-yml.hbs +27 -35
- package/lib/content/_step-node-yml.hbs +3 -48
- package/lib/content/_steps-setup-yml.hbs +10 -1
- package/lib/content/action-create-check-yml.hbs +50 -0
- package/lib/content/action-install-latest-npm-yml.hbs +55 -0
- package/lib/content/ci-release-yml.hbs +16 -4
- package/lib/content/index.js +12 -1
- package/lib/content/package-json.hbs +13 -3
- package/lib/content/pull-request-yml.hbs +2 -4
- package/lib/content/release-integration-yml.hbs +24 -0
- package/lib/content/release-please-config-json.hbs +13 -3
- package/lib/content/release-yml.hbs +140 -112
- package/lib/release/changelog.js +184 -0
- package/lib/release/node-workspace-format.js +107 -0
- package/lib/release/node-workspace.js +95 -0
- package/lib/release/release-manager.js +287 -0
- package/lib/release/release-please.js +180 -0
- package/lib/release/util.js +42 -0
- package/package.json +12 -4
- package/lib/content/_step-checks-yml.hbs +0 -54
- package/lib/release-please/changelog.js +0 -92
- package/lib/release-please/github.js +0 -72
- package/lib/release-please/index.js +0 -262
- package/lib/release-please/node-workspace.js +0 -192
- package/lib/release-please/util.js +0 -14
- package/lib/release-please/version.js +0 -103
|
@@ -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
|