Download releases from github

This commit is contained in:
John Lenz 2021-09-03 17:53:31 -05:00
parent bcad811784
commit e78206c588
16 changed files with 947 additions and 751 deletions

134
README.md
View File

@ -6,17 +6,25 @@ Install pnpm package manager.
### `version` ### `version`
**Required** Version of pnpm to install. Can either specifiy a specific version of pnpm to install (e.g. "6.14.6") or can
specifiy a version range (in the [semver range
format](https://github.com/npm/node-semver#ranges)). The latest version to
match the range is used and if the input version is not specified, the latest
overall version is used. Version ranges are only supported for versions
starting at 6.13.0 and higher, because that is the first version of pnpm to be
published to github releases.
### `dest` ### `dest`
**Optional** Where to store pnpm files. This option is obsolete. It is only used when a specific version of pnpm which
is 6.12 or lower is specified. For these old versions of pnpm, the `dest` input
is where to store pnpm files.
### `run_install` ### `run_install`
**Optional** (_default:_ `null`) If specified, run `pnpm install`. If specified, run `pnpm install`.
If `run_install` is either `null` or `false`, pnpm will not install any npm package. If `run_install` is either `null` (the default) or `false`, pnpm will not install any npm package.
If `run_install` is `true`, pnpm will install dependencies recursively. If `run_install` is `true`, pnpm will install dependencies recursively.
@ -24,100 +32,100 @@ If `run_install` is a YAML string representation of either an object or an array
#### `run_install.recursive` #### `run_install.recursive`
**Optional** (_type:_ `boolean`, _default:_ `false`) Whether to use `pnpm recursive install`. (_type:_ `boolean`, _default:_ `false`) Whether to use `pnpm recursive install`.
#### `run_install.cwd` #### `run_install.cwd`
**Optional** (_type:_ `string`) Working directory when run `pnpm [recursive] install`. (_type:_ `string`) Working directory when run `pnpm [recursive] install`.
#### `run_install.args` #### `run_install.args`
**Optional** (_type:_ `string[]`) Additional arguments after `pnpm [recursive] install`, e.g. `[--frozen-lockfile, --strict-peer-dependencies]`. (_type:_ `string[]`) Additional arguments after `pnpm [recursive] install`, e.g. `[--frozen-lockfile, --strict-peer-dependencies]`.
## Outputs ## Outputs
### `dest`
Expanded path of inputs#dest.
### `bin_dest` ### `bin_dest`
Location of `pnpm` and `pnpx` command. Folder containing the `pnpm` executable.
### `dest`
Expanded path of inputs#dest, only set if a version before "6.14.6" is specified.
## Usage example ## Usage example
### Just install pnpm ### Install latest pnpm and cache store
The following yaml will first install the latest version of pnpm, install node,
and setup caching of the pnpm store.
[actions/setup-node](https://github.com/actions/setup-node) has support for
caching the pnpm store, as long as pnpm is installed first.
```yaml ```yaml
on:
- push
- pull_request
jobs:
runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v2.0.1
- uses: actions/setup-node@v2
with:
node-version: "16"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
# more build/run commands ...
- run: pnpm store prune
```
### Install specific range of pnpm
```yaml
steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v2.0.1 - uses: pnpm/action-setup@v2.0.1
with: with:
version: 6.0.2 version: "^6.14.6"
- uses: actions/setup-node@v2
with:
node-version: "16"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
# more build/run commands ...
- run: pnpm store prune
``` ```
### Install pnpm and a few npm packages ### Install pnpm and a few npm packages
Unfortunately, using `run_install` does not work together with the caching
in `actions/setup-node`. The caching in `actions/setup-node` requires pnpm
to be installed before node, while the `run_install` option
requires node to be installed first. In this situation, you will need to setup
the caching yourself.
```yaml ```yaml
on:
- push
- pull_request
jobs:
runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "16"
- uses: pnpm/action-setup@v2.0.1 - uses: pnpm/action-setup@v2.0.1
with: with:
version: 6.0.2 version: "6.*"
run_install: | run_install: |
- recursive: true - recursive: true
args: [--frozen-lockfile, --strict-peer-dependencies] args: [--frozen-lockfile, --strict-peer-dependencies]
- args: [--global, gulp, prettier, typescript] - args: [--global, gulp, prettier, typescript]
# Setup caching here using actions/cache
``` ```
### Use cache to reduce installation time
```yaml
on:
- push
- pull_request
jobs:
runs-on: ubuntu-latest
steps:
build:
- uses: actions/checkout@v2
- name: Cache pnpm modules
uses: actions/cache@v2
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- uses: pnpm/action-setup@v2.0.1
with:
version: 6.0.2
run_install: true
```
**Note:** You don't need to run `pnpm store prune` at the end; post-action has already taken care of that.
## Notes
This action does not setup Node.js for you, use [actions/setup-node](https://github.com/actions/setup-node) yourself.
## License ## License
[MIT](https://git.io/JfclH) © [Hoàng Văn Khải](https://github.com/KSXGitHub/) [MIT](https://git.io/JfclH) © [Hoàng Văn Khải](https://github.com/KSXGitHub/)

View File

@ -6,7 +6,8 @@ branding:
inputs: inputs:
version: version:
description: Version of PNPM to install description: Version of PNPM to install
required: true required: false
default: ''
dest: dest:
description: Where to store PNPM files description: Where to store PNPM files
required: false required: false
@ -14,8 +15,7 @@ inputs:
run_install: run_install:
description: If specified, run `pnpm install` description: If specified, run `pnpm install`
required: false required: false
default: 'null' default: ''
runs: runs:
using: node12 using: node12
main: dist/index.js main: dist/index.js
post: dist/index.js

BIN
dist/index.js vendored

Binary file not shown.

View File

@ -3,25 +3,29 @@
"scripts": { "scripts": {
"build:schemas": "ts-schema-autogen generate", "build:schemas": "ts-schema-autogen generate",
"build:ncc": "ncc build --minify --no-source-map-register --no-cache dist/tsc/index.js --out dist/", "build:ncc": "ncc build --minify --no-source-map-register --no-cache dist/tsc/index.js --out dist/",
"build": "pnpm run build:schemas && tsc && pnpm run build:ncc", "build": "pnpm run build:schemas && tsc --skipLibCheck && pnpm run build:ncc",
"start": "pnpm run build && sh ./run.sh" "start": "pnpm run build && sh ./run.sh"
}, },
"dependencies": { "dependencies": {
"node-fetch": "^2.6.1", "@actions/core": "^1.5.0",
"expand-tilde": "^2.0.2", "@actions/tool-cache": "^1.7.1",
"js-yaml": "^4.0.0", "@octokit/rest": "^18.10.0",
"ajv": "^6.12.5",
"fs-extra": "^9.1.0",
"@actions/core": "^1.2.6",
"@types/expand-tilde": "^2.0.0", "@types/expand-tilde": "^2.0.0",
"@types/node-fetch": "^2.5.8", "@types/fs-extra": "^9.0.12",
"@types/js-yaml": "^4.0.0", "@types/js-yaml": "^4.0.3",
"@types/fs-extra": "^9.0.8", "@types/node": "^14.17.14",
"@types/node": "^14.14.35" "@types/node-fetch": "^2.5.12",
"ajv": "^6.12.6",
"expand-tilde": "^2.0.2",
"fs-extra": "^9.1.0",
"js-yaml": "^4.1.0",
"node-fetch": "^2.6.1",
"semver": "^7.3.5"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^4.2.3",
"@ts-schema-autogen/cli": "^0.1.2", "@ts-schema-autogen/cli": "^0.1.2",
"@vercel/ncc": "^0.27.0" "@types/semver": "^7.3.8",
"@vercel/ncc": "^0.27.0",
"typescript": "^4.4.2"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,18 @@
import { setFailed, saveState, getState } from '@actions/core' import { setFailed } from "@actions/core";
import getInputs from './inputs' import getInputs from "./inputs";
import setOutputs from './outputs' import setOutputs from "./outputs";
import installPnpm from './install-pnpm' import installPnpm from "./install-pnpm";
import pnpmInstall from './pnpm-install' import pnpmInstall from "./pnpm-install";
import pruneStore from './pnpm-store-prune'
async function main() { async function main() {
const inputs = getInputs() const inputs = getInputs();
const isPost = getState('is_post') const installLoc = await installPnpm(inputs);
if (isPost === 'true') return pruneStore(inputs) console.log("Installation Completed!");
saveState('is_post', 'true') setOutputs(installLoc);
await installPnpm(inputs) pnpmInstall(inputs, installLoc);
console.log('Installation Completed!')
setOutputs(inputs)
pnpmInstall(inputs)
} }
main().catch(error => { main().catch((error) => {
console.error(error) console.error(error);
setFailed(error) setFailed(error);
}) });

View File

@ -1,23 +1,19 @@
import { getInput, InputOptions } from '@actions/core' import { getInput } from "@actions/core";
import expandTilde from 'expand-tilde' import expandTilde from "expand-tilde";
import { RunInstall, parseRunInstall } from './run-install' import { RunInstall, parseRunInstall } from "./run-install";
export interface Inputs { export interface Inputs {
readonly version: string readonly version?: string;
readonly dest: string readonly dest: string;
readonly runInstall: RunInstall[] readonly runInstall: RunInstall[];
} }
const options: InputOptions = { export function getInputs(): Inputs {
required: true, const dest = expandTilde(getInput("dest"));
let version: string | undefined = getInput("version");
if (version === "") version = undefined;
const runInstall = parseRunInstall(getInput("run_install"));
return { version, dest, runInstall };
} }
const parseInputPath = (name: string) => expandTilde(getInput(name, options)) export default getInputs;
export const getInputs = (): Inputs => ({
version: getInput('version', options),
dest: parseInputPath('dest'),
runInstall: parseRunInstall('run_install'),
})
export default getInputs

View File

@ -1,39 +1,34 @@
import process from 'process' import process from "process";
import { load } from 'js-yaml' import { load } from "js-yaml";
import Ajv from 'ajv' import Ajv from "ajv";
import { getInput, error, InputOptions } from '@actions/core' import { getInput, error } from "@actions/core";
import runInstallSchema from './run-install-input.schema.json' import runInstallSchema from "./run-install-input.schema.json";
export interface RunInstall { export interface RunInstall {
readonly recursive?: boolean readonly recursive?: boolean;
readonly cwd?: string readonly cwd?: string;
readonly args?: readonly string[] readonly args?: readonly string[];
} }
export type RunInstallInput = export type RunInstallInput = null | boolean | RunInstall | RunInstall[];
| null
| boolean
| RunInstall
| RunInstall[]
const options: InputOptions = {
required: true,
}
export function parseRunInstall(name: string): RunInstall[] { export function parseRunInstall(name: string): RunInstall[] {
const result: RunInstallInput = load(getInput(name, options)) as any const runInstall = getInput(name, { required: false });
if (runInstall === "") return [];
const result: RunInstallInput = load(runInstall) as any;
const ajv = new Ajv({ const ajv = new Ajv({
allErrors: true, allErrors: true,
}) });
const validate = ajv.compile(runInstallSchema) const validate = ajv.compile(runInstallSchema);
if (!validate(result)) { if (!validate(result)) {
for (const errorItem of validate.errors!) { for (const errorItem of validate.errors!) {
error(`with.run_install${errorItem.dataPath}: ${errorItem.message}`) error(`with.run_install${errorItem.dataPath}: ${errorItem.message}`);
} }
return process.exit(1) return process.exit(1);
} }
if (!result) return [] if (!result) return [];
if (result === true) return [{ recursive: true }] if (result === true) return [{ recursive: true }];
if (Array.isArray(result)) return result if (Array.isArray(result)) return result;
return [result] return [result];
} }

View File

@ -0,0 +1,83 @@
import { Inputs } from "../inputs";
import * as tc from "@actions/tool-cache";
import { info } from "@actions/core";
import { Octokit } from "@octokit/rest";
import * as semver from "semver";
import * as fs from "fs";
import * as util from "util";
const chmod = util.promisify(fs.chmod);
export async function installFromGithubRelease(
inputs: Inputs
): Promise<string> {
let releaseExeName: string;
let targetExeName: string;
let needChmod: boolean = false;
if (process.platform === "win32") {
releaseExeName = "pnpm-win-x64.exe";
targetExeName = "pnpm.exe";
} else if (process.platform === "darwin") {
releaseExeName = "pnpm-macos-x64";
targetExeName = "pnpm";
} else {
releaseExeName = "pnpm-linux-x64";
targetExeName = "pnpm";
needChmod = true;
}
let verToInstall: string;
let downloadUrl: string;
if (inputs.version && semver.valid(inputs.version)) {
// a specific version is requested
verToInstall = semver.clean(inputs.version)!;
downloadUrl = `https://github.com/pnpm/pnpm/releases/download/v${verToInstall}/${releaseExeName}`;
} else {
// search for version by pattern in inputs.version or use latest
const octokit = new Octokit();
const allReleases = await octokit.repos.listReleases({
owner: "pnpm",
repo: "pnpm",
});
const releases = allReleases.data.filter((r) => {
if (inputs.version) {
const ver = semver.parse(r.tag_name);
return ver && semver.satisfies(ver, inputs.version);
} else {
return semver.valid(r.tag_name) && !r.prerelease && !r.draft;
}
});
if (releases.length === 0) {
throw new Error(
`Unable to find any pnpm releases matching ${inputs.version}`
);
}
const release = releases.reduce((max, r) =>
semver.gt(max.tag_name, r.tag_name) ? max : r
);
verToInstall = semver.clean(release.tag_name)!;
downloadUrl = release.assets.find(
(a) => a.name === releaseExeName
)!.browser_download_url;
}
const cachedToolPath = tc.find("pnpm", verToInstall);
if (!cachedToolPath) {
info("Downloading " + downloadUrl);
const pnpmDownloadPath = await tc.downloadTool(downloadUrl);
if (needChmod) {
await chmod(pnpmDownloadPath, 0o755);
}
return await tc.cacheFile(
pnpmDownloadPath,
targetExeName,
"pnpm",
verToInstall
);
} else {
info("Loading from tool cache");
return cachedToolPath;
}
}

View File

@ -1,16 +1,34 @@
import { setFailed, startGroup, endGroup } from '@actions/core' import { startGroup, endGroup } from "@actions/core";
import { Inputs } from '../inputs' import { Inputs } from "../inputs";
import runSelfInstaller from './run' import { runSelfInstaller } from "./run";
import { installFromGithubRelease } from "./githubRelease";
import * as semver from "semver";
export { runSelfInstaller } export interface InstallLocation {
readonly installFolder: string;
readonly dest?: string;
}
export async function install(inputs: Inputs) { export async function install(inputs: Inputs): Promise<InstallLocation> {
startGroup('Running self-installer...') if (inputs.version) {
const status = await runSelfInstaller(inputs) const ver = semver.parse(inputs.version);
endGroup() if (ver && semver.lt(ver, "6.13.0")) {
if (status) { startGroup("Running self-installer...");
return setFailed(`Something does wrong, self-installer exits with code ${status}`) const binDir = await runSelfInstaller(inputs);
endGroup();
return {
installFolder: binDir,
dest: inputs.dest,
};
} }
} }
export default install startGroup("Installing from github releases...");
const installFolder = await installFromGithubRelease(inputs);
endGroup();
return {
installFolder,
};
}
export default install;

View File

@ -1,31 +1,35 @@
import { spawn } from 'child_process' import { spawn } from "child_process";
import { execPath } from 'process' import { execPath } from "process";
import { join } from 'path' import { join } from "path";
import { remove, ensureFile, writeFile } from 'fs-extra' import { remove, ensureFile, writeFile } from "fs-extra";
import fetch from 'node-fetch' import fetch from "node-fetch";
import { Inputs } from '../inputs' import { Inputs } from "../inputs";
export async function runSelfInstaller(inputs: Inputs): Promise<number> { export async function runSelfInstaller(inputs: Inputs): Promise<string> {
const { version, dest } = inputs const { version, dest } = inputs;
const target = version ? `pnpm@${version}` : 'pnpm' const target = version ? `pnpm@${version}` : "pnpm";
const pkgJson = join(dest, 'package.json') const pkgJson = join(dest, "package.json");
await remove(dest) await remove(dest);
await ensureFile(pkgJson) await ensureFile(pkgJson);
await writeFile(pkgJson, JSON.stringify({ private: true })) await writeFile(pkgJson, JSON.stringify({ private: true }));
const cp = spawn(execPath, ['-', 'install', target, '--no-lockfile'], { const cp = spawn(execPath, ["-", "install", target, "--no-lockfile"], {
cwd: dest, cwd: dest,
stdio: ['pipe', 'inherit', 'inherit'], stdio: ["pipe", "inherit", "inherit"],
}) });
const response = await fetch('https://pnpm.js.org/pnpm.js') const response = await fetch("https://pnpm.js.org/pnpm.js");
response.body.pipe(cp.stdin) response.body.pipe(cp.stdin);
return new Promise((resolve, reject) => { const status: number = await new Promise((resolve, reject) => {
cp.on('error', reject) cp.on("error", reject);
cp.on('close', resolve) cp.on("close", resolve);
}) });
if (status) {
throw new Error(`pnpm self installer exited with status ${status}`);
} else {
return join(inputs.dest, "node_modules", ".bin");
}
} }
export default runSelfInstaller

View File

@ -1,12 +1,12 @@
import { setOutput, addPath } from '@actions/core' import { addPath, setOutput } from "@actions/core";
import { Inputs } from '../inputs' import { InstallLocation } from "../install-pnpm";
import { getBinDest } from '../utils'
export function setOutputs(inputs: Inputs) { export function setOutputs(install: InstallLocation) {
const binDest = getBinDest(inputs) setOutput("bin_dest", install.installFolder);
addPath(binDest) addPath(install.installFolder);
setOutput('dest', inputs.dest) if (install.dest) {
setOutput('bin_dest', binDest) setOutput("dest", install.dest);
}
} }
export default setOutputs export default setOutputs;

View File

@ -1,38 +1,44 @@
import { spawnSync } from 'child_process' import { spawnSync } from "child_process";
import { setFailed, startGroup, endGroup } from '@actions/core' import path from "path";
import { Inputs } from '../inputs' import { setFailed, startGroup, endGroup } from "@actions/core";
import { patchPnpmEnv } from '../utils' import { Inputs } from "../inputs";
import { InstallLocation } from "../install-pnpm";
export function runPnpmInstall(inputs: Inputs) { export function runPnpmInstall(inputs: Inputs, installLoc: InstallLocation) {
const env = patchPnpmEnv(inputs) const env = {
...process.env,
PATH: installLoc.installFolder + path.delimiter + process.env.PATH,
};
for (const options of inputs.runInstall) { for (const options of inputs.runInstall) {
const args = ['install'] const args = ["install"];
if (options.recursive) args.unshift('recursive') if (options.recursive) args.unshift("recursive");
if (options.args) args.push(...options.args) if (options.args) args.push(...options.args);
const cmdStr = ['pnpm', ...args].join(' ') const cmdStr = ["pnpm", ...args].join(" ");
startGroup(`Running ${cmdStr}...`) startGroup(`Running ${cmdStr}...`);
const { error, status } = spawnSync('pnpm', args, { const { error, status } = spawnSync("pnpm", args, {
stdio: 'inherit', stdio: "inherit",
cwd: options.cwd, cwd: options.cwd,
shell: true, shell: true,
env, env,
}) });
endGroup() endGroup();
if (error) { if (error) {
setFailed(error) setFailed(error);
continue continue;
} }
if (status) { if (status) {
setFailed(`Command ${cmdStr} (cwd: ${options.cwd}) exits with status ${status}`) setFailed(
continue `Command ${cmdStr} (cwd: ${options.cwd}) exits with status ${status}`
);
continue;
} }
} }
} }
export default runPnpmInstall export default runPnpmInstall;

View File

@ -1,31 +0,0 @@
import { spawnSync } from 'child_process'
import { warning, startGroup, endGroup } from '@actions/core'
import { Inputs } from '../inputs'
import { patchPnpmEnv } from '../utils'
export function pruneStore(inputs: Inputs) {
if (inputs.runInstall.length === 0) {
console.log('Pruning is unnecessary.')
return
}
startGroup('Running pnpm store prune...')
const { error, status } = spawnSync('pnpm', ['store', 'prune'], {
stdio: 'inherit',
shell: true,
env: patchPnpmEnv(inputs)
})
endGroup()
if (error) {
warning(error)
return
}
if (status) {
warning(`command pnpm store prune exits with code ${status}`)
return
}
}
export default pruneStore

View File

@ -1,10 +0,0 @@
import process from 'process'
import path from 'path'
import { Inputs } from '../inputs'
export const getBinDest = (inputs: Inputs): string => path.join(inputs.dest, 'node_modules', '.bin')
export const patchPnpmEnv = (inputs: Inputs): NodeJS.ProcessEnv => ({
...process.env,
PATH: getBinDest(inputs) + path.delimiter + process.env.PATH
})

View File

@ -1,7 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2018", "target": "ES2019",
"module": "CommonJS", "module": "CommonJS",
"moduleResolution": "Node", "moduleResolution": "Node",
"resolveJsonModule": true, "resolveJsonModule": true,