-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathnode.js
239 lines (210 loc) · 8.9 KB
/
node.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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import path from "path"
import assert from "assert"
import fsPromises from "fs/promises"
import fetch from "node-fetch"
import core from "@actions/core"
import findVersions from "find-versions"
import Tool from "./tool.js"
import { nodeVersions } from "./node-version-data.js"
export default class Node extends Tool {
static tool = "node"
static envVar = "NODENV_ROOT"
static envPaths = ["bin", "shims"]
static installer = "nodenv"
constructor() {
super(Node.tool)
}
async setup(desiredVersion) {
const [checkVersion, isVersionOverridden] =
await this.getNodeVersion(desiredVersion)
if (!(await this.haveVersion(checkVersion))) {
if (checkVersion) {
// Ensure yarn is present as well, but don't error if it breaks
await this.installYarn().catch(() => {})
}
return checkVersion
}
// Check if nodenv exists and can be run, and capture the version info while
// we're at it, should be pre-installed on self-hosted runners.
await this.findInstaller()
// Update nodeenv versions in case the user is requesting a node version
// that did not exist when nodenenv was installed
const updateVersionsCommand = `${this.installer} update-version-defs`
// Remove NODENV related vars from the environment, as when running
// nodenv it will pick them and try to use the specified node version
// which can lead into issues if that node version does not exist
// eslint-disable-next-line no-unused-vars
const { NODENV_VERSION, ...envWithoutNodenv } = process.env
await this.subprocessShell(updateVersionsCommand, {
// Run the cmd in a tmp file so that nodenv doesn't pick any .node-version file in the repo with an unknown
// node version
cwd: process.env.RUNNER_TEMP,
env: { ...envWithoutNodenv, ...this.getEnv() },
}).catch((error) => {
this.warning(
`Failed to update nodenv version refs, install may fail`,
)
if (error.stderr) {
this.debug(error.stderr)
}
})
// Set downstream environment variable for future steps in this Job
if (isVersionOverridden) {
core.exportVariable("NODENV_VERSION", checkVersion)
}
// Install the desired version as it was not in the system
const installCommand = `${this.installer} install -s ${checkVersion}`
await this.subprocessShell(installCommand).catch(
this.logAndExit(`failed to install node version ${checkVersion}`),
)
// Sanity check that the node command works and its reported version matches what we have
// requested to be in place.
await this.validateVersion(checkVersion)
// Could make this conditional? But for right now we always install `yarn`
await this.installYarn()
// If we got this far, we have successfully configured node.
this.info("node success!")
return checkVersion
}
/**
* Download Node version data. If requests fail, use a local file
* @returns {Promise<void>}
*/
async getVersionData() {
const nodeVersionUrl = "https://nodejs.org/download/release/index.json"
try {
return await fetch(nodeVersionUrl).then((response) => {
if (!response.ok) {
throw new Error(
`Failed to fetch Node version data: ${response.status} ${response.statusText}`,
)
}
return response.json()
})
} catch (e) {
this.error(
`Failed to fetch Node version data: ${e.message}. Returning local cache`,
)
return nodeVersions
}
}
/**
* Given an nvmrc node version spec, convert it to a SemVer node version
* @param {String} fileName File where to look for the node version
* @returns {Promise<string | undefined>} Parsed version
*/
async parseNvmrcVersion(fileName) {
const nodeVersion = this.getVersion(null, fileName)[0]
if (!nodeVersion) {
return undefined
}
// Versions are sorted from newest to oldest
const versionData = await this.getVersionData()
let version
if (/^lts\/.*/i.test(nodeVersion)) {
if (nodeVersion === "lts/*") {
// We just want the latest LTS
version = versionData.find((v) => v.lts !== false)?.version
} else {
version = versionData.find(
(v) =>
nodeVersion.substring(4).toLowerCase() ===
(v.lts || "").toLowerCase(),
)?.version
}
} else if (nodeVersion === "node") {
// We need the latest version
version = versionData[0].version
} else {
// This could be a full or a partial version, so use partial matching
version = versionData.find((v) =>
v.version.startsWith(`v${nodeVersion}`),
)?.version
}
if (version !== undefined) {
return findVersions(version)[0]
}
throw new Error(`Could not parse Node version "${nodeVersion}"`)
}
/**
* Return a [version, override] pair where version is the SemVer string
* and the override is a boolean indicating the version must be manually set
* for installs.
*
* If a desired version is specified the function returns this one. If not,
* it will look for a .node-version or a .nvmrc file (in this order) to extract
* the desired node version
*
* It is expected that these files follow the NVM spec: https://github.com/nvm-sh/nvm#nvmrc
* @param [desiredVersion] Desired node version
* @returns {Promise<[string | null, boolean | null]>} Resolved node version
*/
async getNodeVersion(desiredVersion) {
// If we're given a version, it's the one we want
if (desiredVersion) return [desiredVersion, true]
// If .node-version is present, it's the one we want, and it's not
// considered an override
const nodeVersion = await this.parseNvmrcVersion(".node-version")
if (nodeVersion) {
return [nodeVersion, false]
}
// If .nvmrc is present, we fall back to it
const nvmrcVersion = await this.parseNvmrcVersion(".nvmrc")
if (nvmrcVersion) {
// In this case we want to override the version, as nodenv is not aware of this file
// and we want to use it
return [nvmrcVersion, true]
}
// Otherwise we have no node
return [null, null]
}
/**
* Download and configures nodenv.
*
* @param {string} root - Directory to install nodenv into (NODENV_ROOT).
* @return {string} The value of NODENV_ROOT.
*/
async install(root) {
assert(root, "root is required")
// Build our URLs
const gh = `https://${process.env.GITHUB_SERVER || "github.com"}/nodenv`
const url = {}
url.nodenv = `${gh}/nodenv/archive/refs/heads/master.tar.gz`
url.nodebulid = `${gh}/node-build/archive/refs/heads/master.tar.gz`
url.nodedoctor = `${gh}/nodenv-installer/raw/master/bin/nodenv-doctor`
root = await this.downloadTool(url.nodenv, { dest: root, strip: 1 })
this.info(`Downloaded nodenv to ${root}`)
await this.downloadTool(url.nodebulid, path.join(root, "plugins"))
this.info(`Downloaded node-build to ${root}/plugins`)
const doctor = await this.downloadTool(url.nodedoctor)
this.info(`Downloaded node-doctor to ${doctor}`)
// Create environment for running node-doctor
await this.setEnv(root)
await this.subprocessShell(`bash ${doctor}`)
// Asynchronously clean up the downloaded doctor script
fsPromises.rm(doctor, { recursive: true }).catch(() => {})
return root
}
/**
* Run `npm install -g yarn` and `nodenv rehash` to ensure `yarn` is on the CLI.
*/
async installYarn() {
// Check for an existing version
let yarnVersion = await this.version("yarn --version", {
soft: true,
}).catch(() => {})
if (yarnVersion) {
this.debug(`yarn is already installed (${yarnVersion})`)
return
}
// Installing yarn with npm, which if this errors means ... things are
// badly broken?
this.info("Installing yarn")
await this.subprocessShell("npm install -g yarn")
// Just run `nodenv rehash` always and ignore errors because we might be
// in a setup-node environment that doesn't have nodenv
this.info("Rehashing node shims")
await this.subprocessShell("nodenv rehash").catch(() => {})
}
}
Node.register()