-
Notifications
You must be signed in to change notification settings - Fork 158
/
Copy pathcreate-homebrew-pr.js
executable file
·180 lines (158 loc) · 6.94 KB
/
create-homebrew-pr.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#!/usr/bin/env node
process.removeAllListeners('warning');
// ES Modules
import {findUp} from "find-up"
import {createHash} from 'node:crypto'
import {createRequire} from 'module'
import {fileURLToPath} from "node:url"
import * as path from "pathe"
import fetch from 'node-fetch'
import glob from 'fast-glob';
import {Liquid} from 'liquidjs'
// CJS Modules
const require = createRequire(import.meta.url)
const {readFile, mkdir, lstat, copy, outputFile, pathExists, rm} = require('fs-extra')
const {program} = require('commander')
const colors = require('ansi-colors')
import {withOctokit} from './github-utils.js'
const packagingDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../packaging")
program
.description('Packages the CLI for distribution through package managers')
.requiredOption('-p, --open-pr', 'when passed it opens a PR in the https://github.com/shopify/homebrew-shopify repository', true)
.action(async (options) => {
// Constants
const version = await versionToRelease()
const templateVersion = getTemplateVersion(version)
console.log(`${colors.green(colors.bold("Version to package:"))} ${version}`)
const outputDirectory = path.join(packagingDirectory, templateVersion, "dist")
console.log(`${colors.green(colors.bold("Output directory:"))} ${outputDirectory}`)
const homebrewVariables = await getHomebrewVariables(version)
console.log(`We'll populate the Homebrew formula with the following content:`)
console.log(homebrewVariables)
// Create formulas
await recursiveDirectoryCopy(path.join(packagingDirectory, templateVersion, "src"), outputDirectory, homebrewVariables)
if (options.openPr) {
console.log(`Opening a PR in shopify/homebrew-shopify to update the formula ${version}`)
const files = {}
switch (templateVersion) {
case "3":
files["shopify-cli.rb"] = (await readFile(path.join(outputDirectory, "shopify-cli.rb"))).toString()
files["shopify-cli@3.rb"] = (await readFile(path.join(outputDirectory, "shopify-cli@3.rb"))).toString()
break
case "pre":
files["shopify-cli-pre.rb"] = (await readFile(path.join(outputDirectory, "shopify-cli-pre.rb"))).toString()
break
case "nightly":
files["shopify-cli-nightly.rb"] = (await readFile(path.join(outputDirectory, "shopify-cli-nightly.rb"))).toString()
break
default:
throw new Error(`Unrecognized template version string ${templateVersion}`)
}
await withOctokit("shopify", async (octokit) => {
const response = await octokit
.createPullRequest({
owner: "shopify",
repo: "homebrew-shopify",
title: `Shopify CLI ${version}`,
body: `We are updating the formula to point to the recently released version of the Shopify CLI [${version}](https://www.npmjs.com/package/@shopify/cli/v/${version})`,
head: `shopify-cli-${version}`,
base: "master",
update: true,
forceFork: false,
changes: [
{
files,
commit: `Update Shopify CLI 3 formula to install the version ${version}`,
},
],
})
if (["pre", "nightly"].includes(templateVersion)) {
// Merge the PR immediately if we're releasing a pre or nightly version
octokit.request("PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge", {
owner: "Shopify",
repo: "homebrew-shopify",
pull_number: response.data.number,
})
} else {
// When releasing a minor version, mandate a manual review of the PR before merge
console.log(`${colors.green(colors.bold("PR opened:"))} ${response.data.html_url}`)
}
})
}
})
program.parse()
async function versionToRelease() {
const cliKitPackageJsonPath = await findUp("packages/cli-kit/package.json", {type: "file"})
return JSON.parse(await readFile(cliKitPackageJsonPath)).version
}
function getTemplateVersion(version) {
if (version.includes("pre")) return "pre"
if (version.includes("nightly")) return "nightly"
if (version.match(/^3\.\d+\.\d+$/)) return "3"
throw `Unrecognized version string ${version}`
}
async function getHomebrewVariables(cliVersion) {
const [cliTarball, cliSha] = await getTarballAndShaForPackage('@shopify/cli', cliVersion)
return {cliTarball, cliSha}
}
async function getTarballAndShaForPackage(pkg, cliVersion) {
const tarball = await getTarballForPackage(pkg, cliVersion)
const sha = await getSha256ForTarball(tarball)
return [tarball, sha]
}
async function getTarballForPackage(pkg, cliVersion) {
let retryCount = 1
while (true) {
const response = await fetch(`https://registry.npmjs.com/${pkg}`)
const npmPackage = (await response.json()).versions[cliVersion]
if (npmPackage) return npmPackage.dist.tarball
if (retryCount++ < 10) {
console.log(`${pkg} v${cliVersion} not found in NPM registry. Retrying in 5 seconds...`)
await new Promise((resolve) => setTimeout(resolve, 5000))
} else {
throw new Error(`${pkg} v${cliVersion} not found in NPM registry`)
}
}
}
async function getSha256ForTarball(url) {
const hash = createHash('sha256').setEncoding('hex')
const response = await fetch(url)
const stream = response.body.pipe(hash)
await new Promise((resolve) => stream.on('finish', resolve))
return hash.read()
}
async function recursiveDirectoryCopy(from, to, data) {
console.log(`Generating the formula into ${to}`)
// Must go up to `packaging` dir so nightly and pre can find the primary formula template
const engine = new Liquid({root: path.join(from, '../..')})
const templateFiles = await glob(path.join(from, '**/*'), {dot: true})
if (await pathExists(to)) {
await rm(to, {recursive: true})
}
await mkdir(to)
const sortedTemplateFiles = templateFiles
.map((path) => path.split('/'))
.sort((lhs, rhs) => (lhs.length < rhs.length ? 1 : -1))
.map((components) => components.join('/'))
for (const templateItemPath of sortedTemplateFiles) {
const outputPath = await engine.render(engine.parse(path.join(to, path.relative(from, templateItemPath))), data)
const isDirectory = (await lstat(templateItemPath)).isDirectory()
if (isDirectory) {
if (!await pathExists(templateItemPath)) {
await mkdir(outputPath)
}
} else if (templateItemPath.endsWith('.liquid')) {
if (!await pathExists(path.dirname(outputPath))) {
await mkdir(path.dirname(outputPath))
}
const content = (await readFile(templateItemPath)).toString()
const contentOutput = await engine.render(engine.parse(content), data)
const outputPathWithoutLiquid = outputPath.replace('.liquid', '')
await copy(templateItemPath, outputPathWithoutLiquid)
await outputFile(outputPathWithoutLiquid, contentOutput)
} else {
await copy(templateItemPath, outputPath)
}
}
console.log(`The formula has been generated in ${to}`)
}