@npmcli/template-oss 3.6.0 → 3.8.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,31 @@
1
+ #!/usr/bin/env node
2
+
3
+ const core = require('@actions/core')
4
+ const main = require('../lib/release-please/index.js')
5
+
6
+ const dryRun = !process.env.CI
7
+ const [branch] = process.argv.slice(2)
8
+
9
+ const setOutput = (key, val) => {
10
+ if (val && (!Array.isArray(val) || val.length)) {
11
+ if (dryRun) {
12
+ console.log(key, JSON.stringify(val, null, 2))
13
+ } else {
14
+ core.setOutput(key, JSON.stringify(val))
15
+ }
16
+ }
17
+ }
18
+
19
+ main({
20
+ token: process.env.GITHUB_TOKEN,
21
+ repo: process.env.GITHUB_REPOSITORY,
22
+ dryRun,
23
+ branch,
24
+ }).then(({ pr, releases, release }) => {
25
+ setOutput('pr', pr)
26
+ setOutput('releases', releases)
27
+ setOutput('release', release)
28
+ return null
29
+ }).catch(err => {
30
+ core.setFailed(`failed: ${err}`)
31
+ })
package/lib/config.js CHANGED
@@ -101,6 +101,12 @@ const getConfig = async ({
101
101
  const derived = {
102
102
  isRoot,
103
103
  isWorkspace: !isRoot,
104
+ // Some files are written to the base of a repo but will
105
+ // include configuration for all packages within a monorepo
106
+ // For these cases it is helpful to know if we are in a
107
+ // monorepo since template-oss might be used only for
108
+ // workspaces and not the root or vice versa.
109
+ isMono: (isRoot && workspaces.length > 0) || !isRoot,
104
110
  // repo
105
111
  repoDir: root,
106
112
  repoFiles,
@@ -12,5 +12,5 @@ jobs:
12
12
  steps:
13
13
  {{> setupGit}}
14
14
  {{> setupNode}}
15
- - run: npm i --ignore-scripts --no-audit --no-fund --package-lock
15
+ {{> setupDeps flags="--package-lock"}}
16
16
  - run: npm audit
@@ -28,7 +28,7 @@ jobs:
28
28
  steps:
29
29
  {{> setupGit}}
30
30
  {{> setupNode}}
31
- - run: npm i --ignore-scripts --no-audit --no-fund
31
+ {{> setupDeps}}
32
32
  - run: npm run lint {{~#if isWorkspace}} -w {{pkgName}}{{/if}}
33
33
 
34
34
  test:
@@ -55,7 +55,7 @@ jobs:
55
55
  steps:
56
56
  {{> setupGit}}
57
57
  {{> setupNode useMatrix=true}}
58
+ {{> setupDeps}}
58
59
  - name: add tap problem matcher
59
60
  run: echo "::add-matcher::.github/matchers/tap.json"
60
- - run: npm i --ignore-scripts --no-audit --no-fund
61
61
  - run: npm test --ignore-scripts {{~#if isWorkspace}} -w {{pkgName}}{{/if}}
@@ -1,5 +1,30 @@
1
1
  const { name: NAME, version: LATEST_VERSION } = require('../../package.json')
2
2
 
3
+ const releasePlease = () => ({
4
+ '.github/workflows/release-please.yml': {
5
+ file: 'release-please.yml',
6
+ filter: (o) => !o.pkg.private,
7
+ },
8
+ '.github/workflows/release-test.yml': {
9
+ file: 'release-test.yml',
10
+ filter: (o) => !o.pkg.private && o.config.releaseTest === 'release-test.yml',
11
+ },
12
+ '.release-please-manifest.json': {
13
+ file: 'release-please-manifest.json',
14
+ filter: (o) => !o.pkg.private,
15
+ parser: (p) => class NoCommentJson extends p.JsonMerge {
16
+ comment = null
17
+ },
18
+ },
19
+ 'release-please-config.json': {
20
+ file: 'release-please-config.json',
21
+ filter: (o) => !o.pkg.private,
22
+ parser: (p) => class NoCommentJson extends p.JsonMerge {
23
+ comment = null
24
+ },
25
+ },
26
+ })
27
+
3
28
  // Changes applied to the root of the repo
4
29
  const rootRepo = {
5
30
  add: {
@@ -14,10 +39,7 @@ const rootRepo = {
14
39
  '.github/workflows/codeql-analysis.yml': 'codeql-analysis.yml',
15
40
  '.github/workflows/post-dependabot.yml': 'post-dependabot.yml',
16
41
  '.github/workflows/pull-request.yml': 'pull-request.yml',
17
- '.github/workflows/release-please.yml': {
18
- file: 'release-please.yml',
19
- filter: (o) => !o.pkg.private,
20
- },
42
+ ...releasePlease(),
21
43
  },
22
44
  }
23
45
 
@@ -43,12 +65,14 @@ const rootModule = {
43
65
  // Changes for each workspace but applied to the root of the repo
44
66
  const workspaceRepo = {
45
67
  add: {
46
- '.github/workflows/release-please-{{pkgNameFs}}.yml': {
47
- file: 'release-please.yml',
48
- filter: (o) => !o.pkg.private,
49
- },
68
+ ...releasePlease(true),
69
+ '.github/matchers/tap.json': 'tap.json',
50
70
  '.github/workflows/ci-{{pkgNameFs}}.yml': 'ci.yml',
51
71
  },
72
+ rm: [
73
+ // These are the old release please files that should be removed now
74
+ '.github/workflows/release-please-{{pkgNameFs}}.yml',
75
+ ],
52
76
  }
53
77
 
54
78
  // Changes for each workspace but applied to the relative workspace dir
@@ -72,6 +96,11 @@ module.exports = {
72
96
  workspaceModule,
73
97
  windowsCI: true,
74
98
  branches: ['main', 'latest'],
99
+ defaultBranch: 'main',
100
+ // Escape hatch since we write a release test file but the
101
+ // CLI has a very custom one we dont want to overwrite. This
102
+ // setting allows us to call a workflow by any name during release
103
+ releaseTest: 'release-test.yml',
75
104
  distPaths: ['bin/', 'lib/'],
76
105
  ciVersions: ['12.13.0', '12.x', '14.15.0', '14.x', '16.0.0', '16.x'],
77
106
  lockfile: false,
@@ -12,22 +12,20 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  if: github.actor == 'dependabot[bot]'
14
14
  steps:
15
- {{> setupGit}}
15
+ {{> setupGit with=(obj ref="${{ github.event.pull_request.head_ref }}")}}
16
16
  {{> setupNode}}
17
+ {{> setupDeps}}
17
18
  - name: Dependabot metadata
18
19
  id: metadata
19
20
  uses: dependabot/fetch-metadata@v1.1.1
20
21
  with:
21
22
  github-token: "$\{{ secrets.GITHUB_TOKEN }}"
22
- - name: npm install and commit
23
+ - name: Apply {{__NAME__}} changes and lint
23
24
  if: contains(steps.metadata.outputs.dependency-names, '{{__NAME__}}')
24
25
  env:
25
26
  GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
26
27
  run: |
27
- gh pr checkout $\{{ github.event.pull_request.number }}
28
- npm install --ignore-scripts --no-audit --no-fund
29
28
  npm run template-oss-apply
30
- git add .
31
29
  git commit -am "chore: postinstall for dependabot template-oss PR"
32
30
  git push
33
31
  npm run lint
@@ -15,11 +15,10 @@ jobs:
15
15
  steps:
16
16
  {{> setupGit with=(obj fetch-depth=0)}}
17
17
  {{> setupNode}}
18
- - name: Install deps
19
- run: npm i -D @commitlint/cli @commitlint/config-conventional
20
- - name: Check commits OR PR title
18
+ {{> setupDeps}}
19
+ - name: Check commits or PR title
21
20
  env:
22
21
  PR_TITLE: $\{{ github.event.pull_request.title }}
23
22
  run: |
24
- npx --offline commitlint -V --from origin/main --to $\{{ github.event.pull_request.head.sha }} \
23
+ npx --offline commitlint -V --from origin/{{defaultBranch}} --to $\{{ github.event.pull_request.head.sha }} \
25
24
  || echo $PR_TITLE | npx --offline commitlint -V
@@ -0,0 +1,13 @@
1
+ {
2
+ "separate-pull-requests": {{{del}}},
3
+ "plugins": {{#if isMono}}["node-workspace", "workspace-deps"]{{else}}{{{del}}}{{/if}},
4
+ "exclude-packages-from-root": true,
5
+ "group-pull-request-title-pattern": "chore: release ${version}",
6
+ "pull-request-title-pattern": "chore: release${component} ${version}",
7
+ "changelog-sections": {{{json changelogTypes}}},
8
+ "packages": {
9
+ "{{#unless pkgRelPath}}.{{/unless}}{{pkgRelPath}}": {
10
+ {{#unless pkgRelPath}}"package-name": ""{{/unless}}
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "{{#unless pkgRelPath}}.{{/unless}}{{pkgRelPath}}": "{{pkg.version}}"
3
+ }
@@ -1,56 +1,70 @@
1
- name: Release Please {{~#if isWorkspace}} - {{pkgName}}{{/if}}
1
+ name: Release Please
2
2
 
3
3
  on:
4
4
  push:
5
- {{#if pkgRelPath}}
6
- paths:
7
- - {{pkgRelPath}}/**
8
- {{/if}}
9
5
  branches:
10
6
  {{#each branches}}
11
7
  - {{.}}
12
8
  {{/each}}
13
9
 
14
- {{#if isWorkspace}}
15
10
  permissions:
16
11
  contents: write
17
12
  pull-requests: write
18
- {{/if}}
19
13
 
20
14
  jobs:
21
15
  release-please:
22
16
  runs-on: ubuntu-latest
17
+ outputs:
18
+ pr: $\{{ steps.release.outputs.pr }}
19
+ release: $\{{ steps.release.outputs.release }}
23
20
  steps:
24
- - uses: google-github-actions/release-please-action@v3
25
- id: release
26
- with:
27
- release-type: node
28
- {{#if pkgRelPath}}
29
- monorepo-tags: true
30
- path: {{pkgRelPath}}
31
- # name can be removed after this is merged
32
- # https://github.com/google-github-actions/release-please-action/pull/459
33
- package-name: "{{pkgName}}"
34
- {{/if}}
35
- changelog-types: >
36
- [
37
- {{#each changelogTypes}}
38
- {{{json .}}}{{#unless @last}},{{/unless}}
39
- {{/each}}
40
- ]
41
- {{#if isWorkspace}}
42
21
  {{> setupGit}}
43
22
  {{> setupNode}}
44
- - name: Update package-lock.json and commit
45
- if: steps.release.outputs.pr
23
+ {{> setupDeps}}
24
+ - name: Release Please
25
+ id: release
26
+ run: npx --offline template-oss-release-please
27
+ env:
28
+ GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
29
+
30
+ post-pr:
31
+ needs: release-please
32
+ if: needs.release-please.outputs.pr
33
+ runs-on: ubuntu-latest
34
+ outputs:
35
+ ref: $\{{ steps.ref.outputs.branch }}
36
+ steps:
37
+ - name: Output ref
38
+ id: ref
39
+ run: echo "::set-output name=branch::$\{{ fromJSON(needs.release-please.outputs.pr).headBranchName }}"
40
+ {{> setupGit with=(obj ref="${{ steps.ref.outputs.branch }}")}}
41
+ {{> setupNode}}
42
+ {{> setupDeps}}
43
+ - name: Post pull request actions
46
44
  env:
47
45
  GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
48
46
  run: |
49
- gh pr checkout $\{{ fromJSON(steps.release.outputs.pr).number }}
50
- npm run resetdeps
51
- title="$\{{ fromJSON(steps.release.outputs.pr).title }}"
52
- # get the version from the pr title
53
- # get everything after the last space
54
- git commit -am "deps: {{pkgName}}@${title##* }"
47
+ npm run rp-pull-request --ignore-scripts --if-present -ws -iwr
48
+ git commit -am "chore: post pull request" || true
55
49
  git push
56
- {{/if}}
50
+
51
+ release-test:
52
+ needs: post-pr
53
+ if: needs.post-pr.outputs.ref
54
+ uses: ./.github/workflows/{{releaseTest}}
55
+ with:
56
+ ref: $\{{ needs.post-pr.outputs.ref }}
57
+
58
+ post-release:
59
+ needs: release-please
60
+ if: needs.release-please.outputs.release
61
+ runs-on: ubuntu-latest
62
+ steps:
63
+ {{> setupGit}}
64
+ {{> setupNode}}
65
+ {{> setupDeps}}
66
+ - name: Post release actions
67
+ env:
68
+ GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
69
+ run: |
70
+ npm run rp-release --ignore-scripts --if-present -ws -iwr
@@ -0,0 +1,46 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ ref:
7
+ required: true
8
+ type: string
9
+
10
+ jobs:
11
+ lint-all:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ {{> setupGit with=(obj ref="${{ inputs.ref }}")}}
15
+ {{> setupNode}}
16
+ {{> setupDeps}}
17
+ - run: npm run lint --if-present --workspaces --include-workspace-root
18
+
19
+ test-all:
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ node-version:
24
+ {{#each ciVersions}}
25
+ - {{.}}
26
+ {{/each}}
27
+ platform:
28
+ - os: ubuntu-latest
29
+ shell: bash
30
+ - os: macos-latest
31
+ shell: bash
32
+ {{#if windowsCI}}
33
+ - os: windows-latest
34
+ shell: cmd
35
+ {{/if}}
36
+ runs-on: $\{{ matrix.platform.os }}
37
+ defaults:
38
+ run:
39
+ shell: $\{{ matrix.platform.shell }}
40
+ steps:
41
+ {{> setupGit with=(obj ref="${{ inputs.ref }}")}}
42
+ {{> setupNode useMatrix=true}}
43
+ {{> setupDeps}}
44
+ - name: add tap problem matcher
45
+ run: echo "::add-matcher::.github/matchers/tap.json"
46
+ - run: npm run test --if-present --workspaces --include-workspace-root
@@ -0,0 +1 @@
1
+ - run: npm i --ignore-scripts --no-audit --no-fund {{~#if flags}} {{flags}}{{/if}}
@@ -0,0 +1,225 @@
1
+ const RP = require('release-please/build/src/changelog-notes/default')
2
+
3
+ module.exports = class DefaultChangelogNotes extends RP.DefaultChangelogNotes {
4
+ constructor (options) {
5
+ super(options)
6
+ this.github = options.github
7
+ }
8
+
9
+ async buildDefaultNotes (commits, options) {
10
+ // The default generator has a title with the version and date
11
+ // and a link to the diff between the last two versions
12
+ const notes = await super.buildNotes(commits, options)
13
+ const lines = notes.split('\n')
14
+
15
+ let foundBreakingHeader = false
16
+ let foundNextHeader = false
17
+ const breaking = lines.reduce((acc, line) => {
18
+ if (line.match(/^### .* BREAKING CHANGES$/)) {
19
+ foundBreakingHeader = true
20
+ } else if (!foundNextHeader && foundBreakingHeader && line.match(/^### /)) {
21
+ foundNextHeader = true
22
+ }
23
+ if (foundBreakingHeader && !foundNextHeader) {
24
+ acc.push(line)
25
+ }
26
+ return acc
27
+ }, []).join('\n')
28
+
29
+ return {
30
+ title: lines[0],
31
+ breaking: breaking.trim(),
32
+ }
33
+ }
34
+
35
+ async buildNotes (commits, options) {
36
+ const { title, breaking } = await this.buildDefaultNotes(commits, options)
37
+ const body = await generateChangelogBody(commits, { github: this.github, ...options })
38
+ return [title, breaking, body].filter(Boolean).join('\n\n')
39
+ }
40
+ }
41
+
42
+ // a naive implementation of console.log/group for indenting console
43
+ // output but keeping it in a buffer to be output to a file or console
44
+ const logger = (init) => {
45
+ let indent = 0
46
+ const step = 2
47
+ const buffer = [init]
48
+ return {
49
+ toString () {
50
+ return buffer.join('\n').trim()
51
+ },
52
+ group (s) {
53
+ this.log(s)
54
+ indent += step
55
+ },
56
+ groupEnd () {
57
+ indent -= step
58
+ },
59
+ log (s) {
60
+ if (!s) {
61
+ buffer.push('')
62
+ } else {
63
+ buffer.push(s.split('\n').map((l) => ' '.repeat(indent) + l).join('\n'))
64
+ }
65
+ },
66
+ }
67
+ }
68
+
69
+ const generateChangelogBody = async (_commits, { github, changelogSections }) => {
70
+ const changelogMap = new Map(
71
+ changelogSections.filter(c => !c.hidden).map((c) => [c.type, c.section])
72
+ )
73
+
74
+ const { repository } = await github.graphql(
75
+ `fragment commitCredit on GitObject {
76
+ ... on Commit {
77
+ message
78
+ url
79
+ abbreviatedOid
80
+ authors (first:10) {
81
+ nodes {
82
+ user {
83
+ login
84
+ url
85
+ }
86
+ email
87
+ name
88
+ }
89
+ }
90
+ associatedPullRequests (first:10) {
91
+ nodes {
92
+ number
93
+ url
94
+ merged
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ query {
101
+ repository (owner:"${github.repository.owner}", name:"${github.repository.repo}") {
102
+ ${_commits.map(({ sha: s }) => `_${s}: object (expression: "${s}") { ...commitCredit }`)}
103
+ }
104
+ }`
105
+ )
106
+
107
+ // collect commits by valid changelog type
108
+ const commits = [...changelogMap.values()].reduce((acc, type) => {
109
+ acc[type] = []
110
+ return acc
111
+ }, {})
112
+
113
+ const allCommits = Object.values(repository)
114
+
115
+ for (const commit of allCommits) {
116
+ // get changelog type of commit or bail if there is not a valid one
117
+ const [, type] = /(^\w+)[\s(:]?/.exec(commit.message) || []
118
+ const changelogType = changelogMap.get(type)
119
+ if (!changelogType) {
120
+ continue
121
+ }
122
+
123
+ const message = commit.message
124
+ .trim() // remove leading/trailing spaces
125
+ .replace(/(\r?\n)+/gm, '\n') // replace multiple newlines with one
126
+ .replace(/([^\s]+@\d+\.\d+\.\d+.*)/gm, '`$1`') // wrap package@version in backticks
127
+
128
+ // the title is the first line of the commit, 'let' because we change it later
129
+ let [title, ...body] = message.split('\n')
130
+
131
+ const prs = commit.associatedPullRequests.nodes.filter((pull) => pull.merged)
132
+
133
+ // external squashed PRs dont get the associated pr node set
134
+ // so we try to grab it from the end of the commit title
135
+ // since thats where it goes by default
136
+ const [, titleNumber] = title.match(/\s+\(#(\d+)\)$/) || []
137
+ if (titleNumber && !prs.find((pr) => pr.number === +titleNumber)) {
138
+ try {
139
+ // it could also reference an issue so we do one extra check
140
+ // to make sure it is really a pr that has been merged
141
+ const { data: realPr } = await github.octokit.pulls.get({
142
+ owner: github.repository.owner,
143
+ repo: github.repository.repo,
144
+ pull_number: titleNumber,
145
+ })
146
+ if (realPr.state === 'MERGED') {
147
+ prs.push(realPr)
148
+ }
149
+ } catch {
150
+ // maybe an issue or something else went wrong
151
+ // not super important so keep going
152
+ }
153
+ }
154
+
155
+ for (const pr of prs) {
156
+ title = title.replace(new RegExp(`\\s*\\(#${pr.number}\\)`, 'g'), '')
157
+ }
158
+
159
+ body = body
160
+ .map((line) => line.trim()) // remove artificial line breaks
161
+ .filter(Boolean) // remove blank lines
162
+ .join('\n') // rejoin on new lines
163
+ .split(/^[*-]/gm) // split on lines starting with bullets
164
+ .map((line) => line.trim()) // remove spaces around bullets
165
+ .filter((line) => !title.includes(line)) // rm lines that exist in the title
166
+ // replace new lines for this bullet with spaces and re-bullet it
167
+ .map((line) => `* ${line.trim().replace(/\n/gm, ' ')}`)
168
+ .join('\n') // re-join with new lines
169
+
170
+ commits[changelogType].push({
171
+ hash: commit.abbreviatedOid,
172
+ url: commit.url,
173
+ title,
174
+ type: changelogType,
175
+ body,
176
+ prs,
177
+ credit: commit.authors.nodes.map((author) => {
178
+ if (author.user && author.user.login) {
179
+ return {
180
+ name: `@${author.user.login}`,
181
+ url: author.user.url,
182
+ }
183
+ }
184
+ // if the commit used an email that's not associated with a github account
185
+ // then the user field will be empty, so we fall back to using the committer's
186
+ // name and email as specified by git
187
+ return {
188
+ name: author.name,
189
+ url: `mailto:${author.email}`,
190
+ }
191
+ }),
192
+ })
193
+ }
194
+
195
+ const output = logger()
196
+
197
+ for (const key of Object.keys(commits)) {
198
+ if (commits[key].length > 0) {
199
+ output.group(`### ${key}\n`)
200
+
201
+ for (const commit of commits[key]) {
202
+ let groupCommit = `* [\`${commit.hash}\`](${commit.url})`
203
+
204
+ for (const pr of commit.prs) {
205
+ groupCommit += ` [#${pr.number}](${pr.url})`
206
+ }
207
+
208
+ groupCommit += ` ${commit.title}`
209
+ if (key !== 'Dependencies') {
210
+ for (const user of commit.credit) {
211
+ groupCommit += ` (${user.name})`
212
+ }
213
+ }
214
+
215
+ output.group(groupCommit)
216
+ output.groupEnd()
217
+ }
218
+
219
+ output.log()
220
+ output.groupEnd()
221
+ }
222
+ }
223
+
224
+ return output.toString()
225
+ }
@@ -0,0 +1,39 @@
1
+ const RP = require('release-please')
2
+ const logger = require('./logger.js')
3
+ const ChangelogNotes = require('./changelog.js')
4
+ const Version = require('./version.js')
5
+ const WorkspaceDeps = require('./workspace-deps.js')
6
+
7
+ RP.setLogger(logger)
8
+ RP.registerChangelogNotes('default', (options) => new ChangelogNotes(options))
9
+ RP.registerVersioningStrategy('default', (options) => new Version(options))
10
+ RP.registerPlugin('workspace-deps', (options) => new WorkspaceDeps(options))
11
+
12
+ const main = async ({ repo: fullRepo, token, dryRun, branch }) => {
13
+ if (!token) {
14
+ throw new Error('Token is required')
15
+ }
16
+
17
+ if (!fullRepo) {
18
+ throw new Error('Repo is required')
19
+ }
20
+
21
+ const [owner, repo] = fullRepo.split('/')
22
+ const github = await RP.GitHub.create({ owner, repo, token })
23
+ const manifest = await RP.Manifest.fromManifest(
24
+ github,
25
+ branch ?? github.repository.defaultBranch
26
+ )
27
+
28
+ const pullRequests = await (dryRun ? manifest.buildPullRequests() : manifest.createPullRequests())
29
+ const releases = await (dryRun ? manifest.buildReleases() : manifest.createReleases())
30
+
31
+ return {
32
+ // We only ever get a single pull request with our current release-please settings
33
+ pr: pullRequests.filter(Boolean)[0],
34
+ releases: releases.filter(Boolean),
35
+ release: releases.find(r => r.path === '.'),
36
+ }
37
+ }
38
+
39
+ module.exports = main
@@ -0,0 +1,3 @@
1
+ const { CheckpointLogger } = require('release-please/build/src/util/logger')
2
+
3
+ module.exports = new CheckpointLogger(true, true)
@@ -0,0 +1,76 @@
1
+ const semver = require('semver')
2
+ const RP = require('release-please/build/src/version.js')
3
+
4
+ // A way to compare the "level" of a release since we ignore some things during prereleases
5
+ const LEVELS = new Map([['prerelease', 4], ['major', 3], ['minor', 2], ['patch', 1]]
6
+ .flatMap((kv) => [kv, kv.slice().reverse()]))
7
+
8
+ const parseVersion = (v) => {
9
+ const { prerelease, minor, patch, version } = semver.parse(v)
10
+ const hasPre = prerelease.length > 0
11
+ const preId = prerelease.filter(p => typeof p === 'string').join('.')
12
+ const release = !patch ? (minor ? LEVELS.get('minor') : LEVELS.get('major')) : LEVELS.get('patch')
13
+ return { version, release, prerelease: hasPre, preId }
14
+ }
15
+
16
+ const parseCommits = (commits, prerelease) => {
17
+ let release = LEVELS.get('patch')
18
+ for (const commit of commits) {
19
+ if (commit.breaking) {
20
+ release = LEVELS.get('major')
21
+ break
22
+ } else if (['feat', 'feature'].includes(commit.type)) {
23
+ release = LEVELS.get('minor')
24
+ }
25
+ }
26
+ return { release, prerelease: !!prerelease }
27
+ }
28
+
29
+ const preInc = ({ version, prerelease, preId }, release) => {
30
+ if (!release.startsWith('pre')) {
31
+ release = `pre${release}`
32
+ }
33
+ // `pre` is the default prerelease identifier when creating a new
34
+ // prerelease version
35
+ return semver.inc(version, release, prerelease ? preId : 'pre')
36
+ }
37
+
38
+ const releasePleaseVersion = (v) => {
39
+ const { major, minor, patch, prerelease } = semver.parse(v)
40
+ return new RP.Version(major, minor, patch, prerelease.join('.'))
41
+ }
42
+
43
+ // This does not account for pre v1 semantics since we don't publish those
44
+ // Always 1.0.0 your initial versions!
45
+ module.exports = class DefaultVersioningStrategy {
46
+ constructor (options) {
47
+ this.prerelease = options.prerelease
48
+ }
49
+
50
+ bump (currentVersion, commits) {
51
+ const next = parseCommits(commits, this.prerelease)
52
+ // Release please passes in a version class with a toString() method
53
+ const current = parseVersion(currentVersion.toString())
54
+
55
+ // This is a special case where semver doesn't align exactly with what we want.
56
+ // We are currently at a prerelease and our next is also a prerelease.
57
+ // In this case we want to ignore the release type we got from our conventional
58
+ // commits if the "level" of the next release is <= the level of the current one.
59
+ //
60
+ // This has the effect of only bumping the prerelease identifier and nothing else
61
+ // when we are actively working (and breaking) a prerelease. For example:
62
+ //
63
+ // `9.0.0-pre.4` + breaking changes = `9.0.0-pre.5`
64
+ // `8.5.0-pre.4` + breaking changes = `9.0.0-pre.0`
65
+ // `8.5.0-pre.4` + feature or patch changes = `8.5.0-pre.5`
66
+ if (current.prerelease && next.prerelease && next.release <= current.release) {
67
+ next.release = LEVELS.get('prerelease')
68
+ }
69
+
70
+ const release = LEVELS.get(next.release)
71
+ const releaseVersion = next.prerelease
72
+ ? preInc(current, release)
73
+ : semver.inc(current.version, release)
74
+ return releasePleaseVersion(releaseVersion)
75
+ }
76
+ }
@@ -0,0 +1,72 @@
1
+ const { ManifestPlugin } = require('release-please/build/src/plugin')
2
+
3
+ const matchLine = (line, re) => {
4
+ const trimmed = line.trim().replace(/^[*\s]+/, '')
5
+ if (typeof re === 'string') {
6
+ return trimmed === re
7
+ }
8
+ return trimmed.match(re)
9
+ }
10
+
11
+ module.exports = class WorkspaceDeps extends ManifestPlugin {
12
+ run (pullRequests) {
13
+ for (const { pullRequest } of pullRequests) {
14
+ const depLinks = pullRequest.body.releaseData.reduce((acc, release) => {
15
+ if (release.component) {
16
+ const url = matchLine(release.notes.split('\n')[0], /\[[^\]]+\]\((.*?)\)/)
17
+ if (url) {
18
+ acc[release.component] = url[1]
19
+ }
20
+ }
21
+ return acc
22
+ }, {})
23
+
24
+ for (const release of pullRequest.body.releaseData) {
25
+ const lines = release.notes.split('\n')
26
+ const newLines = []
27
+
28
+ let inWorkspaceDeps = false
29
+ let collectWorkspaceDeps = false
30
+
31
+ for (const line of lines) {
32
+ if (matchLine(line, 'The following workspace dependencies were updated')) {
33
+ // We are in the section with our workspace deps
34
+ // Set the flag and discard this line since we dont want it in the final output
35
+ inWorkspaceDeps = true
36
+ } else if (inWorkspaceDeps) {
37
+ if (collectWorkspaceDeps) {
38
+ const depMatch = matchLine(line, /^(\S+) bumped from \S+ to (\S+)$/)
39
+ if (depMatch) {
40
+ // If we have a line that is a workspace dep update, then reformat
41
+ // it and save it to the new lines
42
+ const [, depName, newVersion] = depMatch
43
+ const depSpec = `\`${depName}@${newVersion}\``
44
+ const url = depLinks[depName]
45
+ newLines.push(` * ${url ? `[${depSpec}](${url})` : depSpec}`)
46
+ } else {
47
+ // Anything else means we are done with dependencies so ignore
48
+ // this line and dont look for any more
49
+ collectWorkspaceDeps = false
50
+ }
51
+ } else if (matchLine(line, 'dependencies')) {
52
+ // Only collect dependencies discard dev deps and everything else
53
+ collectWorkspaceDeps = true
54
+ } else if (matchLine(line, '') || matchLine(line, /^#/)) {
55
+ inWorkspaceDeps = false
56
+ newLines.push(line)
57
+ }
58
+ } else {
59
+ newLines.push(line)
60
+ }
61
+ }
62
+
63
+ const newNotes = newLines.join('\n').trim()
64
+ const emptyDeps = newNotes.match(/### Dependencies[\n]+(### .*)/m)
65
+
66
+ release.notes = emptyDeps ? newNotes.replace(emptyDeps[0], emptyDeps[1]) : newNotes
67
+ }
68
+ }
69
+
70
+ return pullRequests
71
+ }
72
+ }
@@ -2,6 +2,7 @@ const semver = require('semver')
2
2
  const npa = require('npm-package-arg')
3
3
  const { has } = require('lodash')
4
4
  const { join } = require('path')
5
+ const { name: NAME } = require('../../package.json')
5
6
 
6
7
  const installLocations = [
7
8
  'dependencies',
@@ -30,6 +31,10 @@ const getSpecVersion = (spec, where) => {
30
31
  const pkg = require(join(arg.fetchSpec, 'package.json'))
31
32
  return new semver.SemVer(pkg.version)
32
33
  }
34
+ case 'git': {
35
+ // allow installing only this project from git to test in other projects
36
+ return arg.name === NAME
37
+ }
33
38
  }
34
39
  return null
35
40
  }
@@ -58,6 +63,9 @@ const hasPackage = (
58
63
  .filter(Boolean)
59
64
 
60
65
  return existingByLocation.some((existing) => {
66
+ if (existing === true) {
67
+ return true
68
+ }
61
69
  switch ([existing, requested].map((t) => isVersion(t) ? 'VER' : 'RNG').join('-')) {
62
70
  case `VER-VER`:
63
71
  // two versions, use semver.eq to check equality
@@ -198,7 +198,7 @@ class Json extends Base {
198
198
  comment = (c) => ({ [`//${this.options.config.__NAME__}`]: c })
199
199
 
200
200
  toString (s) {
201
- return JSON.stringify(s, (_, v) => v === this.DELETE ? undefined : v, 2)
201
+ return JSON.stringify(s, (_, v) => v === this.DELETE ? undefined : v, 2).trim() + '\n'
202
202
  }
203
203
 
204
204
  parse (s) {
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@npmcli/template-oss",
3
- "version": "3.6.0",
3
+ "version": "3.8.0",
4
4
  "description": "templated files used in npm CLI team oss projects",
5
5
  "main": "lib/content/index.js",
6
6
  "bin": {
7
7
  "template-oss-apply": "bin/apply.js",
8
- "template-oss-check": "bin/check.js"
8
+ "template-oss-check": "bin/check.js",
9
+ "template-oss-release-please": "bin/release-please.js"
9
10
  },
10
11
  "scripts": {
11
12
  "lint": "eslint \"**/*.js\"",
@@ -31,6 +32,9 @@
31
32
  "author": "GitHub Inc.",
32
33
  "license": "ISC",
33
34
  "dependencies": {
35
+ "@actions/core": "^1.9.1",
36
+ "@commitlint/cli": "^17.1.1",
37
+ "@commitlint/config-conventional": "^17.1.0",
34
38
  "@npmcli/fs": "^2.0.1",
35
39
  "@npmcli/git": "^3.0.0",
36
40
  "@npmcli/map-workspaces": "^2.0.2",
@@ -44,6 +48,7 @@
44
48
  "lodash": "^4.17.21",
45
49
  "npm-package-arg": "^9.0.1",
46
50
  "proc-log": "^2.0.0",
51
+ "release-please": "npm:@npmcli/release-please@^14.2.4",
47
52
  "semver": "^7.3.5",
48
53
  "yaml": "2.0.0-11"
49
54
  },
@@ -56,6 +61,9 @@
56
61
  "@npmcli/template-oss": "file:./",
57
62
  "tap": "^16.0.0"
58
63
  },
64
+ "tap": {
65
+ "timeout": 600
66
+ },
59
67
  "templateOSS": {
60
68
  "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten."
61
69
  },