Compare commits

...

29 Commits

Author SHA1 Message Date
Eric Clemmons a61fbea322 Merge pull request #429 from cloudflare/changeset-release/main
Version Packages
2026-05-12 15:34:29 -05:00
github-actions[bot] e804ea3dfd Version Packages 2026-05-12 18:20:02 +00:00
Eric Clemmons 8d0324a88e fix: update release workflow to v5 actions and regenerate lockfile 2026-05-12 13:19:37 -05:00
Eric Clemmons 2f18b18a46 Merge pull request #431 from cloudflare/fix/semgrep-blocking-findings
fix: address Semgrep blocking findings
2026-05-12 13:10:33 -05:00
Eric Clemmons 622ff0d765 fix: upgrade checkout and setup-node to v5 for Node 24 runtime 2026-05-12 13:02:33 -05:00
Eric Clemmons f501f05fbd fix: force GitHub actions to run on Node 24 via env var 2026-05-12 13:01:47 -05:00
Eric Clemmons f990691b6b fix: resolve npm audit vulnerabilities via undici override and vitest v3 2026-05-12 11:58:45 -05:00
Eric Clemmons 652762d3f6 fix: migrate action runtime from node20 to node24 2026-05-12 11:43:52 -05:00
Eric Clemmons bd3f4f0b30 fix: add retry support to worker health check using better-result
Replace manual fetch() with Result.tryPromise from better-result,
which provides built-in retry with exponential backoff. This handles
the transient 404s that occur immediately after wrangler deploy due
to edge propagation delays.

- Rename workerHealthCheck.cjs -> .mjs for ESM support
- Add better-result as devDependency
- Update deploy.yml to invoke .mjs file
- Retry up to 5 times with 2s exponential backoff
2026-05-11 16:35:22 -05:00
Eric Clemmons 38dbf5b59f Merge pull request #428 from cloudflare/hrushikesh/add-semgrep-oss-workflow
ci: add Semgrep OSS scanning workflow
2026-05-11 15:04:44 -05:00
Eric Clemmons cdf65921e0 ci: pin pnpm to v10 to fix build script failures 2026-05-11 14:55:57 -05:00
Eric Clemmons bfdfae5e0f fix: address Semgrep blocking findings
- Replace execSync(`curl`) with native fetch() in workerHealthCheck.cjs
  to resolve javascript.lang.security.detect-child-process finding.
- Run npm audit fix to update rollup 4.34.4 -> 4.60.3 and vite 5.4.14 -> 5.4.21,
  resolving CVE-2026-27606 and other supply chain vulnerabilities.
2026-05-11 14:51:16 -05:00
Eric Clemmons a3edea85f7 style: fix prettier formatting in semgrep.yml 2026-05-11 14:10:07 -05:00
Eric Clemmons b6290ea70c Merge pull request #412 from cloudflare/BANDA-1664/wrangler-v4-support
Update default Wrangler version to v4.x
2026-05-11 13:55:58 -05:00
Eric Clemmons 65553b39e6 Clarify v4@latest is installed, not 4.72.0
Co-authored-by: Eric Clemmons <eric@smarterspam.com>
2026-04-26 13:56:44 -05:00
Hrushikesh Deshpande 4f154b96e0 ci: add Semgrep OSS scanning workflow 2026-04-23 18:36:26 -04:00
Somhairle MacLeòid 5f269ee58e Merge pull request #424 from ulgens/update-docs-checkout-action
Update actions/checkout in example code blocks
2026-04-17 12:00:59 +01:00
Eric Clemmons d181764e4b Merge pull request #427 from cloudflare/changeset-release/main
Version Packages
2026-04-15 15:48:16 -05:00
Eric Clemmons 3c924dd85f Default to 4, not 4.72.0 2026-04-15 15:39:44 -05:00
Eric Clemmons c1f90e624b major, not minor, version bump 2026-04-15 15:39:04 -05:00
github-actions[bot] 0501554080 Version Packages 2026-04-15 19:40:15 +00:00
Eric Clemmons d8f3eaa359 Merge pull request #426 from cloudflare/willtaylor/escalation-963-support-version-ranges
Support version ranges and tags in wranglerVersion input
2026-04-15 14:39:45 -05:00
Will Taylor febbda69f8 Support version ranges and tags in wranglerVersion input
The wranglerVersion input only accepted exact versions like '4.81.0'.
Setting it to a major version like '4', a range like '^4.0.0', or a
tag like 'latest' crashed with 'Invalid Version' during secret
uploads. npm installed the right version fine, but subsequent version
comparisons failed because they expected exact X.Y.Z format.

After installing wrangler, the action now runs wrangler --version to
get the concrete installed version and uses that for all version
comparisons. If version detection fails and the user gave an exact
version, the action falls back to using it directly, preserving
existing behavior.

Pre-installed versions that satisfy a range skip reinstallation.

Fixes #390
Relates to #366
Relates to #379
2026-04-15 14:40:29 -04:00
Ülgen Sarıkavak 59b83385d8 Update actions/checkout in example code blocks
Similar to https://github.com/cloudflare/cloudflare-docs/pull/29846
2026-04-14 18:04:30 +03:00
Eric Clemmons dd8ea1b72d Clarify Wrangler v3 Support
Co-authored-by: Ben <4991309+NuroDev@users.noreply.github.com>
2026-03-24 15:36:43 -05:00
Eric Clemmons 1029e90033 Update default Wrangler version to v4 (4.72.0)
The action now defaults to Wrangler v4 instead of v3. All existing
version-gated logic (secret upload, deploy vs publish) correctly
handles v4. Users can pin to v3 via wranglerVersion input.

Closes BANDA-1664
2026-03-12 17:51:09 -05:00
Maximo Guk 707f637509 Merge pull request #359 from cloudflare/changeset-release/main
Version Packages
2025-03-14 21:01:10 -05:00
github-actions[bot] e764ef3355 Version Packages 2025-03-15 01:57:01 +00:00
Maximo Guk 8d761e6bdc Merge pull request #358 from cloudflare/penalosa-patch-2
Support Wrangler v4 bulk secrets
2025-03-14 20:56:46 -05:00
16 changed files with 2048 additions and 1041 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"wrangler-action": patch
---
Use `secret bulk` instead of deprecated `secret:bulk` command
+6 -7
View File
@@ -9,13 +9,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
# Pinned due to compatibility issues on 23.2.0
node-version: "22"
node-version: "24"
cache: "npm"
- name: Install modules and build
@@ -76,7 +75,7 @@ jobs:
SECRET2: ${{ secrets.SECRET2 }}
- name: Health Check Deployed Worker
run: node .github/workflows/workerHealthCheck.cjs wrangler-action-test-secrets-v2
run: node .github/workflows/workerHealthCheck.ts wrangler-action-test-secrets-v2
shell: bash
- name: Deploy app secrets w/ default version
@@ -93,7 +92,7 @@ jobs:
SECRET2: ${{ secrets.SECRET2 }}
- name: Health Check Deployed Worker
run: node .github/workflows/workerHealthCheck.cjs wrangler-action-test-secrets-default
run: node .github/workflows/workerHealthCheck.ts wrangler-action-test-secrets-default
shell: bash
- name: Clean Up Deployed Workers
@@ -143,7 +142,7 @@ jobs:
command: deploy --dry-run
- name: Install pnpm
run: npm i -g pnpm
run: npm i -g pnpm@10
- name: Support pnpm package manager
uses: ./
+3 -4
View File
@@ -19,16 +19,15 @@ jobs:
issues: read
steps:
- name: Checkout Repo
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: true
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
# Pinned due to compatibility issues on 23.2.0
node-version: "22"
node-version: "24"
cache: "npm"
- name: Install modules
+23 -17
View File
@@ -1,24 +1,30 @@
name: Semgrep OSS scan
on:
pull_request: {}
workflow_dispatch: {}
push:
branches:
- main
- master
branches: [main, master]
workflow_dispatch: {}
schedule:
- cron: "0 0 * * *"
name: Semgrep config
- cron: "0 0 20 * *"
concurrency:
group: semgrep-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
semgrep:
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
name: semgrep-oss
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v4
- run: semgrep ci
- 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
-28
View File
@@ -1,28 +0,0 @@
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
@@ -0,0 +1,41 @@
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);
},
});
+18 -1
View File
@@ -1,5 +1,23 @@
# 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
@@ -107,7 +125,6 @@
### 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`)
+22 -9
View File
@@ -2,6 +2,17 @@
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.
@@ -27,7 +38,7 @@ jobs:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Deploy
uses: cloudflare/wrangler-action@v3
with:
@@ -52,7 +63,7 @@ jobs:
## Configuration
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`:
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`.
```yaml
jobs:
@@ -61,9 +72,11 @@ jobs:
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
wranglerVersion: "2.20.0"
wranglerVersion: "4"
```
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
@@ -153,7 +166,7 @@ jobs:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Deploy
uses: cloudflare/wrangler-action@v3
with:
@@ -177,7 +190,7 @@ jobs:
contents: read
deployments: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Deploy
uses: cloudflare/wrangler-action@v3
with:
@@ -202,7 +215,7 @@ jobs:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Deploy app
uses: cloudflare/wrangler-action@v3
with:
@@ -228,7 +241,7 @@ jobs:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Deploy app
uses: cloudflare/wrangler-action@v3
with:
@@ -248,7 +261,7 @@ jobs:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Upload Worker Version
uses: cloudflare/wrangler-action@v3
with:
@@ -374,7 +387,7 @@ jobs:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- 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: "node20"
using: "node24"
main: "dist/index.mjs"
inputs:
apiToken:
+1490 -921
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -1,6 +1,6 @@
{
"name": "wrangler-action",
"version": "3.14.0",
"version": "4.0.0",
"description": "GitHub Action to use [Wrangler](https://developers.cloudflare.com/workers/cli-wrangler/).",
"author": "wrangler@cloudflare.com",
"license": "MIT OR Apache-2.0",
@@ -42,11 +42,15 @@
"@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": "^2.1.9"
"vitest": "^3.2.4"
},
"overrides": {
"undici": "^6.23.1"
}
}
+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 = "3.90.0";
const DEFAULT_WRANGLER_VERSION = "4";
/**
* A configuration object that contains all the inputs & immutable state for the action.
+1 -1
View File
@@ -7,7 +7,7 @@ export function getTestConfig({
} = {}): WranglerActionConfig {
return Object.assign(
{
WRANGLER_VERSION: "3.81.0",
WRANGLER_VERSION: "4.72.0",
didUserProvideWranglerVersion: false,
secrets: [],
workingDirectory: "/src/test/fixtures",
+4
View File
@@ -42,6 +42,10 @@ 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],
])(
+352 -5
View File
@@ -1,9 +1,59 @@
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { describe, expect, it, vi } from "vitest";
import { installWrangler, uploadSecrets } from "./wranglerAction";
import {
installWrangler,
isExactSemver,
main,
parseWranglerVersion,
uploadSecrets,
} 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",
@@ -30,10 +80,14 @@ describe("installWrangler", () => {
};
});
const infoSpy = vi.spyOn(core, "info");
await installWrangler(testConfig, testPackageManager);
const resolvedVersion = 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 () => {
@@ -51,9 +105,14 @@ describe("installWrangler", () => {
};
});
const infoSpy = vi.spyOn(core, "info");
await installWrangler(testConfig, testPackageManager);
const resolvedVersion = 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: {
@@ -61,19 +120,208 @@ 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.20.0 (update available 3.53.1)`,
stdout: ` ⛅️ wrangler 3.48.0`,
};
});
vi.spyOn(exec, "exec").mockImplementation(async () => {
return 0;
});
const infoSpy = vi.spyOn(core, "info");
await installWrangler(testConfig, testPackageManager);
const resolvedVersion = 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");
});
});
@@ -162,4 +410,103 @@ describe("uploadSecrets", () => {
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();
});
});
+80 -39
View File
@@ -7,7 +7,8 @@ import {
setOutput,
} from "@actions/core";
import { getExecOutput } from "@actions/exec";
import semverEq from "semver/functions/eq";
import semverSatisfies from "semver/functions/satisfies";
import semverValid from "semver/functions/valid";
import { z } from "zod";
import { exec, execShell } from "./exec";
import { PackageManager } from "./packageManagers";
@@ -51,32 +52,60 @@ async function main(
try {
wranglerActionConfig.parse(config);
authenticationSetup(config);
await installWrangler(config, packageManager);
const resolvedVersion = await installWrangler(config, packageManager);
const resolvedConfig = { ...config, WRANGLER_VERSION: resolvedVersion };
await execCommands(
config,
resolvedConfig,
packageManager,
getMultilineInput("preCommands"),
"pre",
);
await uploadSecrets(config, packageManager);
await wranglerCommands(config, packageManager);
await uploadSecrets(resolvedConfig, packageManager);
await wranglerCommands(resolvedConfig, packageManager);
await execCommands(
config,
resolvedConfig,
packageManager,
getMultilineInput("postCommands"),
"post",
);
info(config, "🏁 Wrangler Action completed", true);
info(resolvedConfig, "🏁 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`,
@@ -85,33 +114,25 @@ async function installWrangler(
startGroup(config, "🔍 Checking for existing Wrangler installation");
let installedVersion = "";
let installedVersionSatisfiesRequirement = false;
let versionSatisfied = false;
try {
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,
},
);
installedVersion = await resolveInstalledVersion(config, packageManager);
// 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) {
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;
}
}
}
if (!config.didUserProvideWranglerVersion && installedVersion) {
info(
@@ -120,15 +141,12 @@ async function installWrangler(
true,
);
endGroup(config);
return;
return installedVersion;
}
if (
config.didUserProvideWranglerVersion &&
installedVersionSatisfiesRequirement
) {
if (config.didUserProvideWranglerVersion && versionSatisfied) {
info(config, `✅ Using Wrangler ${installedVersion}`, true);
endGroup(config);
return;
return installedVersion;
}
info(
config,
@@ -161,6 +179,27 @@ 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) {
@@ -373,7 +412,9 @@ export {
execCommands,
info,
installWrangler,
isExactSemver,
main,
parseWranglerVersion,
uploadSecrets,
wranglerCommands,
};