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`
**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`
**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`
**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.
@ -24,100 +32,100 @@ If `run_install` is a YAML string representation of either an object or an array
#### `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`
**Optional** (_type:_ `string`) Working directory when run `pnpm [recursive] install`.
(_type:_ `string`) Working directory when run `pnpm [recursive] install`.
#### `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
### `dest`
Expanded path of inputs#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
### 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
on:
- push
- pull_request
jobs:
runs-on: ubuntu-latest
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
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
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
on:
- push
- pull_request
jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "16"
- uses: pnpm/action-setup@v2.0.1
with:
version: 6.0.2
version: "6.*"
run_install: |
- recursive: true
args: [--frozen-lockfile, --strict-peer-dependencies]
- 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
[MIT](https://git.io/JfclH) © [Hoàng Văn Khải](https://github.com/KSXGitHub/)

View File

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

BIN
dist/index.js vendored

Binary file not shown.

View File

@ -3,25 +3,29 @@
"scripts": {
"build:schemas": "ts-schema-autogen generate",
"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"
},
"dependencies": {
"node-fetch": "^2.6.1",
"expand-tilde": "^2.0.2",
"js-yaml": "^4.0.0",
"ajv": "^6.12.5",
"fs-extra": "^9.1.0",
"@actions/core": "^1.2.6",
"@actions/core": "^1.5.0",
"@actions/tool-cache": "^1.7.1",
"@octokit/rest": "^18.10.0",
"@types/expand-tilde": "^2.0.0",
"@types/node-fetch": "^2.5.8",
"@types/js-yaml": "^4.0.0",
"@types/fs-extra": "^9.0.8",
"@types/node": "^14.14.35"
"@types/fs-extra": "^9.0.12",
"@types/js-yaml": "^4.0.3",
"@types/node": "^14.17.14",
"@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": {
"typescript": "^4.2.3",
"@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 getInputs from './inputs'
import setOutputs from './outputs'
import installPnpm from './install-pnpm'
import pnpmInstall from './pnpm-install'
import pruneStore from './pnpm-store-prune'
import { setFailed } from "@actions/core";
import getInputs from "./inputs";
import setOutputs from "./outputs";
import installPnpm from "./install-pnpm";
import pnpmInstall from "./pnpm-install";
async function main() {
const inputs = getInputs()
const isPost = getState('is_post')
if (isPost === 'true') return pruneStore(inputs)
saveState('is_post', 'true')
await installPnpm(inputs)
console.log('Installation Completed!')
setOutputs(inputs)
pnpmInstall(inputs)
const inputs = getInputs();
const installLoc = await installPnpm(inputs);
console.log("Installation Completed!");
setOutputs(installLoc);
pnpmInstall(inputs, installLoc);
}
main().catch(error => {
console.error(error)
setFailed(error)
})
main().catch((error) => {
console.error(error);
setFailed(error);
});

View File

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

View File

@ -1,39 +1,34 @@
import process from 'process'
import { load } from 'js-yaml'
import Ajv from 'ajv'
import { getInput, error, InputOptions } from '@actions/core'
import runInstallSchema from './run-install-input.schema.json'
import process from "process";
import { load } from "js-yaml";
import Ajv from "ajv";
import { getInput, error } from "@actions/core";
import runInstallSchema from "./run-install-input.schema.json";
export interface RunInstall {
readonly recursive?: boolean
readonly cwd?: string
readonly args?: readonly string[]
readonly recursive?: boolean;
readonly cwd?: string;
readonly args?: readonly string[];
}
export type RunInstallInput =
| null
| boolean
| RunInstall
| RunInstall[]
const options: InputOptions = {
required: true,
}
export type RunInstallInput = null | boolean | RunInstall | 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({
allErrors: true,
})
const validate = ajv.compile(runInstallSchema)
});
const validate = ajv.compile(runInstallSchema);
if (!validate(result)) {
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 === true) return [{ recursive: true }]
if (Array.isArray(result)) return result
return [result]
if (!result) return [];
if (result === true) return [{ recursive: true }];
if (Array.isArray(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 { Inputs } from '../inputs'
import runSelfInstaller from './run'
import { startGroup, endGroup } from "@actions/core";
import { Inputs } from "../inputs";
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) {
startGroup('Running self-installer...')
const status = await runSelfInstaller(inputs)
endGroup()
if (status) {
return setFailed(`Something does wrong, self-installer exits with code ${status}`)
export async function install(inputs: Inputs): Promise<InstallLocation> {
if (inputs.version) {
const ver = semver.parse(inputs.version);
if (ver && semver.lt(ver, "6.13.0")) {
startGroup("Running self-installer...");
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 { execPath } from 'process'
import { join } from 'path'
import { remove, ensureFile, writeFile } from 'fs-extra'
import fetch from 'node-fetch'
import { Inputs } from '../inputs'
import { spawn } from "child_process";
import { execPath } from "process";
import { join } from "path";
import { remove, ensureFile, writeFile } from "fs-extra";
import fetch from "node-fetch";
import { Inputs } from "../inputs";
export async function runSelfInstaller(inputs: Inputs): Promise<number> {
const { version, dest } = inputs
const target = version ? `pnpm@${version}` : 'pnpm'
const pkgJson = join(dest, 'package.json')
export async function runSelfInstaller(inputs: Inputs): Promise<string> {
const { version, dest } = inputs;
const target = version ? `pnpm@${version}` : "pnpm";
const pkgJson = join(dest, "package.json");
await remove(dest)
await ensureFile(pkgJson)
await writeFile(pkgJson, JSON.stringify({ private: true }))
await remove(dest);
await ensureFile(pkgJson);
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,
stdio: ['pipe', 'inherit', 'inherit'],
})
stdio: ["pipe", "inherit", "inherit"],
});
const response = await fetch('https://pnpm.js.org/pnpm.js')
response.body.pipe(cp.stdin)
const response = await fetch("https://pnpm.js.org/pnpm.js");
response.body.pipe(cp.stdin);
return new Promise((resolve, reject) => {
cp.on('error', reject)
cp.on('close', resolve)
})
const status: number = await new Promise((resolve, reject) => {
cp.on("error", reject);
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 { Inputs } from '../inputs'
import { getBinDest } from '../utils'
import { addPath, setOutput } from "@actions/core";
import { InstallLocation } from "../install-pnpm";
export function setOutputs(inputs: Inputs) {
const binDest = getBinDest(inputs)
addPath(binDest)
setOutput('dest', inputs.dest)
setOutput('bin_dest', binDest)
export function setOutputs(install: InstallLocation) {
setOutput("bin_dest", install.installFolder);
addPath(install.installFolder);
if (install.dest) {
setOutput("dest", install.dest);
}
}
export default setOutputs
export default setOutputs;

View File

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