Compare commits

..

1 Commits

Author SHA1 Message Date
Maximo Guk ce4fd508df test 2024-11-26 07:39:09 -06:00
50 changed files with 38845 additions and 2637 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"wrangler-action": minor
---
Add GitHub deployments and job summaries for parity with pages-action
+7 -6
View File
@@ -9,12 +9,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v5
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v3
with:
node-version: "24"
# Pinned due to compatibility issues on 23.2.0
node-version: "22"
cache: "npm"
- name: Install modules and build
@@ -75,7 +76,7 @@ jobs:
SECRET2: ${{ secrets.SECRET2 }}
- name: Health Check Deployed Worker
run: node .github/workflows/workerHealthCheck.ts wrangler-action-test-secrets-v2
run: node .github/workflows/workerHealthCheck.cjs wrangler-action-test-secrets-v2
shell: bash
- name: Deploy app secrets w/ default version
@@ -92,7 +93,7 @@ jobs:
SECRET2: ${{ secrets.SECRET2 }}
- name: Health Check Deployed Worker
run: node .github/workflows/workerHealthCheck.ts wrangler-action-test-secrets-default
run: node .github/workflows/workerHealthCheck.cjs wrangler-action-test-secrets-default
shell: bash
- name: Clean Up Deployed Workers
@@ -142,7 +143,7 @@ jobs:
command: deploy --dry-run
- name: Install pnpm
run: npm i -g pnpm@10
run: npm i -g pnpm
- name: Support pnpm package manager
uses: ./
+4 -3
View File
@@ -19,15 +19,16 @@ jobs:
issues: read
steps:
- name: Checkout Repo
uses: actions/checkout@v5
uses: actions/checkout@v3
with:
fetch-depth: 0
fetch-tags: true
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v3
with:
node-version: "24"
# Pinned due to compatibility issues on 23.2.0
node-version: "22"
cache: "npm"
- name: Install modules
+17 -23
View File
@@ -1,30 +1,24 @@
name: Semgrep OSS scan
on:
pull_request: {}
push:
branches: [main, master]
workflow_dispatch: {}
push:
branches:
- main
- master
schedule:
- cron: "0 0 20 * *"
concurrency:
group: semgrep-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
- cron: "0 0 * * *"
name: Semgrep config
jobs:
semgrep:
name: semgrep-oss
runs-on: ubuntu-slim
name: semgrep/ci
runs-on: ubuntu-latest
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
SEMGREP_URL: https://cloudflare.semgrep.dev
SEMGREP_APP_URL: https://cloudflare.semgrep.dev
SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 1
- id: cache-semgrep
uses: actions/cache@v5
with:
path: ~/.local
key: semgrep-1.160.0-${{ runner.os }}
- if: steps.cache-semgrep.outputs.cache-hit != 'true'
run: pip install --user semgrep==1.160.0
- run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- run: semgrep scan --config=auto
- uses: actions/checkout@v4
- run: semgrep ci
+28
View File
@@ -0,0 +1,28 @@
const { execSync } = require("child_process");
function workerHealthCheck(workerName) {
const url = `https://${workerName}.devprod-testing7928.workers.dev/secret-health-check`;
const buffer = execSync(`curl ${url}`);
const response = buffer.toString();
if (response.includes("OK")) {
console.log(`Status: Worker is up! Response: ${response}`);
} else {
throw new Error(`Worker is down! Response: ${response}`);
}
return response;
}
const args = Array.from(process.argv);
const workerName = args.pop();
if (!workerName) {
throw new Error(
"Please provide the worker name as an argument when calling this program.",
);
}
workerHealthCheck(workerName);
-41
View File
@@ -1,41 +0,0 @@
import { Result } from "better-result";
async function workerHealthCheck(workerName) {
const url = `https://${workerName}.devprod-testing7928.workers.dev/secret-health-check`;
const response = await fetch(url);
const text = await response.text();
if (text.includes("OK")) {
console.log(`Status: Worker is up! Response: ${text}`);
} else {
throw new Error(`Worker is down! Response: ${text}`);
}
return text;
}
const args = Array.from(process.argv);
const workerName = args.pop();
if (!workerName) {
throw new Error(
"Please provide the worker name as an argument when calling this program.",
);
}
const result = await Result.tryPromise(() => workerHealthCheck(workerName), {
retry: {
times: 5,
delayMs: 2000,
backoff: "exponential",
},
});
result.match({
ok: () => {},
err: (error) => {
console.error(error);
process.exit(1);
},
});
-1
View File
@@ -1,4 +1,3 @@
dist
.idea
.vscode
+1 -42
View File
@@ -1,47 +1,5 @@
# Changelog
## 4.0.0
### Major Changes
- [#412](https://github.com/cloudflare/wrangler-action/pull/412) [`1029e90`](https://github.com/cloudflare/wrangler-action/commit/1029e90033977ccf46c2a9b3ddc55e42ad5da467) Thanks [@ericclemmons](https://github.com/ericclemmons)! - Update default Wrangler version to v4 (`latest`). The action now installs Wrangler v4 by default when no `wranglerVersion` input is specified. Users can still pin to v3 by setting `wranglerVersion: "3.90.0"` explicitly.
## 3.15.0
### Minor Changes
- [#426](https://github.com/cloudflare/wrangler-action/pull/426) [`febbda6`](https://github.com/cloudflare/wrangler-action/commit/febbda69f8c5838bf8b07fd6b9dfc836f00962db) Thanks [@WillTaylorDev](https://github.com/WillTaylorDev)! - Support version ranges and tags in `wranglerVersion` input. You can now set `wranglerVersion` to values like `4`, `^4.0.0`, `4.x`, or `latest` instead of only exact versions like `4.81.0`.
## 3.14.1
### Patch Changes
- [#358](https://github.com/cloudflare/wrangler-action/pull/358) [`cd6314a`](https://github.com/cloudflare/wrangler-action/commit/cd6314a97b09d9a764e30cacd0870edc86f92986) Thanks [@penalosa](https://github.com/penalosa)! - Use `secret bulk` instead of deprecated `secret:bulk` command
## 3.14.0
### Minor Changes
- [#351](https://github.com/cloudflare/wrangler-action/pull/351) [`4ff07f4`](https://github.com/cloudflare/wrangler-action/commit/4ff07f4310dc5067d84a254cd9af3d2e91df119e) Thanks [@Maximo-Guk](https://github.com/Maximo-Guk)! - Use wrangler outputs for version upload and wrangler deploy
### Patch Changes
- [#350](https://github.com/cloudflare/wrangler-action/pull/350) [`e209094`](https://github.com/cloudflare/wrangler-action/commit/e209094e624c6f6b418141b7e9d0ab7838d794a3) Thanks [@Maximo-Guk](https://github.com/Maximo-Guk)! - Handle failures in createGitHubDeployment and createGitHubJobSummary
## 3.13.1
### Patch Changes
- [#345](https://github.com/cloudflare/wrangler-action/pull/345) [`e819570`](https://github.com/cloudflare/wrangler-action/commit/e819570b2d0a69816a1c2e9d2f2954e278748d80) Thanks [@Maximo-Guk](https://github.com/Maximo-Guk)! - fix: Pages GitHub Deployment not triggering
## 3.13.0
### Minor Changes
- [#325](https://github.com/cloudflare/wrangler-action/pull/325) [`cada7a6`](https://github.com/cloudflare/wrangler-action/commit/cada7a63124ded3471bef7e8001b76356b838e40) Thanks [@Maximo-Guk](https://github.com/Maximo-Guk)! - Add GitHub deployments and job summaries for parity with pages-action
- [#334](https://github.com/cloudflare/wrangler-action/pull/334) [`9fed19a`](https://github.com/cloudflare/wrangler-action/commit/9fed19aa4ed79946f009e8aad7437a922e62d523) Thanks [@Maximo-Guk](https://github.com/Maximo-Guk)! - Bump default wrangler version to 3.90.0
## 3.12.1
### Patch Changes
@@ -125,6 +83,7 @@
### Minor Changes
- [#213](https://github.com/cloudflare/wrangler-action/pull/213) [`d13856dfc92816473ebf47f66e263a2668a97896`](https://github.com/cloudflare/wrangler-action/commit/d13856dfc92816473ebf47f66e263a2668a97896) Thanks [@GrantBirki](https://github.com/GrantBirki)! - This change introduces three new GitHub Actions output variables. These variables are as follows:
- `command-output` - contains the string results of `stdout`
- `command-stderr` - contains the string results of `stderr`
- `deployment-url` - contains the string results of the URL that was deployed (ex: `https://<your_pages_site>.pages.dev`)
+10 -23
View File
@@ -2,17 +2,6 @@
Easy-to-use GitHub Action to use [Wrangler](https://developers.cloudflare.com/workers/cli-wrangler/). Makes deploying Workers a breeze.
## Wrangler v3 Support
The action now defaults to **Wrangler v4**. If you need to stay on Wrangler v3, you can pin the version explicitly:
```yaml
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
wranglerVersion: "3.90.0"
```
## Big Changes in v3
- Wrangler v1 is no longer supported.
@@ -38,7 +27,7 @@ jobs:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Deploy
uses: cloudflare/wrangler-action@v3
with:
@@ -63,7 +52,7 @@ jobs:
## Configuration
You can pass `wranglerVersion` to install a specific version of Wrangler from NPM. This accepts any version format NPM understands: an exact version like `4.81.0`, a major version like `4`, a range like `^4.0.0` or `4.x`, or `latest`.
If you need to install a specific version of Wrangler to use for deployment, you can also pass the input `wranglerVersion` to install a specific version of Wrangler from NPM. This should be a [SemVer](https://semver.org/)-style version number, such as `2.20.0`:
```yaml
jobs:
@@ -72,11 +61,9 @@ jobs:
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
wranglerVersion: "4"
wranglerVersion: "2.20.0"
```
If you omit `wranglerVersion` and Wrangler is already installed in your environment, the action uses the existing installation. If Wrangler is not installed, the action installs a default version.
Optionally, you can also pass a `workingDirectory` key to the action. This will allow you to specify a subdirectory of the repo to run the Wrangler command from.
```yaml
@@ -166,7 +153,7 @@ jobs:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Deploy
uses: cloudflare/wrangler-action@v3
with:
@@ -190,7 +177,7 @@ jobs:
contents: read
deployments: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Deploy
uses: cloudflare/wrangler-action@v3
with:
@@ -215,7 +202,7 @@ jobs:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Deploy app
uses: cloudflare/wrangler-action@v3
with:
@@ -241,7 +228,7 @@ jobs:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Deploy app
uses: cloudflare/wrangler-action@v3
with:
@@ -253,7 +240,7 @@ For more advanced usage or to programmatically trigger the workflow from scripts
### Upload a Worker Version
To create a new version of your Worker that is not deployed immediately, use the `wrangler versions upload` command. Worker versions created in this way can then be deployed all at once at a later time or gradually deployed using the `wrangler versions deploy` command or via the Cloudflare dashboard under the Deployments tab. Wrangler v3.40.0 or above is required to use this feature.
To create a new version of your Worker that is not deployed immediately, use the `wrangler versions upload` command. Worker versions created in this way can then be deployed all at once at a later time or gradually deployed using the `wranger versions deploy` command or via the Cloudflare dashboard under the Deployments tab. Wrangler v3.40.0 or above is required to use this feature.
```yaml
jobs:
@@ -261,7 +248,7 @@ jobs:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Upload Worker Version
uses: cloudflare/wrangler-action@v3
with:
@@ -387,7 +374,7 @@ jobs:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Deploy app
uses: cloudflare/wrangler-action@v3
with:
+1 -1
View File
@@ -5,7 +5,7 @@ branding:
description: "Deploy your Cloudflare projects from GitHub using Wrangler"
runs:
# Possible values: https://github.com/actions/runner/blob/main/src/Runner.Common/Util/NodeUtil.cs#L9
using: "node24"
using: "node20"
main: "dist/index.mjs"
inputs:
apiToken:
+6
View File
@@ -0,0 +1,6 @@
import { exec as _childProcessExec } from "node:child_process";
export { exec } from "@actions/exec";
declare const childProcessExec: typeof _childProcessExec.__promisify__;
export declare function execShell(command: string, { silent, ...options }?: Parameters<typeof childProcessExec>[1] & {
silent?: boolean;
}): Promise<number | null>;
+1
View File
@@ -0,0 +1 @@
export {};
+37255
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+24
View File
@@ -0,0 +1,24 @@
export interface PackageManager {
install: string;
exec: string;
execNoInstall: string;
}
export declare function getPackageManager(name: string, { workingDirectory }?: {
workingDirectory?: string;
}): {
readonly install: "npm i";
readonly exec: "npx";
readonly execNoInstall: "npx --no-install";
} | {
readonly install: "yarn add";
readonly exec: "yarn";
readonly execNoInstall: "yarn";
} | {
readonly install: "pnpm add";
readonly exec: "pnpm exec";
readonly execNoInstall: "pnpm exec";
} | {
readonly install: "bun i";
readonly exec: "bunx";
readonly execNoInstall: "bun run";
};
+1
View File
@@ -0,0 +1 @@
export {};
+23
View File
@@ -0,0 +1,23 @@
import { getOctokit } from "@actions/github";
import { OutputEntryPagesDeployment } from "../wranglerArtifactManager";
import { WranglerActionConfig } from "../wranglerAction";
type Octokit = ReturnType<typeof getOctokit>;
export declare function createGitHubDeployment({ config, octokit, productionBranch, environment, deploymentId, projectName, deploymentUrl, }: {
config: WranglerActionConfig;
octokit: Octokit;
productionBranch: string;
environment: string;
deploymentId: string | null;
projectName: string;
deploymentUrl?: string;
}): Promise<void>;
export declare function createJobSummary({ commitHash, deploymentUrl, aliasUrl, }: {
commitHash: string;
deploymentUrl?: string;
aliasUrl?: string;
}): Promise<void>;
/**
* Create github deployment, if GITHUB_TOKEN is present in config
*/
export declare function createGitHubDeploymentAndJobSummary(config: WranglerActionConfig, pagesArtifactFields: OutputEntryPagesDeployment): Promise<void>;
export {};
+1
View File
@@ -0,0 +1 @@
export {};
+8
View File
@@ -0,0 +1,8 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
declare const _default: {
fetch(request: Request, env: Env): Response;
};
export default _default;
+8
View File
@@ -0,0 +1,8 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
declare const _default: {
fetch(request: Request, env: Env): Response;
};
export default _default;
+8
View File
@@ -0,0 +1,8 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
declare const _default: {
fetch(request: Request, env: Env): Response;
};
export default _default;
+8
View File
@@ -0,0 +1,8 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
declare const _default: {
fetch(request: Request, env: Env): Response;
};
export default _default;
+8
View File
@@ -0,0 +1,8 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
declare const _default: {
fetch(request: Request, env: Env): Response;
};
export default _default;
+8
View File
@@ -0,0 +1,8 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
declare const _default: {
fetch(request: Request, env: Env): Response;
};
export default _default;
+8
View File
@@ -0,0 +1,8 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
declare const _default: {
fetch(request: Request, env: Env): Response;
};
export default _default;
+8
View File
@@ -0,0 +1,8 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
declare const _default: {
fetch(request: Request, env: Env): Response;
};
export default _default;
+8
View File
@@ -0,0 +1,8 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
declare const _default: {
fetch(request: Request, env: Env): Response;
};
export default _default;
+2
View File
@@ -0,0 +1,2 @@
declare const _default: {};
export default _default;
@@ -0,0 +1,2 @@
declare const _default: {};
export default _default;
+8
View File
@@ -0,0 +1,8 @@
type Env = {
SECRET1?: string;
SECRET2?: string;
};
declare const _default: {
fetch(request: Request, env: Env): Response;
};
export default _default;
+6
View File
@@ -0,0 +1,6 @@
export declare function mockGithubDeployments({ githubUser, githubRepoName, }: {
githubUser: string;
githubRepoName: string;
}): {
handlers: import("msw").HttpHandler[];
};
+4
View File
@@ -0,0 +1,4 @@
import { WranglerActionConfig } from "../wranglerAction";
export declare function getTestConfig({ config, }?: {
config?: Partial<WranglerActionConfig>;
}): WranglerActionConfig;
+8
View File
@@ -0,0 +1,8 @@
import { WranglerActionConfig } from "./wranglerAction";
/**
* A helper function to compare two semver versions. If the second arg is greater than the first arg, it returns true.
*/
export declare function semverCompare(version1: string, version2: string): boolean;
export declare function checkWorkingDirectory(workingDirectory?: string): string;
export declare function info(config: WranglerActionConfig, message: string, bypass?: boolean): void;
export declare function error(config: WranglerActionConfig, message: string, bypass?: boolean): void;
+1
View File
@@ -0,0 +1 @@
export {};
+54
View File
@@ -0,0 +1,54 @@
import { z } from "zod";
import { PackageManager } from "./packageManagers";
import { info } from "./utils";
export type WranglerActionConfig = z.infer<typeof wranglerActionConfig>;
export declare const wranglerActionConfig: z.ZodObject<{
WRANGLER_VERSION: z.ZodString;
didUserProvideWranglerVersion: z.ZodBoolean;
secrets: z.ZodArray<z.ZodString, "many">;
workingDirectory: z.ZodString;
CLOUDFLARE_API_TOKEN: z.ZodString;
CLOUDFLARE_ACCOUNT_ID: z.ZodString;
ENVIRONMENT: z.ZodString;
VARS: z.ZodArray<z.ZodString, "many">;
COMMANDS: z.ZodArray<z.ZodString, "many">;
QUIET_MODE: z.ZodBoolean;
PACKAGE_MANAGER: z.ZodString;
WRANGLER_OUTPUT_DIR: z.ZodString;
GITHUB_TOKEN: z.ZodString;
}, "strip", z.ZodTypeAny, {
WRANGLER_VERSION: string;
didUserProvideWranglerVersion: boolean;
secrets: string[];
workingDirectory: string;
CLOUDFLARE_API_TOKEN: string;
CLOUDFLARE_ACCOUNT_ID: string;
ENVIRONMENT: string;
VARS: string[];
COMMANDS: string[];
QUIET_MODE: boolean;
PACKAGE_MANAGER: string;
WRANGLER_OUTPUT_DIR: string;
GITHUB_TOKEN: string;
}, {
WRANGLER_VERSION: string;
didUserProvideWranglerVersion: boolean;
secrets: string[];
workingDirectory: string;
CLOUDFLARE_API_TOKEN: string;
CLOUDFLARE_ACCOUNT_ID: string;
ENVIRONMENT: string;
VARS: string[];
COMMANDS: string[];
QUIET_MODE: boolean;
PACKAGE_MANAGER: string;
WRANGLER_OUTPUT_DIR: string;
GITHUB_TOKEN: string;
}>;
declare function main(config: WranglerActionConfig, packageManager: PackageManager): Promise<void>;
declare function installWrangler(config: WranglerActionConfig, packageManager: PackageManager): Promise<void>;
declare function authenticationSetup(config: WranglerActionConfig): void;
declare function execCommands(config: WranglerActionConfig, packageManager: PackageManager, commands: string[], cmdType: string): Promise<void>;
declare function uploadSecrets(config: WranglerActionConfig, packageManager: PackageManager): Promise<void>;
declare function wranglerCommands(config: WranglerActionConfig, packageManager: PackageManager): Promise<void>;
export { authenticationSetup, execCommands, info, installWrangler, main, uploadSecrets, wranglerCommands, };
+1
View File
@@ -0,0 +1 @@
export {};
+103
View File
@@ -0,0 +1,103 @@
import { z } from "zod";
declare const OutputEntryPagesDeployment: z.ZodObject<z.objectUtil.extendShape<{
version: z.ZodLiteral<1>;
type: z.ZodString;
}, {
type: z.ZodLiteral<"pages-deploy-detailed">;
pages_project: z.ZodNullable<z.ZodString>;
deployment_id: z.ZodNullable<z.ZodString>;
url: z.ZodOptional<z.ZodString>;
alias: z.ZodOptional<z.ZodString>;
environment: z.ZodEnum<["production", "preview"]>;
production_branch: z.ZodOptional<z.ZodString>;
stages: z.ZodOptional<z.ZodArray<z.ZodObject<{
name: z.ZodEnum<["queued", "initialize", "clone_repo", "build", "deploy"]>;
status: z.ZodEnum<["idle", "active", "canceled", "success", "failure", "skipped"]>;
started_on: z.ZodNullable<z.ZodString>;
ended_on: z.ZodNullable<z.ZodString>;
}, "strip", z.ZodTypeAny, {
status: "idle" | "active" | "canceled" | "success" | "failure" | "skipped";
name: "deploy" | "queued" | "initialize" | "clone_repo" | "build";
started_on: string | null;
ended_on: string | null;
}, {
status: "idle" | "active" | "canceled" | "success" | "failure" | "skipped";
name: "deploy" | "queued" | "initialize" | "clone_repo" | "build";
started_on: string | null;
ended_on: string | null;
}>, "many">>;
deployment_trigger: z.ZodOptional<z.ZodObject<{
metadata: z.ZodObject<{
/** Commit hash of the deployment trigger metadata for the pages project */
commit_hash: z.ZodString;
}, "strip", z.ZodTypeAny, {
commit_hash: string;
}, {
commit_hash: string;
}>;
}, "strip", z.ZodTypeAny, {
metadata: {
commit_hash: string;
};
}, {
metadata: {
commit_hash: string;
};
}>>;
}>, "strip", z.ZodTypeAny, {
type: "pages-deploy-detailed";
environment: "production" | "preview";
version: 1;
pages_project: string | null;
deployment_id: string | null;
url?: string | undefined;
alias?: string | undefined;
production_branch?: string | undefined;
stages?: {
status: "idle" | "active" | "canceled" | "success" | "failure" | "skipped";
name: "deploy" | "queued" | "initialize" | "clone_repo" | "build";
started_on: string | null;
ended_on: string | null;
}[] | undefined;
deployment_trigger?: {
metadata: {
commit_hash: string;
};
} | undefined;
}, {
type: "pages-deploy-detailed";
environment: "production" | "preview";
version: 1;
pages_project: string | null;
deployment_id: string | null;
url?: string | undefined;
alias?: string | undefined;
production_branch?: string | undefined;
stages?: {
status: "idle" | "active" | "canceled" | "success" | "failure" | "skipped";
name: "deploy" | "queued" | "initialize" | "clone_repo" | "build";
started_on: string | null;
ended_on: string | null;
}[] | undefined;
deployment_trigger?: {
metadata: {
commit_hash: string;
};
} | undefined;
}>;
export type OutputEntryPagesDeployment = z.infer<typeof OutputEntryPagesDeployment>;
/**
* Parses file names in a directory to find wrangler artifact files
*
* @param artifactDirectory
* @returns All artifact files from the directory
*/
export declare function getWranglerArtifacts(artifactDirectory: string): Promise<string[]>;
/**
* Searches for detailed wrangler output from a pages deploy
*
* @param artifactDirectory
* @returns The first pages-output-detailed found within a wrangler artifact directory
*/
export declare function getDetailedPagesDeployOutput(artifactDirectory: string): Promise<OutputEntryPagesDeployment | null>;
export {};
+1
View File
@@ -0,0 +1 @@
export {};
+977 -1554
View File
File diff suppressed because it is too large Load Diff
+3 -7
View File
@@ -1,6 +1,6 @@
{
"name": "wrangler-action",
"version": "4.0.0",
"version": "3.12.1",
"description": "GitHub Action to use [Wrangler](https://developers.cloudflare.com/workers/cli-wrangler/).",
"author": "wrangler@cloudflare.com",
"license": "MIT OR Apache-2.0",
@@ -36,21 +36,17 @@
},
"devDependencies": {
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.12",
"@changesets/cli": "^2.27.9",
"@cloudflare/workers-types": "^4.20241022.0",
"@types/mock-fs": "^4.13.4",
"@types/node": "^22.9.0",
"@types/semver": "^7.5.8",
"@vercel/ncc": "^0.38.2",
"better-result": "^2.9.2",
"mock-fs": "^5.4.1",
"msw": "^2.6.4",
"prettier": "^3.3.3",
"semver": "^7.6.3",
"typescript": "^5.6.3",
"vitest": "^3.2.4"
},
"overrides": {
"undici": "^6.23.1"
"vitest": "^2.1.4"
}
}
-180
View File
@@ -1,180 +0,0 @@
import { setOutput } from "@actions/core";
import { info, WranglerActionConfig } from "./wranglerAction";
import {
getOutputEntry,
OutputEntryDeployment,
OutputEntryPagesDeployment,
OutputEntryVersionUpload,
} from "./wranglerArtifactManager";
import { createGitHubDeploymentAndJobSummary } from "./service/github";
// fallback to trying to extract the deployment-url and pages-deployment-alias-url from stdout for wranglerVersion < 3.81.0
function extractDeploymentUrlsFromStdout(stdOut: string): {
deploymentUrl?: string;
aliasUrl?: string;
} {
let deploymentUrl = "";
let aliasUrl = "";
// Try to extract the deployment URL
const deploymentUrlMatch = stdOut.match(/https?:\/\/[a-zA-Z0-9-./]+/);
if (deploymentUrlMatch && deploymentUrlMatch[0]) {
deploymentUrl = deploymentUrlMatch[0].trim();
}
// And also try to extract the alias URL (since wrangler@3.78.0)
const aliasUrlMatch = stdOut.match(/alias URL: (https?:\/\/[a-zA-Z0-9-./]+)/);
if (aliasUrlMatch && aliasUrlMatch[1]) {
aliasUrl = aliasUrlMatch[1].trim();
}
return { deploymentUrl, aliasUrl };
}
async function handlePagesDeployOutputEntry(
config: WranglerActionConfig,
pagesDeployOutputEntry: OutputEntryPagesDeployment,
) {
setOutput("deployment-url", pagesDeployOutputEntry.url);
// DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change
setOutput("deployment-alias-url", pagesDeployOutputEntry.alias);
setOutput("pages-deployment-alias-url", pagesDeployOutputEntry.alias);
setOutput("pages-deployment-id", pagesDeployOutputEntry.deployment_id);
setOutput("pages-environment", pagesDeployOutputEntry.environment);
// Create github deployment, if GITHUB_TOKEN is present in config
await createGitHubDeploymentAndJobSummary(config, pagesDeployOutputEntry);
}
/**
* If no wrangler output file found, fallback to extracting deployment-url from stdout.
* @deprecated Use {@link handlePagesDeployOutputEntry} instead.
*/
function handlePagesDeployCommand(
config: WranglerActionConfig,
stdOut: string,
) {
info(
config,
"Unable to find a WRANGLER_OUTPUT_DIR, environment and id fields will be unavailable for output. Have you updated wrangler to version >=3.81.0?",
);
// DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change
const { deploymentUrl, aliasUrl } = extractDeploymentUrlsFromStdout(stdOut);
setOutput("deployment-url", deploymentUrl);
// DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change
setOutput("deployment-alias-url", aliasUrl);
setOutput("pages-deployment-alias-url", aliasUrl);
}
function handleWranglerDeployOutputEntry(
config: WranglerActionConfig,
wranglerDeployOutputEntry: OutputEntryDeployment,
) {
// If no deployment urls found in wrangler output file, log that we couldn't find any urls and return.
if (
!wranglerDeployOutputEntry.targets ||
wranglerDeployOutputEntry.targets.length === 0
) {
info(config, "No deployment-url found in wrangler deploy output file");
return;
}
// If more than 1 deployment url found, log that we're going to set deployment-url to the first match.
// In a future wrangler-action version we should consider how we're going to output multiple deployment-urls
if (wranglerDeployOutputEntry.targets.length > 1) {
info(
config,
"Multiple deployment urls found in wrangler deploy output file, deployment-url will be set to the first url",
);
}
setOutput("deployment-url", wranglerDeployOutputEntry.targets[0]);
}
/**
* If no wrangler output file found, fallback to extracting deployment-url from stdout.
* @deprecated Use {@link handleWranglerDeployOutputEntry} instead.
*/
function handleWranglerDeployCommand(
config: WranglerActionConfig,
stdOut: string,
) {
info(
config,
"Unable to find a WRANGLER_OUTPUT_DIR, deployment-url may have an unreliable output. Have you updated wrangler to version >=3.88.0?",
);
const { deploymentUrl } = extractDeploymentUrlsFromStdout(stdOut);
setOutput("deployment-url", deploymentUrl);
}
function handleVersionsUploadOutputEntry(
versionsOutputEntry: OutputEntryVersionUpload,
) {
setOutput("deployment-url", versionsOutputEntry.preview_url);
}
/**
* If no wrangler output file found, log a message stating deployment-url will be unavailable for output.
* @deprecated Use {@link handleVersionsOutputEntry} instead.
*/
function handleVersionsOutputCommand(config: WranglerActionConfig) {
info(
config,
"Unable to find a WRANGLER_OUTPUT_DIR, deployment-url will be unavailable for output. Have you updated wrangler to version >=3.88.0?",
);
}
function handleDeprectatedStdoutParsing(
config: WranglerActionConfig,
command: string,
stdOut: string,
) {
// Check if this command is a pages deployment
if (
command.startsWith("pages deploy") ||
command.startsWith("pages publish")
) {
handlePagesDeployCommand(config, stdOut);
return;
}
// Check if this command is a workers deployment
if (command.startsWith("deploy") || command.startsWith("publish")) {
handleWranglerDeployCommand(config, stdOut);
return;
}
// Check if this command is a versions deployment
if (command.startsWith("versions upload")) {
handleVersionsOutputCommand(config);
return;
}
}
export async function handleCommandOutputParsing(
config: WranglerActionConfig,
command: string,
stdOut: string,
) {
// get first OutputEntry found within wrangler artifact output directory
const outputEntry = await getOutputEntry(config.WRANGLER_OUTPUT_DIR);
if (outputEntry === null) {
// if no outputEntry found, fallback to deprecated stdOut parsing
handleDeprectatedStdoutParsing(config, command, stdOut);
return;
}
switch (outputEntry.type) {
case "pages-deploy-detailed":
await handlePagesDeployOutputEntry(config, outputEntry);
break;
case "deploy":
handleWranglerDeployOutputEntry(config, outputEntry);
break;
case "version-upload":
handleVersionsUploadOutputEntry(outputEntry);
break;
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ import { getPackageManager } from "./packageManagers";
import { checkWorkingDirectory } from "./utils";
import { main, WranglerActionConfig } from "./wranglerAction";
const DEFAULT_WRANGLER_VERSION = "4";
const DEFAULT_WRANGLER_VERSION = "3.81.0";
/**
* A configuration object that contains all the inputs & immutable state for the action.
+23 -31
View File
@@ -1,7 +1,7 @@
import { summary } from "@actions/core";
import { context, getOctokit } from "@actions/github";
import { env } from "process";
import { info, warn } from "../utils";
import { info } from "../utils";
import { OutputEntryPagesDeployment } from "../wranglerArtifactManager";
import { WranglerActionConfig } from "../wranglerAction";
@@ -92,37 +92,29 @@ export async function createGitHubDeploymentAndJobSummary(
config.GITHUB_TOKEN &&
pagesArtifactFields.production_branch &&
pagesArtifactFields.pages_project &&
pagesArtifactFields.deployment_trigger
pagesArtifactFields.deployment_trigger &&
pagesArtifactFields.stages
) {
const octokit = getOctokit(config.GITHUB_TOKEN);
const [createGitHubDeploymentRes, createJobSummaryRes] =
await Promise.allSettled([
createGitHubDeployment({
config,
octokit,
deploymentUrl: pagesArtifactFields.url,
productionBranch: pagesArtifactFields.production_branch,
environment: pagesArtifactFields.environment,
deploymentId: pagesArtifactFields.deployment_id,
projectName: pagesArtifactFields.pages_project,
}),
createJobSummary({
commitHash:
pagesArtifactFields.deployment_trigger.metadata.commit_hash.substring(
0,
8,
),
deploymentUrl: pagesArtifactFields.url,
aliasUrl: pagesArtifactFields.alias,
}),
]);
if (createGitHubDeploymentRes.status === "rejected") {
warn(config, "Creating Github Deployment failed");
}
if (createJobSummaryRes.status === "rejected") {
warn(config, "Creating Github Job summary failed");
}
await Promise.all([
createGitHubDeployment({
config,
octokit,
deploymentUrl: pagesArtifactFields.url,
productionBranch: pagesArtifactFields.production_branch,
environment: pagesArtifactFields.environment,
deploymentId: pagesArtifactFields.deployment_id,
projectName: pagesArtifactFields.pages_project,
}),
createJobSummary({
commitHash:
pagesArtifactFields.deployment_trigger.metadata.commit_hash.substring(
0,
8,
),
deploymentUrl: pagesArtifactFields.url,
aliasUrl: pagesArtifactFields.alias,
}),
]);
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ export function getTestConfig({
} = {}): WranglerActionConfig {
return Object.assign(
{
WRANGLER_VERSION: "4.72.0",
WRANGLER_VERSION: "3.81.0",
didUserProvideWranglerVersion: false,
secrets: [],
workingDirectory: "/src/test/fixtures",
-4
View File
@@ -42,10 +42,6 @@ describe("semverCompare", () => {
["3.1.0", "3.15.0", true],
["3.10.0", "3.1.0", false],
["3.20.0", "3.2.0", false],
["3.1.0", "4.0.0", true],
["2.20.0", "4.72.0", true],
["3.4.0", "4.72.0", true],
["3.60.0", "4.72.0", true],
["3.1.0", "latest", true],
["4.0.0", "latest", true],
])(
+1 -15
View File
@@ -1,11 +1,7 @@
import { existsSync } from "node:fs";
import * as path from "node:path";
import semverGt from "semver/functions/gt";
import {
info as originalInfo,
error as originalError,
warning as originalWarn,
} from "@actions/core";
import { info as originalInfo, error as originalError } from "@actions/core";
import { WranglerActionConfig } from "./wranglerAction";
/**
@@ -36,16 +32,6 @@ export function info(
}
}
export function warn(
config: WranglerActionConfig,
message: string,
bypass?: boolean,
): void {
if (!config.QUIET_MODE || bypass) {
originalWarn(message);
}
}
export function error(
config: WranglerActionConfig,
message: string,
+5 -439
View File
@@ -1,59 +1,9 @@
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { describe, expect, it, vi } from "vitest";
import {
installWrangler,
isExactSemver,
main,
parseWranglerVersion,
uploadSecrets,
} from "./wranglerAction";
import { installWrangler } from "./wranglerAction";
import { getTestConfig } from "./test/test-utils";
describe("parseWranglerVersion", () => {
it("parses version from verbose wrangler output", () => {
expect(
parseWranglerVersion(` ⛅️ wrangler 3.48.0 (update available 3.53.1)`),
).toBe("3.48.0");
});
it("parses version from bare version output", () => {
expect(parseWranglerVersion("4.18.1\n")).toBe("4.18.1");
});
it("returns empty string for unparseable output", () => {
expect(parseWranglerVersion("something unexpected")).toBe("");
});
it("parses prerelease version", () => {
expect(parseWranglerVersion(` ⛅️ wrangler 4.0.0-beta.1`)).toBe(
"4.0.0-beta.1",
);
});
it("parses bare prerelease version", () => {
expect(parseWranglerVersion("4.0.0-rc.0\n")).toBe("4.0.0-rc.0");
});
});
describe("isExactSemver", () => {
it.each([
["4.81.0", true],
["3.48.0", true],
["2.20.0", true],
["4.0.0-beta.1", true],
["4", false],
["4.x", false],
["4.*", false],
["^4.0.0", false],
["~4.0.0", false],
["latest", false],
["", false],
])("isExactSemver(%s) === %s", (input, expected) => {
expect(isExactSemver(input)).toBe(expected);
});
});
describe("installWrangler", () => {
const testPackageManager = {
install: "npm i",
@@ -80,14 +30,10 @@ describe("installWrangler", () => {
};
});
const infoSpy = vi.spyOn(core, "info");
const resolvedVersion = await installWrangler(
testConfig,
testPackageManager,
);
await installWrangler(testConfig, testPackageManager);
expect(infoSpy).toBeCalledWith(
"✅ No wrangler version specified, using pre-installed wrangler version 3.48.0",
);
expect(resolvedVersion).toBe("3.48.0");
});
it("Does nothing if the wrangler version specified is the same as the one installed", async () => {
@@ -105,14 +51,9 @@ describe("installWrangler", () => {
};
});
const infoSpy = vi.spyOn(core, "info");
const resolvedVersion = await installWrangler(
testConfig,
testPackageManager,
);
await installWrangler(testConfig, testPackageManager);
expect(infoSpy).toBeCalledWith("✅ Using Wrangler 3.48.0");
expect(resolvedVersion).toBe("3.48.0");
});
it("Should install wrangler if the version specified is not already available", async () => {
const testConfig = getTestConfig({
config: {
@@ -120,393 +61,18 @@ describe("installWrangler", () => {
didUserProvideWranglerVersion: true,
},
});
let callCount = 0;
vi.spyOn(exec, "getExecOutput").mockImplementation(async () => {
callCount++;
if (callCount === 1) {
// Pre-install check: different version installed
return {
exitCode: 0,
stderr: "",
stdout: ` ⛅️ wrangler 3.20.0 (update available 3.53.1)`,
};
}
// Post-install check: correct version now installed
return {
exitCode: 0,
stderr: "",
stdout: ` ⛅️ wrangler 3.48.0`,
stdout: ` ⛅️ wrangler 3.20.0 (update available 3.53.1)`,
};
});
vi.spyOn(exec, "exec").mockImplementation(async () => {
return 0;
});
const infoSpy = vi.spyOn(core, "info");
const resolvedVersion = await installWrangler(
testConfig,
testPackageManager,
);
await installWrangler(testConfig, testPackageManager);
expect(infoSpy).toBeCalledWith("✅ Wrangler installed");
expect(resolvedVersion).toBe("3.48.0");
});
it("Should install and resolve version when a range like '4' is specified", async () => {
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "4",
didUserProvideWranglerVersion: true,
},
});
let callCount = 0;
vi.spyOn(exec, "getExecOutput").mockImplementation(async () => {
callCount++;
if (callCount === 1) {
// Pre-install check: older version installed
return {
exitCode: 0,
stderr: "",
stdout: ` ⛅️ wrangler 3.90.0`,
};
}
// Post-install check: v4 now installed
return {
exitCode: 0,
stderr: "",
stdout: `4.18.1`,
};
});
vi.spyOn(exec, "exec").mockImplementation(async (cmd, args) => {
if (cmd === "npm i") expect(args).toStrictEqual(["wrangler@4"]);
return 0;
});
const resolvedVersion = await installWrangler(
testConfig,
testPackageManager,
);
expect(resolvedVersion).toBe("4.18.1");
});
it("Should install and resolve version when 'latest' is specified", async () => {
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "latest",
didUserProvideWranglerVersion: true,
},
});
let callCount = 0;
vi.spyOn(exec, "getExecOutput").mockImplementation(async () => {
callCount++;
if (callCount === 1) {
// Pre-install: no wrangler found
throw new Error("command not found");
}
// Post-install check
return {
exitCode: 0,
stderr: "",
stdout: `4.20.0`,
};
});
vi.spyOn(exec, "exec").mockImplementation(async (cmd, args) => {
if (cmd === "npm i") {
expect(args).toStrictEqual(["wrangler@latest"]);
}
return 0;
});
const resolvedVersion = await installWrangler(
testConfig,
testPackageManager,
);
expect(resolvedVersion).toBe("4.20.0");
});
it("Throws if version cannot be resolved after install", async () => {
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "4",
didUserProvideWranglerVersion: true,
},
});
let callCount = 0;
vi.spyOn(exec, "getExecOutput").mockImplementation(async () => {
callCount++;
if (callCount === 1) {
throw new Error("not found");
}
// Post-install: unparseable output
return { exitCode: 0, stderr: "", stdout: "garbage output" };
});
vi.spyOn(exec, "exec").mockResolvedValue(0);
await expect(
installWrangler(testConfig, testPackageManager),
).rejects.toThrowError(
"Failed to determine installed Wrangler version after installing wrangler@4",
);
});
it("Falls back to raw version when post-install resolution fails for exact semver", async () => {
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "3.48.0",
didUserProvideWranglerVersion: true,
},
});
let callCount = 0;
vi.spyOn(exec, "getExecOutput").mockImplementation(async () => {
callCount++;
if (callCount === 1) {
throw new Error("not found");
}
throw new Error("wrangler --version failed");
});
vi.spyOn(exec, "exec").mockResolvedValue(0);
const resolvedVersion = await installWrangler(
testConfig,
testPackageManager,
);
expect(resolvedVersion).toBe("3.48.0");
});
it("Skips reinstall when range is satisfied by pre-installed version", async () => {
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "4",
didUserProvideWranglerVersion: true,
},
});
vi.spyOn(exec, "getExecOutput").mockImplementation(async () => {
return {
exitCode: 0,
stderr: "",
stdout: `4.18.1`,
};
});
const execSpy = vi.spyOn(exec, "exec");
const infoSpy = vi.spyOn(core, "info");
const resolvedVersion = await installWrangler(
testConfig,
testPackageManager,
);
expect(infoSpy).toBeCalledWith("✅ Using Wrangler 4.18.1");
expect(resolvedVersion).toBe("4.18.1");
expect(execSpy).not.toHaveBeenCalled();
});
it("Reinstalls when range is NOT satisfied by pre-installed version", async () => {
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "4",
didUserProvideWranglerVersion: true,
},
});
let callCount = 0;
vi.spyOn(exec, "getExecOutput").mockImplementation(async () => {
callCount++;
if (callCount === 1) {
// Pre-installed is v3, doesn't satisfy "4"
return {
exitCode: 0,
stderr: "",
stdout: ` ⛅️ wrangler 3.90.0`,
};
}
return {
exitCode: 0,
stderr: "",
stdout: `4.18.1`,
};
});
vi.spyOn(exec, "exec").mockResolvedValue(0);
const resolvedVersion = await installWrangler(
testConfig,
testPackageManager,
);
expect(resolvedVersion).toBe("4.18.1");
});
});
describe("uploadSecrets", () => {
const testPackageManager = {
install: "npm i",
exec: "npx",
execNoInstall: "npx --no-install",
};
it("WRANGLER_VERSION < 3.4.0 uses wrangler secret put", async () => {
vi.stubEnv("FAKE_SECRET", "FAKE_VALUE");
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "3.3.0",
didUserProvideWranglerVersion: true,
secrets: ["FAKE_SECRET"],
},
});
vi.spyOn(exec, "exec").mockImplementation(async (cmd, args) => {
expect(cmd).toBe("npx");
expect(args).toStrictEqual([
"wrangler",
"secret",
"put",
"FAKE_SECRET",
"--env",
"dev",
]);
return 0;
});
const startGroup = vi.spyOn(core, "startGroup");
const endGroup = vi.spyOn(core, "endGroup");
await uploadSecrets(testConfig, testPackageManager);
expect(startGroup).toBeCalledWith("🔑 Uploading secrets...");
expect(endGroup).toHaveBeenCalledOnce();
});
it("WRANGLER_VERSION < 3.60.0 uses wrangler secret:bulk", async () => {
vi.stubEnv("FAKE_SECRET", "FAKE_VALUE");
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "3.59.0",
didUserProvideWranglerVersion: true,
secrets: ["FAKE_SECRET"],
},
});
vi.spyOn(exec, "exec").mockImplementation(async (cmd, args) => {
expect(cmd).toBe("npx");
expect(args).toStrictEqual(["wrangler", "secret:bulk", "--env", "dev"]);
return 0;
});
const startGroup = vi.spyOn(core, "startGroup");
const endGroup = vi.spyOn(core, "endGroup");
await uploadSecrets(testConfig, testPackageManager);
expect(startGroup).toBeCalledWith("🔑 Uploading secrets...");
expect(endGroup).toHaveBeenCalledOnce();
});
it("WRANGLER_VERSION 3.61.0 uses wrangler secret bulk", async () => {
vi.stubEnv("FAKE_SECRET", "FAKE_VALUE");
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "3.61.0",
didUserProvideWranglerVersion: true,
secrets: ["FAKE_SECRET"],
},
});
vi.spyOn(exec, "exec").mockImplementation(async (cmd, args) => {
expect(cmd).toBe("npx");
expect(args).toStrictEqual([
"wrangler",
"secret",
"bulk",
"--env",
"dev",
]);
return 0;
});
const startGroup = vi.spyOn(core, "startGroup");
const endGroup = vi.spyOn(core, "endGroup");
await uploadSecrets(testConfig, testPackageManager);
expect(startGroup).toBeCalledWith("🔑 Uploading secrets...");
expect(endGroup).toHaveBeenCalledOnce();
});
it("WRANGLER_VERSION 4.x uses wrangler secret bulk", async () => {
vi.stubEnv("FAKE_SECRET", "FAKE_VALUE");
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "4.72.0",
didUserProvideWranglerVersion: true,
secrets: ["FAKE_SECRET"],
},
});
vi.spyOn(exec, "exec").mockImplementation(async (cmd, args) => {
expect(cmd).toBe("npx");
expect(args).toStrictEqual([
"wrangler",
"secret",
"bulk",
"--env",
"dev",
]);
return 0;
});
const startGroup = vi.spyOn(core, "startGroup");
const endGroup = vi.spyOn(core, "endGroup");
await uploadSecrets(testConfig, testPackageManager);
expect(startGroup).toBeCalledWith("🔑 Uploading secrets...");
expect(endGroup).toHaveBeenCalledOnce();
});
});
describe("main", () => {
const testPackageManager = {
install: "npm i",
exec: "npx",
execNoInstall: "npx --no-install",
};
it("Completes with wranglerVersion '4' and secrets without Invalid Version error", async () => {
vi.stubEnv("MY_SECRET", "secret_value");
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "4",
didUserProvideWranglerVersion: true,
secrets: ["MY_SECRET"],
COMMANDS: ["deploy"],
},
});
vi.spyOn(core, "getMultilineInput").mockReturnValue([]);
let callCount = 0;
vi.spyOn(exec, "getExecOutput").mockImplementation(async () => {
callCount++;
if (callCount === 1) {
throw new Error("command not found");
}
return { exitCode: 0, stderr: "", stdout: "4.18.1" };
});
vi.spyOn(exec, "exec").mockResolvedValue(0);
const setFailedSpy = vi.spyOn(core, "setFailed");
await main(testConfig, testPackageManager);
expect(setFailedSpy).not.toHaveBeenCalled();
});
it("Completes with wranglerVersion 'latest' and secrets without Invalid Version error", async () => {
vi.stubEnv("MY_SECRET", "secret_value");
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "latest",
didUserProvideWranglerVersion: true,
secrets: ["MY_SECRET"],
COMMANDS: ["deploy"],
},
});
vi.spyOn(core, "getMultilineInput").mockReturnValue([]);
let callCount = 0;
vi.spyOn(exec, "getExecOutput").mockImplementation(async () => {
callCount++;
if (callCount === 1) {
throw new Error("command not found");
}
return { exitCode: 0, stderr: "", stdout: "4.20.0" };
});
vi.spyOn(exec, "exec").mockResolvedValue(0);
const setFailedSpy = vi.spyOn(core, "setFailed");
await main(testConfig, testPackageManager);
expect(setFailedSpy).not.toHaveBeenCalled();
});
});
+106 -89
View File
@@ -7,14 +7,13 @@ import {
setOutput,
} from "@actions/core";
import { getExecOutput } from "@actions/exec";
import semverSatisfies from "semver/functions/satisfies";
import semverValid from "semver/functions/valid";
import semverEq from "semver/functions/eq";
import { z } from "zod";
import { exec, execShell } from "./exec";
import { PackageManager } from "./packageManagers";
import { error, info, semverCompare } from "./utils";
import { handleCommandOutputParsing } from "./commandOutputParsing";
import semverLt from "semver/functions/lt";
import { getDetailedPagesDeployOutput } from "./wranglerArtifactManager";
import { createGitHubDeploymentAndJobSummary } from "./service/github";
export type WranglerActionConfig = z.infer<typeof wranglerActionConfig>;
export const wranglerActionConfig = z.object({
@@ -52,60 +51,32 @@ async function main(
try {
wranglerActionConfig.parse(config);
authenticationSetup(config);
const resolvedVersion = await installWrangler(config, packageManager);
const resolvedConfig = { ...config, WRANGLER_VERSION: resolvedVersion };
await installWrangler(config, packageManager);
await execCommands(
resolvedConfig,
config,
packageManager,
getMultilineInput("preCommands"),
"pre",
);
await uploadSecrets(resolvedConfig, packageManager);
await wranglerCommands(resolvedConfig, packageManager);
await uploadSecrets(config, packageManager);
await wranglerCommands(config, packageManager);
await execCommands(
resolvedConfig,
config,
packageManager,
getMultilineInput("postCommands"),
"post",
);
info(resolvedConfig, "🏁 Wrangler Action completed", true);
info(config, "🏁 Wrangler Action completed", true);
} catch (err: unknown) {
err instanceof Error && error(config, err.message);
setFailed("🚨 Action failed");
}
}
function parseWranglerVersion(stdout: string): string {
const match =
stdout.match(/wrangler (\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)/) ??
stdout.match(/^(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)/m);
return match ? match[1] : "";
}
function isExactSemver(version: string): boolean {
return semverValid(version) !== null;
}
async function resolveInstalledVersion(
config: WranglerActionConfig,
packageManager: PackageManager,
): Promise<string> {
const { stdout } = await getExecOutput(
packageManager.execNoInstall,
["wrangler", "--version"],
{
cwd: config["workingDirectory"],
silent: config.QUIET_MODE,
},
);
return parseWranglerVersion(stdout);
}
async function installWrangler(
config: WranglerActionConfig,
packageManager: PackageManager,
): Promise<string> {
) {
if (config["WRANGLER_VERSION"].startsWith("1")) {
throw new Error(
`Wrangler v1 is no longer supported by this action. Please use major version 2 or greater`,
@@ -114,25 +85,33 @@ async function installWrangler(
startGroup(config, "🔍 Checking for existing Wrangler installation");
let installedVersion = "";
let versionSatisfied = false;
let installedVersionSatisfiesRequirement = false;
try {
installedVersion = await resolveInstalledVersion(config, packageManager);
const { stdout } = await getExecOutput(
// We want to simply invoke wrangler to check if it's installed, but don't want to auto-install it at this stage
packageManager.execNoInstall,
["wrangler", "--version"],
{
cwd: config["workingDirectory"],
silent: config.QUIET_MODE,
},
);
if (config.didUserProvideWranglerVersion && installedVersion) {
if (isExactSemver(config["WRANGLER_VERSION"])) {
versionSatisfied = installedVersion === config["WRANGLER_VERSION"];
} else {
// semverSatisfies handles ranges like "4", "^4.0.0", "4.x".
// Returns false for dist-tags like "latest", falling through to reinstall.
try {
versionSatisfied = semverSatisfies(
installedVersion,
config["WRANGLER_VERSION"],
);
} catch {
versionSatisfied = false;
}
}
// There are two possible outputs from `wrangler --version`:
// ` ⛅️ wrangler 3.48.0 (update available 3.53.1)`
// and
// `3.48.0`
const versionMatch =
stdout.match(/wrangler (\d+\.\d+\.\d+)/) ??
stdout.match(/^(\d+\.\d+\.\d+)/m);
if (versionMatch) {
installedVersion = versionMatch[1];
}
if (config.didUserProvideWranglerVersion) {
installedVersionSatisfiesRequirement = semverEq(
installedVersion,
config["WRANGLER_VERSION"],
);
}
if (!config.didUserProvideWranglerVersion && installedVersion) {
info(
@@ -141,12 +120,15 @@ async function installWrangler(
true,
);
endGroup(config);
return installedVersion;
return;
}
if (config.didUserProvideWranglerVersion && versionSatisfied) {
if (
config.didUserProvideWranglerVersion &&
installedVersionSatisfiesRequirement
) {
info(config, `✅ Using Wrangler ${installedVersion}`, true);
endGroup(config);
return installedVersion;
return;
}
info(
config,
@@ -179,27 +161,6 @@ async function installWrangler(
} finally {
endGroup(config);
}
let resolvedVersion = "";
try {
resolvedVersion = await resolveInstalledVersion(config, packageManager);
} catch (err) {
debug(`Error resolving installed Wrangler version: ${err}`);
}
if (resolvedVersion) {
return resolvedVersion;
}
// Fall back to the raw version string if it's already valid semver.
// This preserves pre-existing behavior for exact version inputs.
if (isExactSemver(config["WRANGLER_VERSION"])) {
return config["WRANGLER_VERSION"];
}
throw new Error(
`Failed to determine installed Wrangler version after installing wrangler@${config["WRANGLER_VERSION"]}`,
);
}
function authenticationSetup(config: WranglerActionConfig) {
@@ -305,11 +266,7 @@ async function uploadSecrets(
);
}
let args = ["wrangler", "secret", "bulk"];
// if we're on a WRANGLER_VERSION prior to 3.60.0 use wrangler secret:bulk
if (semverLt(config["WRANGLER_VERSION"], "3.60.0")) {
args = ["wrangler", "secret:bulk"];
}
const args = ["wrangler", "secret:bulk"];
if (environment) {
args.push("--env", environment);
@@ -337,6 +294,29 @@ async function uploadSecrets(
}
}
// fallback to trying to extract the deployment-url and pages-deployment-alias-url from stdout for wranglerVersion < 3.81.0
function extractDeploymentUrlsFromStdout(stdOut: string): {
deploymentUrl?: string;
aliasUrl?: string;
} {
let deploymentUrl = "";
let aliasUrl = "";
// Try to extract the deployment URL
const deploymentUrlMatch = stdOut.match(/https?:\/\/[a-zA-Z0-9-./]+/);
if (deploymentUrlMatch && deploymentUrlMatch[0]) {
deploymentUrl = deploymentUrlMatch[0].trim();
}
// And also try to extract the alias URL (since wrangler@3.78.0)
const aliasUrlMatch = stdOut.match(/alias URL: (https?:\/\/[a-zA-Z0-9-./]+)/);
if (aliasUrlMatch && aliasUrlMatch[1]) {
aliasUrl = aliasUrlMatch[1].trim();
}
return { deploymentUrl, aliasUrl };
}
async function wranglerCommands(
config: WranglerActionConfig,
packageManager: PackageManager,
@@ -399,8 +379,47 @@ async function wranglerCommands(
setOutput("command-output", stdOut);
setOutput("command-stderr", stdErr);
// Handles setting github action outputs and creating github deployment and job summary
await handleCommandOutputParsing(config, command, stdOut);
// Check if this command is a workers deployment
if (command.startsWith("deploy") || command.startsWith("publish")) {
const { deploymentUrl } = extractDeploymentUrlsFromStdout(stdOut);
setOutput("deployment-url", deploymentUrl);
}
// Check if this command is a pages deployment
if (
command.startsWith("pages publish") ||
command.startsWith("pages deploy")
) {
const pagesArtifactFields = await getDetailedPagesDeployOutput(
config.WRANGLER_OUTPUT_DIR,
);
if (pagesArtifactFields) {
setOutput("deployment-url", pagesArtifactFields.url);
// DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change
setOutput("deployment-alias-url", pagesArtifactFields.alias);
setOutput("pages-deployment-alias-url", pagesArtifactFields.alias);
setOutput("pages-deployment-id", pagesArtifactFields.deployment_id);
setOutput("pages-environment", pagesArtifactFields.environment);
// Create github deployment, if GITHUB_TOKEN is present in config
await createGitHubDeploymentAndJobSummary(
config,
pagesArtifactFields,
);
} else {
info(
config,
"Unable to find a WRANGLER_OUTPUT_DIR, environment and id fields will be unavailable for output. Have you updated wrangler to version >=3.81.0?",
);
// DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change
const { deploymentUrl, aliasUrl } =
extractDeploymentUrlsFromStdout(stdOut);
setOutput("deployment-url", deploymentUrl);
// DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change
setOutput("deployment-alias-url", aliasUrl);
setOutput("pages-deployment-alias-url", aliasUrl);
}
}
}
} finally {
endGroup(config);
@@ -412,9 +431,7 @@ export {
execCommands,
info,
installWrangler,
isExactSemver,
main,
parseWranglerVersion,
uploadSecrets,
wranglerCommands,
};
+31 -129
View File
@@ -1,7 +1,7 @@
import mockfs from "mock-fs";
import { afterEach, describe, expect, it } from "vitest";
import {
getOutputEntry,
getDetailedPagesDeployOutput,
getWranglerArtifacts,
} from "./wranglerArtifactManager";
@@ -36,148 +36,50 @@ describe("wranglerArtifactsManager", () => {
});
});
describe("getOutputEntry()", async () => {
describe("OutputEntryPagesDeployment", async () => {
it("Returns only detailed pages deploy output from wrangler artifacts", async () => {
describe("getDetailedPagesDeployOutput()", async () => {
it("Returns only detailed pages deploy output from wrangler artifacts", async () => {
mockfs({
testOutputDir: {
"wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": `
{"version": 1, "type":"wrangler-session", "wrangler_version":"3.81.0", "command_line_args":["what's up"], "log_file_path": "/here"}
{"version": 1, "type":"pages-deploy-detailed", "pages_project": "project", "environment":"production", "alias":"test.com", "deployment_id": "123", "url":"url.com"}`,
"not-wrangler-output.json": "test",
},
});
const artifacts = await getDetailedPagesDeployOutput("./testOutputDir");
expect(artifacts).toEqual({
version: 1,
pages_project: "project",
type: "pages-deploy-detailed",
url: "url.com",
environment: "production",
deployment_id: "123",
alias: "test.com",
});
}),
it("Skips artifact entries that are not parseable", async () => {
mockfs({
testOutputDir: {
"wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": `
{"version": 1, "type":"wrangler-session", "wrangler_version":"3.81.0", "command_line_args":["what's up"], "log_file_path": "/here"}
{"version": 1, "type":"pages-deploy-detailed", "pages_project": "project", "environment":"production", "alias":"test.com", "deployment_id": "123", "url":"url.com"}`,
this line is invalid json.
{"version": 1, "type":"pages-deploy-detailed", "pages_project": "project", "environment":"production", "alias":"test.com", "deployment_id": "123", "url":"url.com"}`,
"not-wrangler-output.json": "test",
},
});
const artifact = await getOutputEntry("./testOutputDir");
if (artifact?.type !== "pages-deploy-detailed") {
throw new Error(`Unexpected type ${artifact?.type}`);
}
const artifacts = await getDetailedPagesDeployOutput("./testOutputDir");
expect(artifact).toEqual({
expect(artifacts).toEqual({
version: 1,
pages_project: "project",
type: "pages-deploy-detailed",
pages_project: "project",
url: "url.com",
environment: "production",
deployment_id: "123",
alias: "test.com",
});
}),
it("Skips artifact entries that are not parseable", async () => {
mockfs({
testOutputDir: {
"wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": `
this line is invalid json.
{"version": 1, "type":"pages-deploy-detailed", "pages_project": "project", "environment":"production", "alias":"test.com", "deployment_id": "123", "url":"url.com"}`,
"not-wrangler-output.json": "test",
},
});
const artifact = await getOutputEntry("./testOutputDir");
if (artifact?.type !== "pages-deploy-detailed") {
throw new Error(`Unexpected type ${artifact?.type}`);
}
expect(artifact).toEqual({
version: 1,
type: "pages-deploy-detailed",
pages_project: "project",
url: "url.com",
environment: "production",
deployment_id: "123",
alias: "test.com",
});
});
});
describe("OutputEntryDeployment", async () => {
it("Returns only wrangler deploy output from wrangler artifacts", async () => {
mockfs({
testOutputDir: {
"wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": `
{"version": 1, "type":"wrangler-session", "wrangler_version":"3.81.0", "command_line_args":["what's up"], "log_file_path": "/here"}
{"version": 1, "type":"deploy", "targets": ["https://example.com"]}`,
"not-wrangler-output.json": "test",
},
});
const artifact = await getOutputEntry("./testOutputDir");
if (artifact?.type !== "deploy") {
throw new Error(`Unexpected type ${artifact?.type}`);
}
expect(artifact).toEqual({
version: 1,
type: "deploy",
targets: ["https://example.com"],
});
}),
it("Skips artifact entries that are not parseable", async () => {
mockfs({
testOutputDir: {
"wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": `
this line is invalid json.
{"version": 1, "type":"deploy", "targets": ["https://example.com"]}`,
"not-wrangler-output.json": "test",
},
});
const artifact = await getOutputEntry("./testOutputDir");
if (artifact?.type !== "deploy") {
throw new Error(`Unexpected type ${artifact?.type}`);
}
expect(artifact).toEqual({
version: 1,
type: "deploy",
targets: ["https://example.com"],
});
});
});
describe("OutputEntryVersionUpload", async () => {
it("Returns only version upload output from wrangler artifacts", async () => {
mockfs({
testOutputDir: {
"wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": `
{"version": 1, "type":"wrangler-session", "wrangler_version":"3.81.0", "command_line_args":["what's up"], "log_file_path": "/here"}
{"version": 1, "type":"version-upload", "preview_url": "https://example.com"}`,
"not-wrangler-output.json": "test",
},
});
const artifact = await getOutputEntry("./testOutputDir");
if (artifact?.type !== "version-upload") {
throw new Error(`Unexpected type ${artifact?.type}`);
}
expect(artifact).toEqual({
version: 1,
type: "version-upload",
preview_url: "https://example.com",
});
}),
it("Skips artifact entries that are not parseable", async () => {
mockfs({
testOutputDir: {
"wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": `
this line is invalid json.
{"version": 1, "type":"version-upload", "preview_url": "https://example.com"}`,
"not-wrangler-output.json": "test",
},
});
const artifact = await getOutputEntry("./testOutputDir");
if (artifact?.type !== "version-upload") {
throw new Error(`Unexpected type ${artifact?.type}`);
}
expect(artifact).toEqual({
version: 1,
type: "version-upload",
preview_url: "https://example.com",
});
});
});
});
});
});
+48 -47
View File
@@ -6,9 +6,6 @@ const OutputEntryBase = z.object({
type: z.string(),
});
export type OutputEntryPagesDeployment = z.infer<
typeof OutputEntryPagesDeployment
>;
const OutputEntryPagesDeployment = OutputEntryBase.merge(
z.object({
type: z.literal("pages-deploy-detailed"),
@@ -17,9 +14,33 @@ const OutputEntryPagesDeployment = OutputEntryBase.merge(
url: z.string().optional(),
alias: z.string().optional(),
environment: z.enum(["production", "preview"]),
// optional, added in wrangler@3.89.0
// optional, added in wrangler@TBD
production_branch: z.string().optional(),
// optional, added in wrangler@3.89.0
// optional, added in wrangler@TBD
stages: z
.array(
z.object({
name: z.enum([
"queued",
"initialize",
"clone_repo",
"build",
"deploy",
]),
status: z.enum([
"idle",
"active",
"canceled",
"success",
"failure",
"skipped",
]),
started_on: z.string().nullable(),
ended_on: z.string().nullable(),
}),
)
.optional(),
// optional, added in wrangler@TBD
deployment_trigger: z
.object({
metadata: z.object({
@@ -31,31 +52,9 @@ const OutputEntryPagesDeployment = OutputEntryBase.merge(
}),
);
export type OutputEntryDeployment = z.infer<typeof OutputEntryDeployment>;
const OutputEntryDeployment = OutputEntryBase.merge(
z.object({
type: z.literal("deploy"),
/** A list of URLs that represent the HTTP triggers associated with this deployment */
/** basically, for wrangler-action purposes this is the deployment urls */
targets: z.array(z.string()).optional(),
}),
);
export type OutputEntryVersionUpload = z.infer<typeof OutputEntryVersionUpload>;
const OutputEntryVersionUpload = OutputEntryBase.merge(
z.object({
type: z.literal("version-upload"),
/** The preview URL associated with this version upload */
preview_url: z.string().optional(),
}),
);
export type SupportedOutputEntry = z.infer<typeof SupportedOutputEntry>;
const SupportedOutputEntry = z.discriminatedUnion("type", [
OutputEntryPagesDeployment,
OutputEntryDeployment,
OutputEntryVersionUpload,
]);
export type OutputEntryPagesDeployment = z.infer<
typeof OutputEntryPagesDeployment
>;
/**
* Parses file names in a directory to find wrangler artifact files
@@ -90,32 +89,34 @@ export async function getWranglerArtifacts(
}
/**
* Searches for a supported wrangler OutputEntry
* Searches for detailed wrangler output from a pages deploy
*
* @param artifactDirectory
* @returns The first SupportedOutputEntry found within a wrangler artifact directory
* @returns The first pages-output-detailed found within a wrangler artifact directory
*/
export async function getOutputEntry(
export async function getDetailedPagesDeployOutput(
artifactDirectory: string,
): Promise<SupportedOutputEntry | null> {
): Promise<OutputEntryPagesDeployment | null> {
const artifactFilePaths = await getWranglerArtifacts(artifactDirectory);
for (const filePath of artifactFilePaths) {
const file = await open(filePath, "r");
try {
for await (const line of file.readLines()) {
try {
// Attempt to parse and validate the JSON line against the union schema.
// Assume, in the context of the action, the first OutputEntry seen will suffice
return SupportedOutputEntry.parse(JSON.parse(line));
} catch {
// Skip lines that are invalid JSON or don't match any schema.
continue;
for (let i = 0; i < artifactFilePaths.length; i++) {
const file = await open(artifactFilePaths[i], "r");
for await (const line of file.readLines()) {
try {
const output = JSON.parse(line);
const parsedOutput = OutputEntryPagesDeployment.parse(output);
if (parsedOutput.type === "pages-deploy-detailed") {
// Assume, in the context of the action, the first detailed deploy instance seen will suffice
return parsedOutput;
}
} catch (err) {
// If the line can't be parsed, skip it
continue;
}
} finally {
await file.close();
}
await file.close();
}
return null;