Compare commits

...

59 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
Maximo Guk cd6314a97b Use wranglers secret:bulk on versions of wrangler prior to 3.60.0 2025-03-14 20:27:34 -05:00
Somhairle MacLeòid ef1f9fb2b0 Update wranglerAction.ts 2025-03-15 00:25:40 +00:00
Maximo Guk 08959b2671 Merge pull request #353 from cloudflare/changeset-release/main
Version Packages
2025-02-10 13:02:55 -06:00
github-actions[bot] abb67eda8d Version Packages 2025-02-10 18:57:44 +00:00
Maximo Guk 883eaf2de0 Merge pull request #351 from cloudflare/maximo/use-wrangler-outputs-for-version-upload-and-wrangler-deploy
Use wrangler outputs for wrangler deploy and versions upload
2025-02-10 12:56:18 -06:00
Maximo Guk 4ff07f4310 Use wrangler outputs for version upload and wrangler deploy 2025-02-09 17:10:09 -06:00
Maximo Guk 4fb15f8b7a Bump @changesets/cli to 2.27.12 and vitest to 2.1.9 2025-02-09 16:51:41 -06:00
Maximo Guk e8cd5f0968 Merge pull request #350 from cloudflare/maximo/handle-failures-in-create-github-deployment-and-job-summary
Handle failures in createGitHubDeployment and createGitHubJobSummary
2025-02-09 16:51:09 -06:00
Maximo Guk e209094e62 Handle failures in createGitHubDeployment and createGitHubJobSummary 2025-02-09 16:49:07 -06:00
Maximo Guk 181b9fb5d6 Merge pull request #346 from cloudflare/changeset-release/main
Version Packages
2025-01-17 09:54:16 -06:00
github-actions[bot] aac328d155 Version Packages 2025-01-17 15:47:45 +00:00
Maximo Guk fc89533f1e Merge pull request #345 from cloudflare/fix/pages-gh-deployment
fix: pages github deployment not triggering due to incorrect condition #2
2025-01-17 09:47:27 -06:00
Maximo Guk e819570b2d Remove stages from pages-deploy-detailed artifact and add changeset 2025-01-17 09:31:23 -06:00
Alexandre Pereira 923db6f87f fix: pages github deployment not triggering due to missing pages artifact stages 2025-01-17 11:30:50 +00:00
Courtney Sims d5031c3dbe Merge pull request #340 from armandsalle/patch-1
Update typo in README.md
2024-12-18 16:22:28 -06:00
Armand SALLE 17da943335 Update typo in README.md 2024-12-14 23:12:56 +01:00
Maximo Guk 3aee84cd6d Merge pull request #330 from cloudflare/changeset-release/main
Version Packages
2024-11-26 16:09:40 -04:00
github-actions[bot] 9d9bee8bfd Version Packages 2024-11-26 19:27:54 +00:00
Maximo Guk ee57f258dc Merge pull request #334 from cloudflare/maximo/bump-default-wrangler-version
Bump default wrangler version to 3.90.0
2024-11-26 15:26:40 -04:00
Maximo Guk 9fed19aa4e Bump default wrangler version to 3.90.0 2024-11-26 07:47:24 -06:00
Maximo Guk 3c6e4da7b3 Merge pull request #332 from uFloppyDisk/chore/action-input-fix
chore(action.yml): rename gitHubToken input in action.yml;
2024-11-25 23:09:49 -04:00
Pawel Bartusiak 1c0eef186f chore(action.yml): rename input;
Rename 'githubToken' input to 'gitHubToken' in action.yml to reflect the key used for intake in the script;
2024-11-25 19:00:09 -08:00
Maximo Guk b19342b08c Merge pull request #325 from cloudflare/maximo/add-github-deployments-parity-for-pages-deployments-in-wrangler-action
Add GitHub deployments for parity with pages-action
2024-11-21 14:52:58 -04:00
Jacob Hands d44c80928a Merge pull request #315 from Alexkuva/patch-1
chore: Add environment parameter for worker secret
2024-11-19 14:34:40 -06:00
Maximo Guk cada7a6312 Add tests for github service, and move all github deployments logic to be inside of github.ts 2024-11-19 12:08:36 -06:00
Maximo Guk 1e92eee7d2 Restructure text fixtures directories, add test-utils.ts and add msw dep 2024-11-19 11:38:18 -06:00
Maximo Guk e7e5400334 Remove pages specific outputs from wrangler deploy command 2024-11-19 11:38:18 -06:00
Maximo Guk d58f116d72 Add github deployments to wrangler action for pages parity 2024-11-19 11:38:18 -06:00
Maximo Guk 64b6339110 Update dependencies and add @actions/github 2024-11-19 11:38:17 -06:00
Alex 95555f6ed0 add environment parameter for worker secret
Add environment parameter for worker secret in readme.md

linked issue example: https://github.com/cloudflare/wrangler-action/issues/251
2024-11-02 17:02:18 +01:00
72 changed files with 3409 additions and 3416 deletions
+19 -20
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
@@ -30,7 +29,7 @@ jobs:
- name: Only build app
uses: ./
with:
workingDirectory: "./test/only-build"
workingDirectory: "./src/test/fixtures/only-build"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --dry-run
@@ -39,7 +38,7 @@ jobs:
uses: ./
with:
quiet: true
workingDirectory: "./test/build-quiet"
workingDirectory: "./src/test/fixtures/build-quiet"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --dry-run
@@ -47,7 +46,7 @@ jobs:
- name: Environment support
uses: ./
with:
workingDirectory: "./test/environment"
workingDirectory: "./src/test/fixtures/environment"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
environment: dev
@@ -65,7 +64,7 @@ jobs:
uses: ./
with:
wranglerVersion: "2.20.0"
workingDirectory: "./test/secrets-v2"
workingDirectory: "./src/test/fixtures/secrets-v2"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
secrets: |
@@ -76,13 +75,13 @@ 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
uses: ./
with:
workingDirectory: "./test/secrets-default"
workingDirectory: "./src/test/fixtures/secrets-default"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
secrets: |
@@ -93,13 +92,13 @@ 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
uses: ./
with:
workingDirectory: "./test/secrets-default"
workingDirectory: "./src/test/fixtures/secrets-default"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: delete --name wrangler-action-test-secrets-v2 --force
@@ -109,7 +108,7 @@ jobs:
- name: Support packageManager variable
uses: ./
with:
workingDirectory: "./test/specify-package-manager"
workingDirectory: "./src/test/fixtures/specify-package-manager"
packageManager: "npm"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
@@ -118,7 +117,7 @@ jobs:
- name: Support unspecified packageManager with no lockfile
uses: ./
with:
workingDirectory: "./test/unspecified-package-manager"
workingDirectory: "./src/test/fixtures/unspecified-package-manager"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --dry-run
@@ -126,7 +125,7 @@ jobs:
- name: Support npm package manager
uses: ./
with:
workingDirectory: "./test/npm"
workingDirectory: "./src/test/fixtures/npm"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --dry-run
@@ -137,29 +136,29 @@ jobs:
- name: Support yarn package manager
uses: ./
with:
workingDirectory: "./test/yarn"
workingDirectory: "./src/test/fixtures/yarn"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --dry-run
- name: Install pnpm
run: npm i -g pnpm
run: npm i -g pnpm@10
- name: Support pnpm package manager
uses: ./
with:
workingDirectory: "./test/pnpm"
workingDirectory: "./src/test/fixtures/pnpm"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --dry-run
- name: Change directory to pre-installed-wrangler and install dependencies
run: |
cd ./test/pre-installed-wrangler
cd ./src/test/fixtures/pre-installed-wrangler
npm install
- name: Support pre-installed wrangler
uses: ./
with:
workingDirectory: "./test/pre-installed-wrangler"
workingDirectory: "./src/test/fixtures/pre-installed-wrangler"
command: action-test
+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);
},
});
+42 -1
View File
@@ -1,5 +1,47 @@
# 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
@@ -83,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`)
+30 -11
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
@@ -76,7 +89,7 @@ jobs:
workingDirectory: "subfoldername"
```
[Worker secrets](https://developers.cloudflare.com/workers/tooling/wrangler/secrets/) can optionally be passed in via `secrets` as a string of names separated by newlines. Each secret name must match the name of an environment variable specified in the `env` field. This creates or replaces the value for the Worker secret using the `wrangler secret put` command.
[Worker secrets](https://developers.cloudflare.com/workers/tooling/wrangler/secrets/) can optionally be passed in via `secrets` as a string of names separated by newlines. Each secret name must match the name of an environment variable specified in the `env` field. This creates or replaces the value for the Worker secret using the `wrangler secret put` command. It's also possible to specify worker environment using environment parameter.
```yaml
jobs:
@@ -85,6 +98,7 @@ jobs:
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
environment: production
secrets: |
SECRET1
SECRET2
@@ -152,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:
@@ -172,14 +186,19 @@ jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy
permissions:
contents: read
deployments: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy YOUR_DIST_FOLDER --project-name=example
# Optional: Enable this if you want to have GitHub Deployments triggered
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
```
### Deploying on a schedule
@@ -196,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:
@@ -222,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:
@@ -234,7 +253,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 `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.
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.
```yaml
jobs:
@@ -242,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:
@@ -368,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:
+4 -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:
@@ -44,6 +44,9 @@ inputs:
packageManager:
description: "The package manager you'd like to use to install and run wrangler. If not specified, the preferred package manager will be inferred based on the presence of a lockfile or fallback to using npm if no lockfile is found. Valid values are `npm` | `pnpm` | `yarn` | `bun`."
required: false
gitHubToken:
description: "GitHub Token"
required: false
outputs:
command-output:
description: "The output of the Wrangler command (comes from stdout)"
+1981 -3036
View File
File diff suppressed because it is too large Load Diff
+20 -12
View File
@@ -1,6 +1,6 @@
{
"name": "wrangler-action",
"version": "3.12.1",
"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",
@@ -29,20 +29,28 @@
"check": "prettier --check ."
},
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.2",
"@cloudflare/workers-types": "^4.20231121.0",
"@types/node": "^20.10.4",
"@vercel/ncc": "^0.38.1",
"prettier": "^3.1.0",
"mock-fs": "^5.4.0",
"semver": "^7.5.4",
"typescript": "^5.3.3",
"vitest": "^1.0.3"
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.12",
"@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"
}
}
+180
View File
@@ -0,0 +1,180 @@
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
@@ -43,7 +43,7 @@ export async function execShell(
await promise;
return child.exitCode;
} catch (err: any) {
} catch (err) {
if (isExecAsyncException(err)) {
process.stderr.write(err.stderr);
throw new Error(`Process failed with exit code ${err.code}`);
+2 -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.81.0";
const DEFAULT_WRANGLER_VERSION = "4";
/**
* A configuration object that contains all the inputs & immutable state for the action.
@@ -26,6 +26,7 @@ const config: WranglerActionConfig = {
tmpdir(),
`wranglerArtifacts-${crypto.randomUUID()}`,
)}`,
GITHUB_TOKEN: getInput("gitHubToken", { required: false }),
} as const;
const packageManager = getPackageManager(config.PACKAGE_MANAGER, {
+24 -17
View File
@@ -3,8 +3,9 @@ import { getPackageManager } from "./packageManagers";
describe("getPackageManager", () => {
test("should use provided value instead of inferring from lockfile", () => {
expect(getPackageManager("npm", { workingDirectory: "test/npm" }))
.toMatchInlineSnapshot(`
expect(
getPackageManager("npm", { workingDirectory: "src/test/fixtures/npm" }),
).toMatchInlineSnapshot(`
{
"exec": "npx",
"execNoInstall": "npx --no-install",
@@ -12,8 +13,9 @@ describe("getPackageManager", () => {
}
`);
expect(getPackageManager("yarn", { workingDirectory: "test/npm" }))
.toMatchInlineSnapshot(`
expect(
getPackageManager("yarn", { workingDirectory: "src/test/fixtures/npm" }),
).toMatchInlineSnapshot(`
{
"exec": "yarn",
"execNoInstall": "yarn",
@@ -21,8 +23,9 @@ describe("getPackageManager", () => {
}
`);
expect(getPackageManager("pnpm", { workingDirectory: "test/npm" }))
.toMatchInlineSnapshot(`
expect(
getPackageManager("pnpm", { workingDirectory: "src/test/fixtures/npm" }),
).toMatchInlineSnapshot(`
{
"exec": "pnpm exec",
"execNoInstall": "pnpm exec",
@@ -30,8 +33,9 @@ describe("getPackageManager", () => {
}
`);
expect(getPackageManager("bun", { workingDirectory: "test/bun" }))
.toMatchInlineSnapshot(`
expect(
getPackageManager("bun", { workingDirectory: "src/test/fixtures/bun" }),
).toMatchInlineSnapshot(`
{
"exec": "bunx",
"execNoInstall": "bun run",
@@ -41,7 +45,7 @@ describe("getPackageManager", () => {
});
test("should use npm if no value provided and package-lock.json exists", () => {
expect(getPackageManager("", { workingDirectory: "test/npm" }))
expect(getPackageManager("", { workingDirectory: "src/test/fixtures/npm" }))
.toMatchInlineSnapshot(`
{
"exec": "npx",
@@ -52,8 +56,9 @@ describe("getPackageManager", () => {
});
test("should use yarn if no value provided and yarn.lock exists", () => {
expect(getPackageManager("", { workingDirectory: "test/yarn" }))
.toMatchInlineSnapshot(`
expect(
getPackageManager("", { workingDirectory: "src/test/fixtures/yarn" }),
).toMatchInlineSnapshot(`
{
"exec": "yarn",
"execNoInstall": "yarn",
@@ -63,8 +68,9 @@ describe("getPackageManager", () => {
});
test("should use pnpm if no value provided and pnpm-lock.yaml exists", () => {
expect(getPackageManager("", { workingDirectory: "test/pnpm" }))
.toMatchInlineSnapshot(`
expect(
getPackageManager("", { workingDirectory: "src/test/fixtures/pnpm" }),
).toMatchInlineSnapshot(`
{
"exec": "pnpm exec",
"execNoInstall": "pnpm exec",
@@ -74,7 +80,7 @@ describe("getPackageManager", () => {
});
test("should use bun if no value provided and bun.lockb exists", () => {
expect(getPackageManager("", { workingDirectory: "test/bun" }))
expect(getPackageManager("", { workingDirectory: "src/test/fixtures/bun" }))
.toMatchInlineSnapshot(`
{
"exec": "bunx",
@@ -85,8 +91,9 @@ describe("getPackageManager", () => {
});
test("should use npm if no value provided and no lockfile is present", () => {
expect(getPackageManager("", { workingDirectory: "test/empty" }))
.toMatchInlineSnapshot(`
expect(
getPackageManager("", { workingDirectory: "src/test/fixtures/empty" }),
).toMatchInlineSnapshot(`
{
"exec": "npx",
"execNoInstall": "npx --no-install",
@@ -97,7 +104,7 @@ describe("getPackageManager", () => {
test("should throw if an invalid value is provided", () => {
expect(() =>
getPackageManager("cargo", { workingDirectory: "test/npm" }),
getPackageManager("cargo", { workingDirectory: "src/test/fixtures/npm" }),
).toThrowError();
});
});
+59
View File
@@ -0,0 +1,59 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { setupServer } from "msw/node";
import { createGitHubDeployment, createJobSummary } from "./github";
import { getOctokit } from "@actions/github";
import { mockGithubDeployments } from "../test/mocks";
import { getTestConfig } from "../test/test-utils";
import mockfs from "mock-fs";
import { readFile } from "fs/promises";
afterEach(() => {
mockfs.restore();
});
describe("github", () => {
it("Calls createGitHubDeployment successfully", async () => {
const githubUser = "mock-user";
const githubRepoName = "wrangler-action";
const server = setupServer(
...mockGithubDeployments({ githubUser, githubRepoName }).handlers,
);
server.listen({ onUnhandledRequest: "error" });
vi.stubEnv("GITHUB_REPOSITORY", `${githubUser}/${githubRepoName}`);
const testConfig = getTestConfig();
const octokit = getOctokit(testConfig.GITHUB_TOKEN, { request: fetch });
await createGitHubDeployment({
config: testConfig,
octokit,
productionBranch: "production-branch",
deploymentId: "fake-deployment-id",
projectName: "fake-project-name",
deploymentUrl: "https://fake-deployment-url.com",
environment: "production",
});
server.close();
});
it("Calls createJobSummary successfully", async () => {
vi.stubEnv("GITHUB_STEP_SUMMARY", "summary");
mockfs({
summary: mockfs.file(),
});
await createJobSummary({
commitHash: "fake-commit-hash",
deploymentUrl: "https://fake-deployment-url.com",
aliasUrl: "https://fake-alias-url.com",
});
expect((await readFile("summary")).toString()).toMatchInlineSnapshot(`
"
# Deploying with Cloudflare Pages
| Name | Result |
| ----------------------- | - |
| **Last commit:** | fake-commit-hash |
| **Preview URL**: | https://fake-deployment-url.com |
| **Branch Preview URL**: | https://fake-alias-url.com |
"
`);
});
});
+128
View File
@@ -0,0 +1,128 @@
import { summary } from "@actions/core";
import { context, getOctokit } from "@actions/github";
import { env } from "process";
import { info, warn } from "../utils";
import { OutputEntryPagesDeployment } from "../wranglerArtifactManager";
import { WranglerActionConfig } from "../wranglerAction";
type Octokit = ReturnType<typeof getOctokit>;
export async function createGitHubDeployment({
config,
octokit,
productionBranch,
environment,
deploymentId,
projectName,
deploymentUrl,
}: {
config: WranglerActionConfig;
octokit: Octokit;
productionBranch: string;
environment: string;
deploymentId: string | null;
projectName: string;
deploymentUrl?: string;
}) {
const githubBranch = env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME;
const productionEnvironment = githubBranch === productionBranch;
const deployment = await octokit.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref: githubBranch || context.ref,
auto_merge: false,
description: "Cloudflare Pages",
required_contexts: [],
environment,
production_environment: productionEnvironment,
});
if (deployment.status !== 201) {
info(config, "Error creating GitHub deployment");
return;
}
await octokit.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: deployment.data.id,
environment,
environment_url: deploymentUrl,
production_environment: productionEnvironment,
// don't have project_name or deployment_id I think
log_url: `https://dash.cloudflare.com/${config.CLOUDFLARE_ACCOUNT_ID}/pages/view/${projectName}/${deploymentId}`,
description: "Cloudflare Pages",
state: "success",
auto_inactive: false,
});
}
export async function createJobSummary({
commitHash,
deploymentUrl,
aliasUrl,
}: {
commitHash: string;
deploymentUrl?: string;
aliasUrl?: string;
}) {
await summary
.addRaw(
`
# Deploying with Cloudflare Pages
| Name | Result |
| ----------------------- | - |
| **Last commit:** | ${commitHash} |
| **Preview URL**: | ${deploymentUrl} |
| **Branch Preview URL**: | ${aliasUrl} |
`,
)
.write();
}
/**
* Create github deployment, if GITHUB_TOKEN is present in config
*/
export async function createGitHubDeploymentAndJobSummary(
config: WranglerActionConfig,
pagesArtifactFields: OutputEntryPagesDeployment,
) {
if (
config.GITHUB_TOKEN &&
pagesArtifactFields.production_branch &&
pagesArtifactFields.pages_project &&
pagesArtifactFields.deployment_trigger
) {
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");
}
}
}
+34
View File
@@ -0,0 +1,34 @@
import { http, HttpResponse } from "msw";
import { z } from "zod";
export function mockGithubDeployments({
githubUser,
githubRepoName,
}: {
githubUser: string;
githubRepoName: string;
}) {
return {
handlers: [
http.post(
`https://api.github.com/repos/${githubUser}/${githubRepoName}/deployments`,
async ({ request }) => {
if (request.headers.get("Authorization") === null) {
return HttpResponse.text("error: no auth token", { status: 400 });
}
const GithubDeploymentsRequest = z.object({
auto_merge: z.literal(false),
description: z.literal("Cloudflare Pages"),
required_contexts: z.array(z.string()).length(0),
environment: z.literal("production"),
production_environment: z.literal(false),
});
// validate request body
GithubDeploymentsRequest.parse(await request.json());
return HttpResponse.json(null);
},
),
],
};
}
+26
View File
@@ -0,0 +1,26 @@
import { WranglerActionConfig } from "../wranglerAction";
export function getTestConfig({
config = {},
}: {
config?: Partial<WranglerActionConfig>;
} = {}): WranglerActionConfig {
return Object.assign(
{
WRANGLER_VERSION: "4.72.0",
didUserProvideWranglerVersion: false,
secrets: [],
workingDirectory: "/src/test/fixtures",
CLOUDFLARE_API_TOKEN: "foo",
CLOUDFLARE_ACCOUNT_ID: "bar",
ENVIRONMENT: "dev",
VARS: [],
COMMANDS: [],
QUIET_MODE: false,
PACKAGE_MANAGER: "npm",
WRANGLER_OUTPUT_DIR: "/tmp/wranglerArtifacts",
GITHUB_TOKEN: "xxxxyy23213123132131",
} as const satisfies WranglerActionConfig,
config,
);
}
+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],
])(
+36
View File
@@ -1,6 +1,12 @@
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 { WranglerActionConfig } from "./wranglerAction";
/**
* A helper function to compare two semver versions. If the second arg is greater than the first arg, it returns true.
@@ -19,3 +25,33 @@ export function checkWorkingDirectory(workingDirectory = ".") {
throw new Error(`Directory ${workingDirectory} does not exist.`);
}
}
export function info(
config: WranglerActionConfig,
message: string,
bypass?: boolean,
): void {
if (!config.QUIET_MODE || bypass) {
originalInfo(message);
}
}
export function warn(
config: WranglerActionConfig,
message: string,
bypass?: boolean,
): void {
if (!config.QUIET_MODE || bypass) {
originalWarn(message);
}
}
export function error(
config: WranglerActionConfig,
message: string,
bypass?: boolean,
): void {
if (!config.QUIET_MODE || bypass) {
originalError(message);
}
}
+469 -82
View File
@@ -1,7 +1,58 @@
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { describe, expect, it, vi } from "vitest";
import { installWrangler } 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 = {
@@ -11,20 +62,7 @@ describe("installWrangler", () => {
};
it("Errors on unsupported wrangler version", async () => {
const testConfig = {
WRANGLER_VERSION: "1",
didUserProvideWranglerVersion: false,
secrets: [],
workingDirectory: "/test",
CLOUDFLARE_API_TOKEN: "foo",
CLOUDFLARE_ACCOUNT_ID: "bar",
ENVIRONMENT: "dev",
VARS: [],
COMMANDS: [],
QUIET_MODE: false,
PACKAGE_MANAGER: "npm",
WRANGLER_OUTPUT_DIR: "/tmp/wranglerArtifacts",
};
const testConfig = getTestConfig({ config: { WRANGLER_VERSION: "1" } });
await expect(
installWrangler(testConfig, testPackageManager),
).rejects.toThrowError(
@@ -33,93 +71,442 @@ describe("installWrangler", () => {
});
it("Does nothing if no wrangler version is specified and wrangler is already installed", async () => {
const testConfig = {
WRANGLER_VERSION: "3.81.0",
didUserProvideWranglerVersion: false,
secrets: [],
workingDirectory: "/test",
CLOUDFLARE_API_TOKEN: "foo",
CLOUDFLARE_ACCOUNT_ID: "bar",
ENVIRONMENT: "dev",
VARS: [],
COMMANDS: [],
QUIET_MODE: false,
PACKAGE_MANAGER: "npm",
WRANGLER_OUTPUT_DIR: "/tmp/wranglerArtifacts",
};
vi.spyOn(exec, "getExecOutput").mockImplementation(
async (commandLine: string, args?: string[]) => {
return {
exitCode: 0,
stderr: "",
stdout: ` ⛅️ wrangler 3.48.0 (update available 3.53.1)`,
};
},
);
const testConfig = getTestConfig();
vi.spyOn(exec, "getExecOutput").mockImplementation(async () => {
return {
exitCode: 0,
stderr: "",
stdout: ` ⛅️ wrangler 3.48.0 (update available 3.53.1)`,
};
});
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 () => {
const testConfig = {
WRANGLER_VERSION: "3.48.0",
didUserProvideWranglerVersion: true,
secrets: [],
workingDirectory: "/test",
CLOUDFLARE_API_TOKEN: "foo",
CLOUDFLARE_ACCOUNT_ID: "bar",
ENVIRONMENT: "dev",
VARS: [],
COMMANDS: [],
QUIET_MODE: false,
PACKAGE_MANAGER: "npm",
WRANGLER_OUTPUT_DIR: "/tmp/wranglerArtifacts",
};
vi.spyOn(exec, "getExecOutput").mockImplementation(
async (commandLine: string, args?: string[]) => {
return {
exitCode: 0,
stderr: "",
stdout: ` ⛅️ wrangler 3.48.0 (update available 3.53.1)`,
};
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "3.48.0",
didUserProvideWranglerVersion: true,
},
);
});
vi.spyOn(exec, "getExecOutput").mockImplementation(async () => {
return {
exitCode: 0,
stderr: "",
stdout: ` ⛅️ wrangler 3.48.0 (update available 3.53.1)`,
};
});
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 = {
WRANGLER_VERSION: "3.48.0",
didUserProvideWranglerVersion: true,
secrets: [],
workingDirectory: "/test",
CLOUDFLARE_API_TOKEN: "foo",
CLOUDFLARE_ACCOUNT_ID: "bar",
ENVIRONMENT: "dev",
VARS: [],
COMMANDS: [],
QUIET_MODE: false,
PACKAGE_MANAGER: "npm",
WRANGLER_OUTPUT_DIR: "/tmp/wranglerArtifacts",
};
vi.spyOn(exec, "getExecOutput").mockImplementation(
async (commandLine: string, args?: string[]) => {
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "3.48.0",
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)`,
};
},
);
vi.spyOn(exec, "exec").mockImplementation(async (commandLine: string) => {
}
// Post-install check: correct version now installed
return {
exitCode: 0,
stderr: "",
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");
});
});
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();
});
});
+91 -127
View File
@@ -2,19 +2,19 @@ import {
debug,
getMultilineInput,
endGroup as originalEndGroup,
error as originalError,
info as originalInfo,
startGroup as originalStartGroup,
setFailed,
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";
import { semverCompare } from "./utils";
import { getDetailedPagesDeployOutput } from "./wranglerArtifactManager";
import { error, info, semverCompare } from "./utils";
import { handleCommandOutputParsing } from "./commandOutputParsing";
import semverLt from "semver/functions/lt";
export type WranglerActionConfig = z.infer<typeof wranglerActionConfig>;
export const wranglerActionConfig = z.object({
@@ -30,28 +30,9 @@ export const wranglerActionConfig = z.object({
QUIET_MODE: z.boolean(),
PACKAGE_MANAGER: z.string(),
WRANGLER_OUTPUT_DIR: z.string(),
GITHUB_TOKEN: z.string(),
});
function info(
config: WranglerActionConfig,
message: string,
bypass?: boolean,
): void {
if (!config.QUIET_MODE || bypass) {
originalInfo(message);
}
}
function error(
config: WranglerActionConfig,
message: string,
bypass?: boolean,
): void {
if (!config.QUIET_MODE || bypass) {
originalError(message);
}
}
function startGroup(config: WranglerActionConfig, name: string): void {
if (!config.QUIET_MODE) {
originalStartGroup(name);
@@ -71,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`,
@@ -105,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(
@@ -140,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,
@@ -181,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) {
@@ -286,7 +305,11 @@ async function uploadSecrets(
);
}
const args = ["wrangler", "secret:bulk"];
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"];
}
if (environment) {
args.push("--env", environment);
@@ -314,29 +337,6 @@ 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,46 +399,8 @@ async function wranglerCommands(
setOutput("command-output", stdOut);
setOutput("command-stderr", stdErr);
// Check if this command is a workers deployment
if (command.startsWith("deploy") || command.startsWith("publish")) {
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);
}
// 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);
} 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);
}
}
// Handles setting github action outputs and creating github deployment and job summary
await handleCommandOutputParsing(config, command, stdOut);
}
} finally {
endGroup(config);
@@ -450,7 +412,9 @@ export {
execCommands,
info,
installWrangler,
isExactSemver,
main,
parseWranglerVersion,
uploadSecrets,
wranglerCommands,
};
+135 -37
View File
@@ -1,17 +1,17 @@
import mock from "mock-fs";
import mockfs from "mock-fs";
import { afterEach, describe, expect, it } from "vitest";
import {
getDetailedPagesDeployOutput,
getOutputEntry,
getWranglerArtifacts,
} from "./wranglerArtifactManager";
afterEach(async () => {
mock.restore();
afterEach(() => {
mockfs.restore();
});
describe("wranglerArtifactsManager", () => {
describe("getWranglerArtifacts()", async () => {
it("Returns only wrangler output files from a given directory", async () => {
mock({
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"}
@@ -27,7 +27,7 @@ describe("wranglerArtifactsManager", () => {
]);
});
it("Returns an empty list when the output directory doesn't exist", async () => {
mock({
mockfs({
notTheDirWeWant: {},
});
@@ -36,50 +36,148 @@ describe("wranglerArtifactsManager", () => {
});
});
describe("getDetailedPagesDeployOutput()", async () => {
it("Returns only detailed pages deploy output from wrangler artifacts", async () => {
mock({
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 () => {
mock({
describe("getOutputEntry()", async () => {
describe("OutputEntryPagesDeployment", 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": `
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"}`,
{"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");
const artifact = await getOutputEntry("./testOutputDir");
if (artifact?.type !== "pages-deploy-detailed") {
throw new Error(`Unexpected type ${artifact?.type}`);
}
expect(artifacts).toEqual({
expect(artifact).toEqual({
version: 1,
type: "pages-deploy-detailed",
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": `
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",
});
});
});
});
});
+56 -20
View File
@@ -6,6 +6,9 @@ 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"),
@@ -14,10 +17,45 @@ const OutputEntryPagesDeployment = OutputEntryBase.merge(
url: z.string().optional(),
alias: z.string().optional(),
environment: z.enum(["production", "preview"]),
// optional, added in wrangler@3.89.0
production_branch: z.string().optional(),
// optional, added in wrangler@3.89.0
deployment_trigger: z
.object({
metadata: z.object({
/** Commit hash of the deployment trigger metadata for the pages project */
commit_hash: z.string(),
}),
})
.optional(),
}),
);
type OutputEntryPagesDeployment = z.infer<typeof OutputEntryPagesDeployment>;
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,
]);
/**
* Parses file names in a directory to find wrangler artifact files
@@ -52,34 +90,32 @@ export async function getWranglerArtifacts(
}
/**
* Searches for detailed wrangler output from a pages deploy
* Searches for a supported wrangler OutputEntry
*
* @param artifactDirectory
* @returns The first pages-output-detailed found within a wrangler artifact directory
* @returns The first SupportedOutputEntry found within a wrangler artifact directory
*/
export async function getDetailedPagesDeployOutput(
export async function getOutputEntry(
artifactDirectory: string,
): Promise<OutputEntryPagesDeployment | null> {
): Promise<SupportedOutputEntry | null> {
const artifactFilePaths = await getWranglerArtifacts(artifactDirectory);
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;
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;
}
} catch (err) {
// If the line can't be parsed, skip it
continue;
}
} finally {
await file.close();
}
await file.close();
}
return null;
+1 -1
View File
@@ -15,6 +15,6 @@
"lib": ["ESNext"],
"types": ["node", "@cloudflare/workers-types"]
},
"exclude": ["node_modules", "**/*.test.ts"],
"exclude": ["node_modules"],
"include": ["src"]
}