Merge pull request #166 from nix6839/many-package-manager

Support many package managers
This commit is contained in:
Jacob M-G Evans 2023-09-18 14:11:51 -05:00 committed by GitHub
commit 13d70757d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 314 additions and 36 deletions

View file

@ -0,0 +1,7 @@
---
"wrangler-action": minor
---
Support for package managers other than npm, such as pnpm and yarn.
fixes #156

View file

@ -111,4 +111,43 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: delete --name wrangler-action-test --force command: delete --name wrangler-action-test --force
# END Setup and teardown of Workers w/ Secrets Tests # END Setup and teardown of Workers w/ Secrets Tests
- name: Support packageManager variable
uses: ./
with:
workingDirectory: "./test/empty"
packageManager: "npm"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --dry-run
- name: Support npm package manager
uses: ./
with:
workingDirectory: "./test/npm"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --dry-run
- name: Install yarn
run: npm i -g yarn
- name: Support yarn package manager
uses: ./
with:
workingDirectory: "./test/yarn"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --dry-run
- name: Install pnpm
run: npm i -g pnpm
- name: Support pnpm package manager
uses: ./
with:
workingDirectory: "./test/pnpm"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --dry-run

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
pnpm-lock.yaml

View file

@ -41,3 +41,6 @@ inputs:
vars: vars:
description: "A string of environment variable names, separated by newlines. These will be bound to your Worker using the values of matching environment variables declared in `env` of this workflow." description: "A string of environment variable names, separated by newlines. These will be bound to your Worker using the values of matching environment variables declared in `env` of this workflow."
required: false required: false
packageManager:
description: "The name of the package manager to install and run wrangler. If not provided, it will be detected via the lock file. Valid values: [npm, pnpm, yarn]"
required: false

View file

@ -1,20 +1,46 @@
import { import {
getBooleanInput,
getInput, getInput,
getMultilineInput, getMultilineInput,
setFailed,
info as originalInfo,
error as originalError,
endGroup as originalEndGroup, endGroup as originalEndGroup,
error as originalError,
info as originalInfo,
startGroup as originalStartGroup, startGroup as originalStartGroup,
getBooleanInput, setFailed,
} from "@actions/core"; } from "@actions/core";
import { execSync, exec } from "node:child_process"; import { exec, execSync } from "node:child_process";
import { checkWorkingDirectory, getNpxCmd, semverCompare } from "./utils";
import * as util from "node:util"; import * as util from "node:util";
import {
PackageManager,
checkWorkingDirectory,
detectPackageManager,
isValidPackageManager,
semverCompare,
} from "./utils";
const execAsync = util.promisify(exec); const execAsync = util.promisify(exec);
const DEFAULT_WRANGLER_VERSION = "3.5.1"; const DEFAULT_WRANGLER_VERSION = "3.5.1";
interface PackageManagerCommands {
install: string;
exec: string;
}
const PACKAGE_MANAGER_COMMANDS = {
npm: {
install: "npm i",
exec: "npx",
},
yarn: {
install: "yarn add",
exec: "yarn",
},
pnpm: {
install: "pnpm add",
exec: "pnpm exec",
},
} as const satisfies Readonly<Record<PackageManager, PackageManagerCommands>>;
/** /**
* A configuration object that contains all the inputs & immutable state for the action. * A configuration object that contains all the inputs & immutable state for the action.
*/ */
@ -28,8 +54,24 @@ const config = {
VARS: getMultilineInput("vars"), VARS: getMultilineInput("vars"),
COMMANDS: getMultilineInput("command"), COMMANDS: getMultilineInput("command"),
QUIET_MODE: getBooleanInput("quiet"), QUIET_MODE: getBooleanInput("quiet"),
PACKAGE_MANAGER: getInput("packageManager"),
} as const; } as const;
function realPackageManager(): PackageManager {
if (isValidPackageManager(config.PACKAGE_MANAGER)) {
return config.PACKAGE_MANAGER;
}
const packageManager = detectPackageManager(config.workingDirectory);
if (packageManager !== null) {
return packageManager;
}
throw new Error("Package manager is not detected");
}
const pkgManagerCmd = PACKAGE_MANAGER_COMMANDS[realPackageManager()];
function info(message: string, bypass?: boolean): void { function info(message: string, bypass?: boolean): void {
if (!config.QUIET_MODE || bypass) { if (!config.QUIET_MODE || bypass) {
originalInfo(message); originalInfo(message);
@ -94,7 +136,7 @@ function installWrangler() {
); );
} }
startGroup("📥 Installing Wrangler"); startGroup("📥 Installing Wrangler");
const command = `npm install wrangler@${config["WRANGLER_VERSION"]}`; const command = `${pkgManagerCmd.install} wrangler@${config["WRANGLER_VERSION"]}`;
info(`Running command: ${command}`); info(`Running command: ${command}`);
execSync(command, { cwd: config["workingDirectory"], env: process.env }); execSync(command, { cwd: config["workingDirectory"], env: process.env });
info(`✅ Wrangler installed`, true); info(`✅ Wrangler installed`, true);
@ -115,7 +157,7 @@ async function execCommands(commands: string[], cmdType: string) {
try { try {
const arrPromises = commands.map(async (command) => { const arrPromises = commands.map(async (command) => {
const cmd = command.startsWith("wrangler") const cmd = command.startsWith("wrangler")
? `${getNpxCmd()} ${command}` ? `${pkgManagerCmd.exec} ${command}`
: command; : command;
info(`🚀 Executing command: ${cmd}`); info(`🚀 Executing command: ${cmd}`);
@ -155,9 +197,9 @@ async function legacyUploadSecrets(
) { ) {
const arrPromises = secrets const arrPromises = secrets
.map((secret) => { .map((secret) => {
const command = `echo ${getSecret( const command = `echo ${getSecret(secret)} | ${
secret, pkgManagerCmd.exec
)} | ${getNpxCmd()} wrangler secret put ${secret}`; } wrangler secret put ${secret}`;
return environment ? command.concat(` --env ${environment}`) : command; return environment ? command.concat(` --env ${environment}`) : command;
}) })
.map( .map(
@ -198,7 +240,7 @@ async function uploadSecrets() {
const secretCmd = `echo "${JSON.stringify(secretObj).replaceAll( const secretCmd = `echo "${JSON.stringify(secretObj).replaceAll(
'"', '"',
'\\"', '\\"',
)}" | ${getNpxCmd()} wrangler secret:bulk ${environmentSuffix}`; )}" | ${pkgManagerCmd.exec} wrangler secret:bulk ${environmentSuffix}`;
execSync(secretCmd, { execSync(secretCmd, {
cwd: workingDirectory, cwd: workingDirectory,
@ -247,7 +289,7 @@ async function wranglerCommands() {
command = command.concat(` --env ${environment}`); command = command.concat(` --env ${environment}`);
} }
const cmd = `${getNpxCmd()} wrangler ${command} ${ const cmd = `${pkgManagerCmd.exec} wrangler ${command} ${
(command.startsWith("deploy") || command.startsWith("publish")) && (command.startsWith("deploy") || command.startsWith("publish")) &&
!command.includes(`--var`) !command.includes(`--var`)
? getVarArgs() ? getVarArgs()
@ -271,9 +313,9 @@ async function wranglerCommands() {
main(); main();
export { export {
wranglerCommands,
execCommands,
uploadSecrets,
authenticationSetup, authenticationSetup,
execCommands,
installWrangler, installWrangler,
uploadSecrets,
wranglerCommands,
}; };

View file

@ -1,19 +1,11 @@
import { expect, test, describe } from "vitest";
import { checkWorkingDirectory, getNpxCmd, semverCompare } from "./utils";
import path from "node:path"; import path from "node:path";
import { describe, expect, test } from "vitest";
test("getNpxCmd ", async () => { import {
process.env.RUNNER_OS = "Windows"; checkWorkingDirectory,
expect(getNpxCmd()).toBe("npx.cmd"); detectPackageManager,
isValidPackageManager,
process.env.RUNNER_OS = "Mac"; semverCompare,
expect(getNpxCmd()).toBe("npx"); } from "./utils";
process.env.RUNNER_OS = "Linux";
expect(getNpxCmd()).toBe("npx");
delete process.env.RUNNER_OS;
});
describe("semverCompare", () => { describe("semverCompare", () => {
test("should return false if the second argument is equal to the first argument", () => { test("should return false if the second argument is equal to the first argument", () => {
@ -43,3 +35,34 @@ describe("checkWorkingDirectory", () => {
); );
}); });
}); });
describe("detectPackageManager", () => {
test("should return name of package manager for current workspace", () => {
expect(detectPackageManager()).toBe("npm");
});
test("should return npm if package-lock.json exists", () => {
expect(detectPackageManager("test/npm")).toBe("npm");
});
test("should return yarn if yarn.lock exists", () => {
expect(detectPackageManager("test/yarn")).toBe("yarn");
});
test("should return pnpm if pnpm-lock.yaml exists", () => {
expect(detectPackageManager("test/pnpm")).toBe("pnpm");
});
test("should return null if no package manager is detected", () => {
expect(detectPackageManager("test/empty")).toBe(null);
});
});
test("isValidPackageManager", () => {
expect(isValidPackageManager("npm")).toBe(true);
expect(isValidPackageManager("pnpm")).toBe(true);
expect(isValidPackageManager("yarn")).toBe(true);
expect(isValidPackageManager("")).toBe(false);
expect(isValidPackageManager("ppnpm")).toBe(false);
});

View file

@ -1,10 +1,6 @@
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
export function getNpxCmd() {
return process.env.RUNNER_OS === "Windows" ? "npx.cmd" : "npx";
}
/** /**
* A helper function to compare two semver versions. If the second arg is greater than the first arg, it returns true. * A helper function to compare two semver versions. If the second arg is greater than the first arg, it returns true.
*/ */
@ -33,3 +29,24 @@ export function checkWorkingDirectory(workingDirectory = ".") {
throw new Error(`Directory ${workingDirectory} does not exist.`); throw new Error(`Directory ${workingDirectory} does not exist.`);
} }
} }
export type PackageManager = "npm" | "yarn" | "pnpm";
export function detectPackageManager(
workingDirectory = ".",
): PackageManager | null {
if (existsSync(path.join(workingDirectory, "package-lock.json"))) {
return "npm";
}
if (existsSync(path.join(workingDirectory, "yarn.lock"))) {
return "yarn";
}
if (existsSync(path.join(workingDirectory, "pnpm-lock.yaml"))) {
return "pnpm";
}
return null;
}
export function isValidPackageManager(name: string): name is PackageManager {
return name === "npm" || name === "yarn" || name === "pnpm";
}

10
test/base/package-lock.json generated Normal file
View file

@ -0,0 +1,10 @@
{
"name": "wrangler-action-test",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wrangler-action-test"
}
}
}

1
test/empty/index.ts Normal file
View file

@ -0,0 +1 @@
export default {};

3
test/empty/package.json Normal file
View file

@ -0,0 +1,3 @@
{
"name": "wrangler-action-detect-package-manager-test"
}

4
test/empty/wrangler.toml Normal file
View file

@ -0,0 +1,4 @@
name = "wrangler-action-test"
main = "./index.ts"
compatibility_date = "2023-07-07"
workers_dev = true

10
test/environment/package-lock.json generated Normal file
View file

@ -0,0 +1,10 @@
{
"name": "wrangler-action-environment-test",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wrangler-action-environment-test"
}
}
}

26
test/npm/index.ts Normal file
View file

@ -0,0 +1,26 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
export default {
fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (url.pathname === "/secret-health-check") {
const { SECRET1, SECRET2 } = env;
if (SECRET1 !== "SECRET_1_VALUE" || SECRET2 !== "SECRET_2_VALUE") {
throw new Error("SECRET1 or SECRET2 is not defined");
}
return new Response("OK");
}
// @ts-expect-error
return Response.json({
...request,
headers: Object.fromEntries(request.headers),
});
},
};

10
test/npm/package-lock.json generated Normal file
View file

@ -0,0 +1,10 @@
{
"name": "wrangler-action-npm-test",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wrangler-action-npm-test"
}
}
}

3
test/npm/package.json Normal file
View file

@ -0,0 +1,3 @@
{
"name": "wrangler-action-npm-test"
}

4
test/npm/wrangler.toml Normal file
View file

@ -0,0 +1,4 @@
name = "wrangler-action-test"
main = "./index.ts"
compatibility_date = "2023-07-07"
workers_dev = true

26
test/pnpm/index.ts Normal file
View file

@ -0,0 +1,26 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
export default {
fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (url.pathname === "/secret-health-check") {
const { SECRET1, SECRET2 } = env;
if (SECRET1 !== "SECRET_1_VALUE" || SECRET2 !== "SECRET_2_VALUE") {
throw new Error("SECRET1 or SECRET2 is not defined");
}
return new Response("OK");
}
// @ts-expect-error
return Response.json({
...request,
headers: Object.fromEntries(request.headers),
});
},
};

3
test/pnpm/package.json Normal file
View file

@ -0,0 +1,3 @@
{
"name": "wrangler-action-pnpm-test"
}

5
test/pnpm/pnpm-lock.yaml Normal file
View file

@ -0,0 +1,5 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

4
test/pnpm/wrangler.toml Normal file
View file

@ -0,0 +1,4 @@
name = "wrangler-action-test"
main = "./index.ts"
compatibility_date = "2023-07-07"
workers_dev = true

26
test/yarn/index.ts Normal file
View file

@ -0,0 +1,26 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
export default {
fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (url.pathname === "/secret-health-check") {
const { SECRET1, SECRET2 } = env;
if (SECRET1 !== "SECRET_1_VALUE" || SECRET2 !== "SECRET_2_VALUE") {
throw new Error("SECRET1 or SECRET2 is not defined");
}
return new Response("OK");
}
// @ts-expect-error
return Response.json({
...request,
headers: Object.fromEntries(request.headers),
});
},
};

3
test/yarn/package.json Normal file
View file

@ -0,0 +1,3 @@
{
"name": "wrangler-action-yarn-test"
}

4
test/yarn/wrangler.toml Normal file
View file

@ -0,0 +1,4 @@
name = "wrangler-action-test"
main = "./index.ts"
compatibility_date = "2023-07-07"
workers_dev = true

4
test/yarn/yarn.lock Normal file
View file

@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1