fix: always rebuild cli source on command

Luke Karrys committed Nov 9, 2022 at 23:37 UTC 7416290a2e3ec7e2a9bd59b875fe6511b5e2f232
6 files changed +112 -171
cli/bin/build.js
-1
@@ -3,7 +3,6 @@ const build = require('../lib/build.js')
3
4 build({
5 loglevel: process.argv.includes('--debug') || process.env.CI ? 'verbose' : 'info',
6 - force: process.argv.includes('--force'),
6 prerelease: false,
7 contentPath: resolve(__dirname, '..', '..', 'content', 'cli'),
8 releasesPath: resolve(__dirname, '..', 'releases.json'),
cli/lib/build.js
+20 -37
@@ -3,7 +3,6 @@ const fs = require('fs').promises
3 const yaml = require('yaml')
4 const semver = require('semver')
5 const pacote = require('pacote')
6 -const gh = require('./gh')
6 const extractRelease = require('./extract')
7 const log = require('./log')
8
@@ -39,20 +38,8 @@ const updateNav = async (updates, { nav, path }) => {
38 return fs.writeFile(path, nav.toString(), 'utf-8')
39 }
40
42 -const updateReleases = async (updates, path) => {
43 - const data = JSON.parse(await fs.readFile(path, 'utf-8'))
44 -
45 - for (const release of updates) {
46 - const index = data.findIndex((item) => item.id === release.id)
47 - data[index].resolved = release.resolved
48 - }
49 -
50 - return fs.writeFile(path, JSON.stringify(data, null, 2) + '\n', 'utf-8')
51 -}
52 -
41 const main = async ({
42 loglevel,
55 - force,
43 releasesPath,
44 navPath,
45 contentPath,
@@ -63,29 +50,28 @@ const main = async ({
50 log.on(loglevel)
51 }
52
66 - const rawReleases = require(releasesPath)
67 -
68 - const releaseManifests = await Promise.all(rawReleases.map(async release => {
69 - const manifest = await pacote.manifest(`${gh.owner}@${release.spec}`, {
70 - preferOnline: true,
71 - })
72 - // the default release is always controlled by the latest dist-tag
73 - release.default = release.spec === 'latest'
74 - release.manifest = manifest
75 - release.version = manifest.version
76 - const sVersion = semver.parse(release.version)
77 - release.semver = sVersion
78 - release.prerelease = sVersion.prerelease.length > 0
79 - return release
53 + const pack = await pacote.packument('npm', { preferOnline: true }).then(p => ({
54 + versions: Object.keys(p.versions),
55 + latest: p['dist-tags'].latest,
56 }))
57
82 - const latestRelease = releaseManifests.find(r => r.spec === 'latest')
58 + const releaseVersions = require(releasesPath).map(release => {
59 + const major = Number(release.id.replace(/^v/, ''))
60 + const range = `>=${major}.0.0-a <${major + 1}.0.0` // include all prereleases
61 + const version = semver.parse(semver.maxSatisfying(pack.versions, range))
62
84 - if (!latestRelease) {
85 - throw new Error(`One of the CLI releases must have \`spec: 'latest'\``)
86 - }
63 + return {
64 + ...release,
65 + version: version.toString(),
66 + // the default release is always controlled by the latest dist-tag
67 + default: semver.eq(version, pack.latest),
68 + prerelease: version.prerelease.length > 0,
69 + }
70 + })
71 +
72 + const latestRelease = releaseVersions.find(r => r.default)
73
88 - const releases = releaseManifests.map((release) => {
74 + const releases = releaseVersions.map((release) => {
75 const type = release.default ? 'Latest Release'
76 : release.prerelease ? 'Prerelease'
77 : semver.gt(release.version, latestRelease.version) ? 'Current Release'
@@ -104,14 +90,11 @@ const main = async ({
90
91 const updates = await Promise.all(
92 releases.map((r) =>
107 - extractRelease(r, { contentPath, baseNav: yaml.parse(baseNav), force, prerelease })
93 + extractRelease(r, { contentPath, baseNav: yaml.parse(baseNav), prerelease })
94 )
95 ).then((r) => r.filter(Boolean))
96
111 - await Promise.all([
112 - updateNav(updates, { nav: yaml.parseDocument(baseNav), path: navPath }),
113 - updateReleases(updates, releasesPath),
114 - ])
97 + await updateNav(updates, { nav: yaml.parseDocument(baseNav), path: navPath })
98 }
99
100 module.exports = main
cli/lib/extract.js
+10 -45
@@ -33,16 +33,12 @@ const unpackTarball = async ({ release, cwd, dir }) => {
33 },
34 })
35
36 - await pacote.tarball.stream(
37 - release.spec,
38 - (stream) =>
39 - new Promise((res, rej) => {
40 - stream.on('end', res)
41 - stream.on('error', rej)
42 - stream.pipe(extract())
43 - }),
44 - { resolved: release.resolved }
45 - )
36 + await pacote.tarball.stream(`npm@${release.version}`, (stream) =>
37 + new Promise((res, rej) => {
38 + stream.on('end', res)
39 + stream.on('error', rej)
40 + stream.pipe(extract())
41 + }))
42
43 return result
44 }
@@ -122,47 +118,16 @@ const writeChangelog = async ({ release, nav, cwd, srcPath, contentPath }) => {
118 })
119 }
120
125 -const resolveRelease = async (
126 - { resolved: current, ...release },
127 - { force, prerelease }
121 +const unpackRelease = async (
122 + release,
123 + { contentPath, baseNav, prerelease = false }
124 ) => {
125 if (release.prerelease && !prerelease) {
126 log.info(`Skipping ${release.id} due to prerelease ${release.version}`)
131 - return null
132 - }
133 -
134 - // The legacy v6 release has updated docs in GitHub that were never
135 - // published. So in this case we skip cloning the repo with pacote
136 - // and get the latest commit on the branch. Later we will use the
137 - // GitHub api to fetch just the docs files we need since that is
138 - // much faster than cloning and preparing with pacote
139 - if (release.useBranch) {
140 - release.resolved = await gh.getLatestSha(release.branch)
141 - } else {
142 - release.resolved = release.manifest._resolved
143 - release.spec = release.manifest._from
144 - }
145 -
146 - log.info(release.id, release.version, release.resolved)
147 -
148 - if (release.resolved === current && !force) {
149 - log.info(`Skipping ${release.id} due to resolved fields matching`)
150 - return null
151 - }
152 -
153 - return release
154 -}
155 -
156 -const unpackRelease = async (
157 - _release,
158 - { contentPath, baseNav, force = false, prerelease = false }
159 -) => {
160 - const release = await resolveRelease(_release, { force, prerelease })
161 - if (!release) {
127 return
128 }
129
165 - log.verbose(release)
130 + log.info(release.id, release)
131
132 const cwd = join(contentPath, release.id)
133 await fs
cli/lib/gh.js
-12
@@ -20,17 +20,6 @@ const getFile = async ({ sha, ref, path }) => {
20 return Buffer.from(data.content, data.encoding)
21 }
22
23 -const getLatestSha = async (ref) => {
24 - const {
25 - data: [commit],
26 - } = await octokit.repos.listCommits({
27 - ...opts,
28 - sha: ref,
29 - per_page: 1,
30 - })
31 - return commit.sha
32 -}
33 -
23 const getAllFiles = async (sha) => {
24 const {
25 data: { tree },
@@ -79,7 +68,6 @@ const pathExists = async (ref, path) => {
68 module.exports = {
69 octokit,
70 getFile,
82 - getLatestSha,
71 getAllFiles,
72 getDirectory,
73 pathExists,
cli/releases.json
+4 -12
@@ -2,26 +2,18 @@
2 {
3 "id": "v6",
4 "branch": "release/v6",
5 - "spec": "^6",
6 - "useBranch": true,
7 - "resolved": "898c0496406e29865b5ae736ed541ca41895e484"
5 + "useBranch": true
6 },
7 {
8 "id": "v7",
11 - "branch": "release/v7",
12 - "spec": "^7",
13 - "resolved": "https://registry.npmjs.org/npm/-/npm-7.24.2.tgz"
9 + "branch": "release/v7"
10 },
11 {
12 "id": "v8",
17 - "branch": "release/v8",
18 - "spec": "^8",
19 - "resolved": "https://registry.npmjs.org/npm/-/npm-8.19.3.tgz"
13 + "branch": "release/v8"
14 },
15 {
16 "id": "v9",
23 - "branch": "latest",
24 - "spec": "latest",
25 - "resolved": "https://registry.npmjs.org/npm/-/npm-9.1.1.tgz"
17 + "branch": "latest"
18 }
19 ]
cli/test/index.js
+78 -64
@@ -12,11 +12,27 @@ const navPath = resolve(
12 'nav.yml'
13 )
14
15 -const getReleases = () => {
16 - return JSON.parse(JSON.stringify(require('../releases.json')))
17 -}
18 -
19 -const mockBuild = async ({ releases, testdir: testdirOpts }) => {
15 +const getReleases = () => [
16 + {
17 + id: 'v6',
18 + branch: 'release/v6',
19 + useBranch: true,
20 + },
21 + {
22 + id: 'v7',
23 + branch: 'release/v7',
24 + },
25 + {
26 + id: 'v8',
27 + branch: 'release/v8',
28 + },
29 + {
30 + id: 'v9',
31 + branch: 'latest',
32 + },
33 +]
34 +
35 +const mockBuild = async ({ releases, packument = {}, testdir: testdirOpts }) => {
36 const testdir = t.testdir({
37 'releases.json': JSON.stringify(releases),
38 'nav.yml': await fs.readFile(navPath, 'utf-8'),
@@ -24,51 +40,58 @@ const mockBuild = async ({ releases, testdir: testdirOpts }) => {
40 ...testdirOpts,
41 })
42
43 + if (!packument.versions) {
44 + packument.versions = releases.map(r => {
45 + // real tarball requests are made for these verions
46 + // so by default they all need to exist
47 + switch (r.id.slice(1)) {
48 + case '6':
49 + return '6.14.17'
50 + case '7':
51 + return '7.24.2'
52 + case '8':
53 + return '8.19.3'
54 + case '9':
55 + return '9.0.0'
56 + }
57 + })
58 + }
59 +
60 + if (!packument.latest) {
61 + packument.latest = packument.versions[packument.versions.length - 1]
62 + }
63 +
64 const build = t.mock('../lib/build', {
65 pacote: {
66 ...pacote,
30 - manifest: async (spec) => {
31 - let version = spec.split('@')[1]
32 - const release = releases.find(r => r.spec === version)
33 -
34 - if (version.match(/^\^\d+$/)) {
35 - version = version.slice(1) + '.0.0'
36 - } else if (version === 'latest') {
37 - version = release.id.slice(1) + '.0.0'
38 - } else if (version.startsWith('next-')) {
39 - version = version.replace('next-', '') + '.0.0-pre.4'
40 - }
41 -
67 + packument: async () => {
68 return {
43 - version,
44 - _resolved: release.resolved,
45 - _from: spec,
69 + 'dist-tags': {
70 + latest: packument.latest,
71 + },
72 + versions: packument.versions.reduce((acc, v) => {
73 + acc[v] = null
74 + return acc
75 + }, {}),
76 }
77 },
78 },
79 })
80
51 - return (opts) => build({
52 - contentPath: join(testdir, 'content'),
53 - releasesPath: join(testdir, 'releases.json'),
54 - navPath: join(testdir, 'nav.yml'),
55 - ...opts,
56 - })
81 + return {
82 + testdir,
83 + build: (opts) => build({
84 + contentPath: join(testdir, 'content'),
85 + releasesPath: join(testdir, 'releases.json'),
86 + navPath: join(testdir, 'nav.yml'),
87 + ...opts,
88 + }),
89 + }
90 }
91
59 -t.test('builds successfully', async (t) => {
60 - const releases = getReleases()
61 - const build = await mockBuild({ releases })
62 -
63 - await build({
64 - force: true,
65 - prerelease: true,
66 - })
67 -})
68 -
69 -t.test('no force', async (t) => {
92 +t.test('basic', async (t) => {
93 const releases = getReleases()
71 - const build = await mockBuild({
94 + const { build, testdir } = await mockBuild({
95 releases,
96 testdir: {
97 'nav.yml': '- title: cli\n url: /cli',
@@ -76,45 +99,39 @@ t.test('no force', async (t) => {
99 })
100
101 await build()
102 + t.strictSame(await fs.readdir(join(testdir, 'content')), releases.map(r => r.id))
103 })
104
105 t.test('no default release', async (t) => {
82 - const releases = getReleases().filter(r => r.spec !== 'latest')
83 - const build = await mockBuild({
106 + const releases = getReleases()
107 + const { build } = await mockBuild({
108 releases,
85 - testdir: {
86 - 'nav.yml': '- title: cli\n url: /cli',
87 - },
109 + packument: { latest: '9999.99999.99999' },
110 })
111
112 await t.rejects(() => build())
113 })
114
93 -t.test('prerelease', async (t) => {
115 +t.test('prereleases', async (t) => {
116 const releases = getReleases()
95 - releases[1].spec = 'next-8'
96 - releases[2].spec = '^8'
97 -
98 - const build = await mockBuild({
117 + const { build, testdir } = await mockBuild({
118 releases,
100 - testdir: {
101 - 'nav.yml': '- title: cli\n url: /cli',
102 - },
119 + packument: { versions: ['6.14.17', '7.24.2', '8.19.3', '9.0.0-pre.2'], latest: '8.19.3' },
120 })
121
105 - await build()
122 + await build({ prerelease: false })
123 + const expectedReleases = releases.map(r => r.id).filter(r => r !== 'v9')
124 + t.strictSame(await fs.readdir(join(testdir, 'content')), expectedReleases)
125 +
126 + await build({ prerelease: true })
127 + t.strictSame(await fs.readdir(join(testdir, 'content')), releases.map(r => r.id))
128 })
129
130 t.test('earlier release is latest', async (t) => {
131 const releases = getReleases()
110 - releases[1].spec = 'latest'
111 - releases[2].spec = '^8'
112 -
113 - const build = await mockBuild({
132 + const { build } = await mockBuild({
133 releases,
115 - testdir: {
116 - 'nav.yml': '- title: cli\n url: /cli',
117 - },
134 + packument: { latest: '8.19.3' },
135 })
136
137 await build()
@@ -122,15 +139,12 @@ t.test('earlier release is latest', async (t) => {
139
140 t.test('add variant to nav', async (t) => {
141 const releases = getReleases()
125 - releases[1].spec = 'latest'
126 - releases[2].spec = '^8'
127 -
128 - const build = await mockBuild({
142 + const { build } = await mockBuild({
143 releases,
144 testdir: {
145 'nav.yml': '- title: cli\n url: /cli\n variants:\n - url: /cli/v0',
146 },
147 })
148
135 - await build({ force: true })
149 + await build()
150 })