@npmcli/template-oss 4.11.3 → 4.12.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.
@@ -4,7 +4,7 @@ const core = require('@actions/core')
4
4
  const main = require('../lib/release-please/index.js')
5
5
 
6
6
  const dryRun = !process.env.CI
7
- const [branch, eventName] = process.argv.slice(2)
7
+ const [branch, forcePullRequest] = process.argv.slice(2)
8
8
 
9
9
  const debugPr = (val) => {
10
10
  if (dryRun) {
@@ -45,7 +45,7 @@ main({
45
45
  repo: process.env.GITHUB_REPOSITORY,
46
46
  dryRun,
47
47
  branch,
48
- force: eventName === 'workflow_dispatch',
48
+ forcePullRequest: forcePullRequest ? +forcePullRequest : null,
49
49
  }).then(({ pr, release, releases }) => {
50
50
  if (pr) {
51
51
  debugPr(pr)
@@ -2,11 +2,10 @@ GitHub takes the security of our software products and services seriously, inclu
2
2
 
3
3
  If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways.
4
4
 
5
- If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly using [private vulnerability reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability).
5
+ If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [opensource-security@github.com](mailto:opensource-security@github.com).
6
6
 
7
7
  If the vulnerability you have found is [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) and you would like for your finding to be considered for a bounty reward, please submit the vulnerability to us through [HackerOne](https://hackerone.com/github) in order to be eligible to receive a bounty award.
8
8
 
9
9
  **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
10
10
 
11
11
  Thanks for helping make GitHub safe for everyone.
12
-
@@ -27,11 +27,11 @@ jobs:
27
27
  run: |
28
28
  dependabot_dir="$\{{ steps.metadata.outputs.directory }}"
29
29
  if [[ "$dependabot_dir" == "/" ]]; then
30
- echo "::set-output name=workspace::-iwr"
30
+ echo "workspace=-iwr" >> $GITHUB_OUTPUT
31
31
  else
32
32
  # strip leading slash from directory so it works as a
33
33
  # a path to the workspace flag
34
- echo "::set-output name=workspace::-w ${dependabot_dir#/}"
34
+ echo "workspace=-w ${dependabot_dir#/}" >> $GITHUB_OUTPUT
35
35
  fi
36
36
 
37
37
  - name: Apply Changes
@@ -40,7 +40,7 @@ jobs:
40
40
  run: |
41
41
  {{ rootNpmPath }} run template-oss-apply $\{{ steps.flags.outputs.workspace }}
42
42
  if [[ `git status --porcelain` ]]; then
43
- echo "::set-output name=changes::true"
43
+ echo "changes=true" >> $GITHUB_OUTPUT
44
44
  fi
45
45
  # This only sets the conventional commit prefix. This workflow can't reliably determine
46
46
  # what the breaking change is though. If a BREAKING CHANGE message is required then
@@ -50,7 +50,7 @@ jobs:
50
50
  else
51
51
  prefix='chore'
52
52
  fi
53
- echo "::set-output name=message::$prefix: postinstall for dependabot template-oss PR"
53
+ echo "message=$prefix: postinstall for dependabot template-oss PR" >> $GITHUB_OUTPUT
54
54
 
55
55
  # This step will fail if template-oss has made any workflow updates. It is impossible
56
56
  # for a workflow to update other workflows. In the case it does fail, we continue
@@ -2,6 +2,10 @@ name: Release
2
2
 
3
3
  on:
4
4
  workflow_dispatch:
5
+ inputs:
6
+ release-pr:
7
+ description: a release PR number to rerun release jobs on
8
+ type: string
5
9
  push:
6
10
  branches:
7
11
  {{#each branches}}
@@ -30,7 +34,7 @@ jobs:
30
34
  env:
31
35
  GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
32
36
  run: |
33
- {{ rootNpxPath }} --offline template-oss-release-please $\{{ github.ref_name }} $\{{ github.event_name }}
37
+ {{ rootNpxPath }} --offline template-oss-release-please "$\{{ github.ref_name }}" "$\{{ inputs.release-pr }}"
34
38
  - name: Post Pull Request Comment
35
39
  if: steps.release.outputs.pr-number
36
40
  uses: actions/github-script@v6
@@ -53,7 +57,7 @@ jobs:
53
57
  body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Update This Release\n\n`
54
58
  body += `This PR will be updated and CI will run for every non-\`chore:\` commit that is pushed to \`{{ defaultBranch }}\`. `
55
59
  body += `To force CI to update this PR, run this command:\n\n`
56
- body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo}\n\`\`\``
60
+ body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo} -f release-pr=${issue_number}\n\`\`\``
57
61
 
58
62
  if (commentId) {
59
63
  await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body })
@@ -90,7 +94,7 @@ jobs:
90
94
  run: |
91
95
  git commit --all --amend --no-edit || true
92
96
  git push --force-with-lease
93
- echo "::set-output name=sha::$(git rev-parse HEAD)"
97
+ echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
94
98
  {{> stepChecks jobName="Update - Release" jobCheck=(obj sha="steps.commit.outputs.sha" name="Release" )}}
95
99
  {{> stepChecks jobCheck=(obj id="needs.release.outputs.check-id" )}}
96
100
 
@@ -117,7 +121,7 @@ jobs:
117
121
  else
118
122
  result="success"
119
123
  fi
120
- echo "::set-output name=result::$result"
124
+ echo "result=$result" >> $GITHUB_OUTPUT
121
125
  {{> stepChecks jobCheck=(obj id="needs.update.outputs.check-id" status="steps.needs-result.outputs.result") }}
122
126
 
123
127
  post-release:
@@ -139,14 +143,17 @@ jobs:
139
143
  }
140
144
 
141
145
  const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number })
142
- const releaseComments = comments.filter(c => c.user.login === 'github-actions[bot]' && c.body.includes('Release is at'))
146
+ .then(cs => cs.map(c => ({ id: c.id, login: c.user.login, body: c.body })))
147
+ console.log(`Found comments: ${JSON.stringify(comments, null, 2)}`)
148
+ const releaseComments = comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at'))
143
149
 
144
150
  for (const comment of releaseComments) {
151
+ console.log(`Release comment: ${JSON.stringify(comment, null, 2)}`)
145
152
  await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id })
146
153
  }
147
154
 
148
155
  const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`
149
- await github.rest.issues.createComment({
156
+ await github.rest.issues.createComment({
150
157
  owner,
151
158
  repo,
152
159
  issue_number,
@@ -165,7 +172,6 @@ jobs:
165
172
  - name: Get Needs Result
166
173
  id: needs-result
167
174
  run: |
168
- result=""
169
175
  if [[ "$\{{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
170
176
  result="x"
171
177
  elif [[ "$\{{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
@@ -173,7 +179,7 @@ jobs:
173
179
  else
174
180
  result="white_check_mark"
175
181
  fi
176
- echo "::set-output name=result::$result"
182
+ echo "result=$result" >> $GITHUB_OUTPUT
177
183
  - name: Update Release PR Comment
178
184
  uses: actions/github-script@v6
179
185
  env:
@@ -182,15 +188,20 @@ jobs:
182
188
  with:
183
189
  script: |
184
190
  const { PR_NUMBER: issue_number, RESULT } = process.env
185
- const { repo: { owner, repo } } = context
191
+ const { runId, repo: { owner, repo } } = context
186
192
 
187
193
  const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number })
188
- const updateComment = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith('## Release Workflow\n\n'))
194
+ const updateComment = comments.find(c =>
195
+ c.user.login === 'github-actions[bot]' &&
196
+ c.body.startsWith('## Release Workflow\n\n') &&
197
+ c.body.includes(runId)
198
+ )
189
199
 
190
200
  if (updateComment) {
191
201
  console.log('Found comment to update:', JSON.stringify(updateComment, null, 2))
192
202
  let body = updateComment.body.replace(/Workflow run: :[a-z_]+:/, `Workflow run: :${RESULT}:`)
193
- if (RESULT === 'x') {
203
+ const tagCodeowner = RESULT !== 'white_check_mark'
204
+ if (tagCodeowner) {
194
205
  body += `\n\n:rotating_light:`
195
206
  body += ` {{ codeowner }}: The post-release workflow failed for this release.`
196
207
  body += ` Manual steps may need to be taken after examining the workflow output`
@@ -19,7 +19,7 @@ module.exports = class ChangelogNotes {
19
19
  }
20
20
 
21
21
  // A link to the pull request if the commit has one
22
- const prNumber = commit.pullRequest && commit.pullRequest.number
22
+ const prNumber = commit.pullRequest?.number
23
23
  if (prNumber) {
24
24
  entry.push(link(`#${prNumber}`, this.gh.pull(prNumber)))
25
25
  }
@@ -63,7 +63,16 @@ module.exports = class ChangelogNotes {
63
63
 
64
64
  // Group commits by type
65
65
  for (const commit of commits) {
66
- const { entry, breaking } = this.buildEntry(commit, authorsByCommit[commit.sha])
66
+ // when rebase merging multiple commits with a single PR, only the first commit
67
+ // will have a pr number when coming from release-please. this check will manually
68
+ // lookup commits without a pr number and find one if it exists
69
+ if (!commit.pullRequest?.number) {
70
+ commit.pullRequest = { number: await this.gh.commitPrNumber(commit) }
71
+ }
72
+ const { entry, breaking } = this.buildEntry(
73
+ commit,
74
+ authorsByCommit[commit.sha]
75
+ )
67
76
 
68
77
  // Collect commits by type
69
78
  changelog[commit.type].entries.push(entry)
@@ -45,10 +45,25 @@ module.exports = (gh) => {
45
45
  }
46
46
  }
47
47
 
48
+ const commitPrNumber = async (commit) => {
49
+ try {
50
+ const res = await gh.octokit.rest.repos.listPullRequestsAssociatedWithCommit({
51
+ owner,
52
+ repo,
53
+ commit_sha: commit.sha,
54
+ per_page: 1,
55
+ })
56
+ return res.data[0].number
57
+ } catch {
58
+ return null
59
+ }
60
+ }
61
+
48
62
  const url = (...p) => `https://github.com/${owner}/${repo}/${p.join('/')}`
49
63
 
50
64
  return {
51
65
  authors,
66
+ commitPrNumber,
52
67
  pull: (number) => url('pull', number),
53
68
  commit: (sha) => url('commit', sha),
54
69
  compare: (a, b) => a ? url('compare', `${a.toString()}...${b.toString()}`) : null,
@@ -4,26 +4,33 @@ const ChangelogNotes = require('./changelog.js')
4
4
  const Version = require('./version.js')
5
5
  const NodeWs = require('./node-workspace.js')
6
6
 
7
- RP.setLogger(new CheckpointLogger(true, true))
7
+ const logger = new CheckpointLogger(true, true)
8
+ RP.setLogger(logger)
8
9
  RP.registerChangelogNotes('default', (o) => new ChangelogNotes(o))
9
10
  RP.registerVersioningStrategy('default', (o) => new Version(o))
10
11
  RP.registerPlugin('node-workspace', (o) => new NodeWs(o.github, o.targetBranch, o.repositoryConfig))
11
12
 
12
- const main = async ({ repo: _fullRepo, token, dryRun, branch, force }) => {
13
- if (!token) {
14
- throw new Error('Token is required')
13
+ const omit = (obj, ...keys) => {
14
+ const res = {}
15
+ for (const [key, value] of Object.entries(obj)) {
16
+ if (!keys.includes(key)) {
17
+ res[key] = value
18
+ }
15
19
  }
20
+ return res
21
+ }
16
22
 
17
- if (!_fullRepo) {
18
- throw new Error('Repo is required')
19
- }
23
+ const getManifest = async ({ repo: fullRepo, token, branch }) => {
24
+ const fullRepoParts = fullRepo.split('/')
25
+ const github = await RP.GitHub.create({
26
+ owner: fullRepoParts[0],
27
+ repo: fullRepoParts[1],
28
+ token,
29
+ })
20
30
 
21
- const fullRepo = _fullRepo.split('/')
22
- const github = await RP.GitHub.create({ owner: fullRepo[0], repo: fullRepo[1], token })
23
- const {
24
- octokit,
25
- repository: { owner, repo, defaultBranch },
26
- } = github
31
+ const { octokit, repository: { owner, repo, defaultBranch } } = github
32
+
33
+ const baseBranch = branch ?? defaultBranch
27
34
 
28
35
  // This is mostly for testing and debugging. Use environs with the
29
36
  // format `RELEASE_PLEASE_<manfiestOverrideConfigName>` (eg
@@ -33,8 +40,6 @@ const main = async ({ repo: _fullRepo, token, dryRun, branch, force }) => {
33
40
  .filter(([k, v]) => k.startsWith('RELEASE_PLEASE_') && v != null)
34
41
  .map(([k, v]) => [k.replace('RELEASE_PLEASE_', ''), v])
35
42
 
36
- const baseBranch = branch ?? defaultBranch
37
-
38
43
  const manifest = await RP.Manifest.fromManifest(
39
44
  github,
40
45
  baseBranch,
@@ -43,79 +48,213 @@ const main = async ({ repo: _fullRepo, token, dryRun, branch, force }) => {
43
48
  Object.fromEntries(manifestOverrides)
44
49
  )
45
50
 
46
- if (force) {
47
- const { data: releasePrs } = await octokit.pulls.list({
48
- owner,
49
- repo,
50
- head: `release-please--branches--${baseBranch}`,
51
- })
51
+ return {
52
+ github,
53
+ manifest,
54
+ octokit,
55
+ owner,
56
+ repo,
57
+ baseBranch,
58
+ }
59
+ }
52
60
 
53
- if (releasePrs.length !== 1) {
54
- throw new Error(`Found ${releasePrs.length} matching PRs, expected 1`)
61
+ const getReleasesFromPr = async ({ manifest, github, number }) => {
62
+ const baseUrl = `https://github.com/${github.repository.owner}/${github.repository.repo}`
63
+ // get the release please formatted pull request
64
+ let pullRequest
65
+ const prGenerator = github.pullRequestIterator(this.targetBranch, 'MERGED', 200, false)
66
+ for await (const pr of prGenerator) {
67
+ if (pr.number === number) {
68
+ pullRequest = pr
69
+ break
55
70
  }
71
+ }
72
+ const strategiesByPath = await manifest.getStrategiesByPath()
73
+ const releases = []
74
+ for (const path in manifest.repositoryConfig) {
75
+ const config = manifest.repositoryConfig[path]
76
+ const release = await strategiesByPath[path].buildRelease(pullRequest)
77
+ if (release) {
78
+ const { tag, ...rest } = release
79
+ releases.push({
80
+ ...rest,
81
+ ...tag.version,
82
+ tagName: tag.toString(),
83
+ version: tag.version.toString(),
84
+ path,
85
+ draft: false,
86
+ url: `${baseUrl}/releases/tag/${tag.toString()}`,
87
+ prerelease: config.prerelease && !!tag.version.preRelease,
88
+ })
89
+ }
90
+ }
91
+ return releases
92
+ }
93
+
94
+ const getReleaseArtifacts = async ({ dryRun, manifest, forceReleases }) => {
95
+ let pullRequests = []
96
+ let releases = []
97
+
98
+ if (forceReleases) {
99
+ releases = forceReleases
100
+ } else if (dryRun) {
101
+ pullRequests = await manifest.buildPullRequests()
102
+ releases = await manifest.buildReleases()
103
+ } else {
104
+ pullRequests = await manifest.createPullRequests()
105
+ releases = await manifest.createReleases()
106
+ }
107
+
108
+ return {
109
+ pullRequests: pullRequests.filter(Boolean),
110
+ releases: releases.filter(Boolean),
111
+ }
112
+ }
113
+
114
+ // XXX(hack): to get release please to recreate a pull request it needs
115
+ // to have a different body string so we append a message a message that CI
116
+ // is running. This will force release-please to rebase the PR but it
117
+ // wont update the body again, so we only append to it.
118
+ const touchPullRequest = async ({ octokit, owner, repo, releasePr }) => {
119
+ const id = process.env.GITHUB_RUN_ID
120
+ ? `by https://github.com/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`
121
+ : `manually starting at ${new Date().toJSON()}`
122
+
123
+ await octokit.pulls.update({
124
+ owner,
125
+ repo,
126
+ pull_number: releasePr.number,
127
+ body: `${releasePr.body.trim()}\n- This PR is being recreated ${id}`,
128
+ })
129
+ }
130
+
131
+ const main = async ({ repo: fullRepo, token, dryRun, branch, forcePullRequest }) => {
132
+ if (!token) {
133
+ throw new Error('Token is required')
134
+ }
135
+
136
+ if (!fullRepo) {
137
+ throw new Error('Repo is required')
138
+ }
139
+
140
+ const {
141
+ github,
142
+ octokit,
143
+ manifest,
144
+ owner,
145
+ repo,
146
+ baseBranch,
147
+ } = await getManifest({ repo: fullRepo, token, branch })
56
148
 
57
- const [releasePr] = releasePrs
58
- const id = process.env.GITHUB_RUN_ID
59
- ? `by https://github.com/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`
60
- : `manually starting at ${new Date().toJSON()}`
149
+ let forceReleases = null
61
150
 
62
- // XXX(hack): to get release please to recreate a pull request it needs
63
- // to have a different body string so we append a message a message that CI
64
- // is running. This will force release-please to rebase the PR but it
65
- // wont update the body again, so we only append to it.
66
- await octokit.pulls.update({
151
+ if (forcePullRequest) {
152
+ const { data: releasePr } = await octokit.rest.pulls.get({
67
153
  owner,
68
154
  repo,
69
- pull_number: releasePr.number,
70
- body: `${releasePr.body.trim()}\n- This PR is being recreated ${id}`,
155
+ pull_number: forcePullRequest,
71
156
  })
157
+
158
+ if (!releasePr) {
159
+ throw new Error(`Could not find PR from number: ${forcePullRequest}`)
160
+ }
161
+
162
+ if (releasePr.state === 'open') {
163
+ await touchPullRequest({ octokit, owner, repo, releasePr })
164
+ } else if (releasePr.state === 'closed' && releasePr.merged) {
165
+ forceReleases = await getReleasesFromPr({ manifest, github, number: releasePr.number })
166
+ } else {
167
+ throw new Error(`Could not run workflow on PR with wrong state: ${JSON.stringify(
168
+ releasePr,
169
+ null,
170
+ 2
171
+ )}`)
172
+ }
72
173
  }
73
174
 
74
- const pullRequests = await (dryRun ? manifest.buildPullRequests() : manifest.createPullRequests())
75
- const allReleases = await (dryRun ? manifest.buildReleases() : manifest.createReleases())
175
+ const { pullRequests, releases } = await getReleaseArtifacts({ dryRun, manifest, forceReleases })
76
176
 
77
177
  // We only ever get a single pull request with our current release-please settings
78
- const rootPr = pullRequests.filter(Boolean)?.[0]
178
+ // Update this if we start creating individual PRs per workspace release
179
+ const rootPr = pullRequests[0]
180
+ let rootRelease = releases[0]
181
+
182
+ logger.debug(`pull requests: ${pullRequests.length}`)
183
+ logger.debug(`releases: ${releases.length}`)
184
+
185
+ if (rootPr) {
186
+ logger.debug(`root pr: ${JSON.stringify(omit(rootPr, 'body'), null, 2)}`)
187
+ }
188
+
79
189
  if (rootPr?.number) {
80
190
  const commits = await octokit.paginate(octokit.rest.pulls.listCommits, {
81
191
  owner,
82
192
  repo,
83
193
  pull_number: rootPr.number,
84
194
  })
85
- rootPr.sha = commits?.[commits.length - 1]?.sha
86
- }
87
195
 
88
- const releases = allReleases.filter(Boolean)
89
- let rootRelease = releases[0]
196
+ const prSha = commits?.[commits.length - 1]?.sha
197
+ if (!prSha) {
198
+ throw new Error(`Could not find a latest sha for pull request: ${rootPr.number}`)
199
+ }
200
+
201
+ rootPr.sha = prSha
202
+ }
90
203
 
91
204
  for (const release of releases) {
92
- const prefix = release.path === '.' ? '' : release.path
205
+ const { path, sha } = release
206
+ const prefix = path === '.' ? '' : path
207
+ const isRoot = !prefix
208
+ const packagePath = `${prefix}/package.json`
93
209
 
94
- if (!prefix) {
95
- rootRelease = release
210
+ logger.debug(`release: ${JSON.stringify({
211
+ ...omit(release, 'notes'),
212
+ isRoot,
213
+ prefix,
214
+ }, null, 2)}`)
215
+
216
+ const releasePrNumber = await octokit.rest.repos.listPullRequestsAssociatedWithCommit({
217
+ owner,
218
+ repo,
219
+ commit_sha: sha,
220
+ per_page: 1,
221
+ }).then(r => r.data[0]?.number)
222
+
223
+ if (!releasePrNumber) {
224
+ throw new Error(`Could not find release PR number from commit: "${sha}"`)
96
225
  }
97
226
 
98
- const [releasePr, releasePkg] = await Promise.all([
99
- octokit.rest.repos.listPullRequestsAssociatedWithCommit({
100
- owner,
101
- repo,
102
- commit_sha: release.sha,
103
- }).then(r => r.data[0]),
104
- octokit.rest.repos.getContent({
105
- owner,
106
- repo,
107
- ref: baseBranch,
108
- path: `${prefix}/package.json`,
109
- }).then(r => JSON.parse(Buffer.from(r.data.content, r.data.encoding))),
110
- ])
111
-
112
- release.prNumber = releasePr.number
113
- release.pkgName = releasePkg.name
227
+ logger.debug(`pr from ${sha}: ${releasePrNumber}`)
228
+
229
+ const releasePkgName = await octokit.rest.repos.getContent({
230
+ owner,
231
+ repo,
232
+ ref: baseBranch,
233
+ path: packagePath,
234
+ }).then(r => {
235
+ try {
236
+ return JSON.parse(Buffer.from(r.data.content, r.data.encoding)).name
237
+ } catch {
238
+ return null
239
+ }
240
+ })
241
+
242
+ if (!releasePkgName) {
243
+ throw new Error(`Could not find package name for release at: "${packagePath}#${baseBranch}"`)
244
+ }
245
+
246
+ logger.debug(`pkg name from ${packagePath}#${baseBranch}: "${releasePkgName}"`)
247
+
248
+ release.prNumber = releasePrNumber
249
+ release.pkgName = releasePkgName
250
+ if (isRoot) {
251
+ rootRelease = release
252
+ }
114
253
  }
115
254
 
116
255
  return {
117
- pr: rootPr,
118
- release: rootRelease,
256
+ pr: rootPr ?? null,
257
+ release: rootRelease ?? null,
119
258
  releases: releases.length ? releases : null,
120
259
  }
121
260
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/template-oss",
3
- "version": "4.11.3",
3
+ "version": "4.12.0",
4
4
  "description": "templated files used in npm CLI team oss projects",
5
5
  "main": "lib/content/index.js",
6
6
  "bin": {