Compare commits

...

38 commits

Author SHA1 Message Date
Maximo Guk
707f637509
Merge pull request 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 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 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 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 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 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 from cloudflare/fix/pages-gh-deployment
fix: pages github deployment not triggering due to incorrect condition 
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 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 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 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 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 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 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
Courtney Sims
ac2fd0ec8f
Merge pull request from cloudflare/courtney-sims-documentation-update
Update documentation
2024-11-13 16:49:06 -06:00
Courtney Sims
0140ffbc93 Update documentation 2024-11-13 12:06:27 -06:00
Courtney Sims
cc4ede3f06
Merge pull request from cloudflare/courtney-sims-testing
Add tests for installWrangler
2024-11-13 09:15:02 -06:00
Courtney Sims
9bf87caf2b Pin node version 2024-11-12 17:25:56 -06:00
Courtney Sims
81f8814855 BANDA-484: Add tests for InstallWrangler 2024-11-12 11:54:43 -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
71 changed files with 2577 additions and 3342 deletions

View file

@ -1,7 +1,7 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
with multi-package repos, or single-package repos to help you version and deploy your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in

View file

@ -14,7 +14,8 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "latest"
# Pinned due to compatibility issues on 23.2.0
node-version: "22"
cache: "npm"
- name: Install modules and build
@ -29,7 +30,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
@ -38,7 +39,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
@ -46,7 +47,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
@ -64,7 +65,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: |
@ -81,7 +82,7 @@ jobs:
- 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: |
@ -98,7 +99,7 @@ jobs:
- 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
@ -108,7 +109,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 }}
@ -117,7 +118,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
@ -125,7 +126,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
@ -136,7 +137,7 @@ 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
@ -147,18 +148,18 @@ jobs:
- 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

View file

@ -27,7 +27,8 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "latest"
# Pinned due to compatibility issues on 23.2.0
node-version: "22"
cache: "npm"
- name: Install modules

View file

@ -1,5 +1,35 @@
# Changelog
## 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

21
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,21 @@
# Contributing Guide
## Releases
### Changesets
Every non-trivial change to the project - those that should appear in the changelog - must be captured in a "changeset". We use the changesets tool for creating changesets, publishing versions and updating the changelog.
Create a changeset for the current change.
> npx changeset
> Select which workspaces are affected by the change and whether the version requires a major, minor or patch release.
> Update the generated changeset with a description of the change.
> Include the generate changeset in the current commit.
> git add ./changeset/\*.md
### Version Packages PRs
Once you merge your PR, a new Version Packages PR will be opened in the wrangler-action repo. Once that PR is merged, your change will be released. Example: https://github.com/cloudflare/wrangler-action/pull/305
Note: Version Packages PRs are only generated if there's been at least one PR merged with a changeset.

View file

@ -76,7 +76,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 +85,7 @@ jobs:
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
environment: production
secrets: |
SECRET1
SECRET2
@ -121,6 +122,20 @@ jobs:
command: whoami
```
You can also add a command that spans multiple lines:
```yaml
jobs:
deploy:
steps:
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: |
pages project list
pages deploy .vercel/output/static --project-name=demo-actions --branch=test
```
## Use cases
### Deploy when commits are merged to main
@ -158,6 +173,9 @@ jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy
permissions:
contents: read
deployments: write
steps:
- uses: actions/checkout@v4
- name: Deploy
@ -166,6 +184,8 @@ jobs:
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
@ -220,7 +240,7 @@ For more advanced usage or to programmatically trigger the workflow from scripts
### Upload a Worker Version
To create a new version of your Worker that is not deployed immediately, use the `wrangler versions upload --experimental-versions` 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 --experimental-versions` command or via the Cloudflare dashboard under the Deployments tab. For now, the `--experimental-versions` flag and 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:
@ -234,11 +254,27 @@ jobs:
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: versions upload --experimental-versions
command: versions upload
```
## Advanced Usage
### Setting A Worker Secret for A Specific Environment
There is an environment parameter that can be set within the workflow to enable this. Example:
```yaml
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env production
secrets: |
SUPER_SECRET
environment: production
env:
SUPER_SECRET: ${{ secrets.SUPER_SECRET }}
```
### Using Wrangler Command Output in Subsequent Steps
More advanced workflows may need to parse the resulting output of Wrangler commands. To do this, you can use the `command-output` output variable in subsequent steps. For example, if you want to print the output of the Wrangler command, you can do the following:

View file

@ -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)"

4056
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "wrangler-action",
"version": "3.12.1",
"version": "3.14.1",
"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,24 @@
"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",
"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"
}
}

180
src/commandOutputParsing.ts Normal file
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;
}
}

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}`);

View file

@ -1,30 +1,16 @@
import {
debug,
getBooleanInput,
getInput,
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 { exec, execShell } from "./exec";
import { getPackageManager } from "./packageManagers";
import { checkWorkingDirectory, semverCompare } from "./utils";
import { getDetailedPagesDeployOutput } from "./wranglerArtifactManager";
import { join } from "path";
import { getBooleanInput, getInput, getMultilineInput } from "@actions/core";
import { tmpdir } from "os";
import { join } from "path";
import { getPackageManager } from "./packageManagers";
import { checkWorkingDirectory } from "./utils";
import { main, WranglerActionConfig } from "./wranglerAction";
const DEFAULT_WRANGLER_VERSION = "3.81.0";
const DEFAULT_WRANGLER_VERSION = "3.90.0";
/**
* A configuration object that contains all the inputs & immutable state for the action.
*/
const config = {
const config: WranglerActionConfig = {
WRANGLER_VERSION: getInput("wranglerVersion") || DEFAULT_WRANGLER_VERSION,
didUserProvideWranglerVersion: Boolean(getInput("wranglerVersion")),
secrets: getMultilineInput("secrets"),
@ -40,382 +26,11 @@ const config = {
tmpdir(),
`wranglerArtifacts-${crypto.randomUUID()}`,
)}`,
GITHUB_TOKEN: getInput("gitHubToken", { required: false }),
} as const;
const packageManager = getPackageManager(config.PACKAGE_MANAGER, {
workingDirectory: config.workingDirectory,
});
function info(message: string, bypass?: boolean): void {
if (!config.QUIET_MODE || bypass) {
originalInfo(message);
}
}
function error(message: string, bypass?: boolean): void {
if (!config.QUIET_MODE || bypass) {
originalError(message);
}
}
function startGroup(name: string): void {
if (!config.QUIET_MODE) {
originalStartGroup(name);
}
}
function endGroup(): void {
if (!config.QUIET_MODE) {
originalEndGroup();
}
}
async function main() {
try {
authenticationSetup();
await installWrangler();
await execCommands(getMultilineInput("preCommands"), "pre");
await uploadSecrets();
await wranglerCommands();
await execCommands(getMultilineInput("postCommands"), "post");
info("🏁 Wrangler Action completed", true);
} catch (err: unknown) {
err instanceof Error && error(err.message);
setFailed("🚨 Action failed");
}
}
async function installWrangler() {
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`,
);
}
startGroup("🔍 Checking for existing Wrangler installation");
let installedVersion = "";
let installedVersionSatisfiesRequirement = 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,
},
);
// There are two possible outputs from `wrangler --version`:
// ` ⛅️ wrangler 3.48.0 (update available 3.53.1)`
// and
// `3.48.0`
const versionMatch =
stdout.match(/wrangler (\d+\.\d+\.\d+)/) ??
stdout.match(/^(\d+\.\d+\.\d+)/m);
if (versionMatch) {
installedVersion = versionMatch[1];
}
if (config.didUserProvideWranglerVersion) {
installedVersionSatisfiesRequirement = semverEq(
installedVersion,
config["WRANGLER_VERSION"],
);
}
if (!config.didUserProvideWranglerVersion && installedVersion) {
info(
`✅ No wrangler version specified, using pre-installed wrangler version ${installedVersion}`,
true,
);
endGroup();
return;
}
if (
config.didUserProvideWranglerVersion &&
installedVersionSatisfiesRequirement
) {
info(`✅ Using Wrangler ${installedVersion}`, true);
endGroup();
return;
}
info(
"⚠️ Wrangler not found or version is incompatible. Installing...",
true,
);
} catch (error) {
debug(`Error checking Wrangler version: ${error}`);
info(
"⚠️ Wrangler not found or version is incompatible. Installing...",
true,
);
} finally {
endGroup();
}
startGroup("📥 Installing Wrangler");
try {
await exec(
packageManager.install,
[`wrangler@${config["WRANGLER_VERSION"]}`],
{
cwd: config["workingDirectory"],
silent: config["QUIET_MODE"],
},
);
info(`✅ Wrangler installed`, true);
} finally {
endGroup();
}
}
function authenticationSetup() {
process.env.CLOUDFLARE_API_TOKEN = config["CLOUDFLARE_API_TOKEN"];
process.env.CLOUDFLARE_ACCOUNT_ID = config["CLOUDFLARE_ACCOUNT_ID"];
}
async function execCommands(commands: string[], cmdType: string) {
if (!commands.length) {
return;
}
startGroup(`🚀 Running ${cmdType}Commands`);
try {
for (const command of commands) {
const cmd = command.startsWith("wrangler")
? `${packageManager.exec} ${command}`
: command;
await execShell(cmd, {
cwd: config["workingDirectory"],
silent: config["QUIET_MODE"],
});
}
} finally {
endGroup();
}
}
function getSecret(secret: string) {
if (!secret) {
throw new Error("Secret name cannot be blank.");
}
const value = process.env[secret];
if (!value) {
throw new Error(`Value for secret ${secret} not found in environment.`);
}
return value;
}
function getEnvVar(envVar: string) {
if (!envVar) {
throw new Error("Var name cannot be blank.");
}
const value = process.env[envVar];
if (!value) {
throw new Error(`Value for var ${envVar} not found in environment.`);
}
return value;
}
async function legacyUploadSecrets(
secrets: string[],
environment?: string,
workingDirectory?: string,
) {
for (const secret of secrets) {
const args = ["wrangler", "secret", "put", secret];
if (environment) {
args.push("--env", environment);
}
await exec(packageManager.exec, args, {
cwd: workingDirectory,
silent: config["QUIET_MODE"],
input: Buffer.from(getSecret(secret)),
});
}
}
async function uploadSecrets() {
const secrets: string[] = config["secrets"];
const environment = config["ENVIRONMENT"];
const workingDirectory = config["workingDirectory"];
if (!secrets.length) {
return;
}
startGroup("🔑 Uploading secrets...");
try {
if (semverCompare(config["WRANGLER_VERSION"], "3.4.0")) {
return legacyUploadSecrets(secrets, environment, workingDirectory);
}
const args = ["wrangler", "secret:bulk"];
if (environment) {
args.push("--env", environment);
}
await exec(packageManager.exec, args, {
cwd: workingDirectory,
silent: config["QUIET_MODE"],
input: Buffer.from(
JSON.stringify(
Object.fromEntries(
secrets.map((secret) => [secret, getSecret(secret)]),
),
),
),
});
} catch (err: unknown) {
if (err instanceof Error) {
error(err.message);
err.stack && debug(err.stack);
}
throw new Error(`Failed to upload secrets.`);
} finally {
endGroup();
}
}
// 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() {
startGroup("🚀 Running Wrangler Commands");
try {
const commands = config["COMMANDS"];
const environment = config["ENVIRONMENT"];
if (!commands.length) {
const wranglerVersion = config["WRANGLER_VERSION"];
const deployCommand = semverCompare("2.20.0", wranglerVersion)
? "deploy"
: "publish";
commands.push(deployCommand);
}
for (let command of commands) {
const args = [];
if (environment && !command.includes("--env")) {
args.push("--env", environment);
}
if (
config["VARS"].length &&
(command.startsWith("deploy") || command.startsWith("publish")) &&
!command.includes("--var")
) {
args.push("--var");
for (const v of config["VARS"]) {
args.push(`${v}:${getEnvVar(v)}`);
}
}
// Used for saving the wrangler output
let stdOut = "";
let stdErr = "";
// set WRANGLER_OUTPUT_FILE_DIRECTORY env for exec
process.env.WRANGLER_OUTPUT_FILE_DIRECTORY = config.WRANGLER_OUTPUT_DIR;
const options = {
cwd: config["workingDirectory"],
silent: config["QUIET_MODE"],
listeners: {
stdout: (data: Buffer) => {
stdOut += data.toString();
},
stderr: (data: Buffer) => {
stdErr += data.toString();
},
},
};
// Execute the wrangler command
await exec(`${packageManager.exec} wrangler ${command}`, args, options);
// Set the outputs for the command
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(
"Unable to find a WRANGLER_OUTPUT_DIR, environment and id fields will be unavailable for output. Have you updated wrangler to version >=3.81.0?",
);
// DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change
const { deploymentUrl, aliasUrl } =
extractDeploymentUrlsFromStdout(stdOut);
setOutput("deployment-url", deploymentUrl);
// DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change
setOutput("deployment-alias-url", aliasUrl);
setOutput("pages-deployment-alias-url", aliasUrl);
}
}
}
} finally {
endGroup();
}
}
main();
export {
authenticationSetup,
execCommands,
installWrangler,
uploadSecrets,
wranglerCommands,
};
main(config, packageManager);

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();
});
});

View file

@ -1,7 +1,7 @@
import { existsSync } from "node:fs";
import * as path from "node:path";
interface PackageManager {
export interface PackageManager {
install: string;
exec: string;
execNoInstall: string;

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
src/service/github.ts Normal file
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
src/test/mocks.ts Normal file
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
src/test/test-utils.ts Normal file
View file

@ -0,0 +1,26 @@
import { WranglerActionConfig } from "../wranglerAction";
export function getTestConfig({
config = {},
}: {
config?: Partial<WranglerActionConfig>;
} = {}): WranglerActionConfig {
return Object.assign(
{
WRANGLER_VERSION: "3.81.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,
);
}

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);
}
}

165
src/wranglerAction.test.ts Normal file
View file

@ -0,0 +1,165 @@
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 { getTestConfig } from "./test/test-utils";
describe("installWrangler", () => {
const testPackageManager = {
install: "npm i",
exec: "npx",
execNoInstall: "npx --no-install",
};
it("Errors on unsupported wrangler version", async () => {
const testConfig = getTestConfig({ config: { WRANGLER_VERSION: "1" } });
await expect(
installWrangler(testConfig, testPackageManager),
).rejects.toThrowError(
`Wrangler v1 is no longer supported by this action. Please use major version 2 or greater`,
);
});
it("Does nothing if no wrangler version is specified and wrangler is already installed", async () => {
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);
expect(infoSpy).toBeCalledWith(
"✅ No wrangler version specified, using pre-installed wrangler version 3.48.0",
);
});
it("Does nothing if the wrangler version specified is the same as the one installed", async () => {
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);
expect(infoSpy).toBeCalledWith("✅ Using Wrangler 3.48.0");
});
it("Should install wrangler if the version specified is not already available", async () => {
const testConfig = getTestConfig({
config: {
WRANGLER_VERSION: "3.48.0",
didUserProvideWranglerVersion: true,
},
});
vi.spyOn(exec, "getExecOutput").mockImplementation(async () => {
return {
exitCode: 0,
stderr: "",
stdout: ` ⛅️ wrangler 3.20.0 (update available 3.53.1)`,
};
});
vi.spyOn(exec, "exec").mockImplementation(async () => {
return 0;
});
const infoSpy = vi.spyOn(core, "info");
await installWrangler(testConfig, testPackageManager);
expect(infoSpy).toBeCalledWith("✅ Wrangler installed");
});
});
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();
});
});

379
src/wranglerAction.ts Normal file
View file

@ -0,0 +1,379 @@
import {
debug,
getMultilineInput,
endGroup as originalEndGroup,
startGroup as originalStartGroup,
setFailed,
setOutput,
} from "@actions/core";
import { getExecOutput } from "@actions/exec";
import semverEq from "semver/functions/eq";
import { z } from "zod";
import { exec, execShell } from "./exec";
import { PackageManager } from "./packageManagers";
import { error, info, semverCompare } from "./utils";
import { handleCommandOutputParsing } from "./commandOutputParsing";
import semverLt from "semver/functions/lt";
export type WranglerActionConfig = z.infer<typeof wranglerActionConfig>;
export const wranglerActionConfig = z.object({
WRANGLER_VERSION: z.string(),
didUserProvideWranglerVersion: z.boolean(),
secrets: z.array(z.string()),
workingDirectory: z.string(),
CLOUDFLARE_API_TOKEN: z.string(),
CLOUDFLARE_ACCOUNT_ID: z.string(),
ENVIRONMENT: z.string(),
VARS: z.array(z.string()),
COMMANDS: z.array(z.string()),
QUIET_MODE: z.boolean(),
PACKAGE_MANAGER: z.string(),
WRANGLER_OUTPUT_DIR: z.string(),
GITHUB_TOKEN: z.string(),
});
function startGroup(config: WranglerActionConfig, name: string): void {
if (!config.QUIET_MODE) {
originalStartGroup(name);
}
}
function endGroup(config: WranglerActionConfig): void {
if (!config.QUIET_MODE) {
originalEndGroup();
}
}
async function main(
config: WranglerActionConfig,
packageManager: PackageManager,
) {
try {
wranglerActionConfig.parse(config);
authenticationSetup(config);
await installWrangler(config, packageManager);
await execCommands(
config,
packageManager,
getMultilineInput("preCommands"),
"pre",
);
await uploadSecrets(config, packageManager);
await wranglerCommands(config, packageManager);
await execCommands(
config,
packageManager,
getMultilineInput("postCommands"),
"post",
);
info(config, "🏁 Wrangler Action completed", true);
} catch (err: unknown) {
err instanceof Error && error(config, err.message);
setFailed("🚨 Action failed");
}
}
async function installWrangler(
config: WranglerActionConfig,
packageManager: PackageManager,
) {
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`,
);
}
startGroup(config, "🔍 Checking for existing Wrangler installation");
let installedVersion = "";
let installedVersionSatisfiesRequirement = 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,
},
);
// There are two possible outputs from `wrangler --version`:
// ` ⛅️ wrangler 3.48.0 (update available 3.53.1)`
// and
// `3.48.0`
const versionMatch =
stdout.match(/wrangler (\d+\.\d+\.\d+)/) ??
stdout.match(/^(\d+\.\d+\.\d+)/m);
if (versionMatch) {
installedVersion = versionMatch[1];
}
if (config.didUserProvideWranglerVersion) {
installedVersionSatisfiesRequirement = semverEq(
installedVersion,
config["WRANGLER_VERSION"],
);
}
if (!config.didUserProvideWranglerVersion && installedVersion) {
info(
config,
`✅ No wrangler version specified, using pre-installed wrangler version ${installedVersion}`,
true,
);
endGroup(config);
return;
}
if (
config.didUserProvideWranglerVersion &&
installedVersionSatisfiesRequirement
) {
info(config, `✅ Using Wrangler ${installedVersion}`, true);
endGroup(config);
return;
}
info(
config,
"⚠️ Wrangler not found or version is incompatible. Installing...",
true,
);
} catch (error) {
debug(`Error checking Wrangler version: ${error}`);
info(
config,
"⚠️ Wrangler not found or version is incompatible. Installing...",
true,
);
} finally {
endGroup(config);
}
startGroup(config, "📥 Installing Wrangler");
try {
await exec(
packageManager.install,
[`wrangler@${config["WRANGLER_VERSION"]}`],
{
cwd: config["workingDirectory"],
silent: config["QUIET_MODE"],
},
);
info(config, `✅ Wrangler installed`, true);
} finally {
endGroup(config);
}
}
function authenticationSetup(config: WranglerActionConfig) {
process.env.CLOUDFLARE_API_TOKEN = config["CLOUDFLARE_API_TOKEN"];
process.env.CLOUDFLARE_ACCOUNT_ID = config["CLOUDFLARE_ACCOUNT_ID"];
}
async function execCommands(
config: WranglerActionConfig,
packageManager: PackageManager,
commands: string[],
cmdType: string,
) {
if (!commands.length) {
return;
}
startGroup(config, `🚀 Running ${cmdType}Commands`);
try {
for (const command of commands) {
const cmd = command.startsWith("wrangler")
? `${packageManager.exec} ${command}`
: command;
await execShell(cmd, {
cwd: config["workingDirectory"],
silent: config["QUIET_MODE"],
});
}
} finally {
endGroup(config);
}
}
function getSecret(secret: string) {
if (!secret) {
throw new Error("Secret name cannot be blank.");
}
const value = process.env[secret];
if (!value) {
throw new Error(`Value for secret ${secret} not found in environment.`);
}
return value;
}
function getEnvVar(envVar: string) {
if (!envVar) {
throw new Error("Var name cannot be blank.");
}
const value = process.env[envVar];
if (!value) {
throw new Error(`Value for var ${envVar} not found in environment.`);
}
return value;
}
async function legacyUploadSecrets(
config: WranglerActionConfig,
packageManager: PackageManager,
secrets: string[],
environment?: string,
workingDirectory?: string,
) {
for (const secret of secrets) {
const args = ["wrangler", "secret", "put", secret];
if (environment) {
args.push("--env", environment);
}
await exec(packageManager.exec, args, {
cwd: workingDirectory,
silent: config["QUIET_MODE"],
input: Buffer.from(getSecret(secret)),
});
}
}
async function uploadSecrets(
config: WranglerActionConfig,
packageManager: PackageManager,
) {
const secrets: string[] = config["secrets"];
const environment = config["ENVIRONMENT"];
const workingDirectory = config["workingDirectory"];
if (!secrets.length) {
return;
}
startGroup(config, "🔑 Uploading secrets...");
try {
if (semverCompare(config["WRANGLER_VERSION"], "3.4.0")) {
return legacyUploadSecrets(
config,
packageManager,
secrets,
environment,
workingDirectory,
);
}
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);
}
await exec(packageManager.exec, args, {
cwd: workingDirectory,
silent: config["QUIET_MODE"],
input: Buffer.from(
JSON.stringify(
Object.fromEntries(
secrets.map((secret) => [secret, getSecret(secret)]),
),
),
),
});
} catch (err: unknown) {
if (err instanceof Error) {
error(config, err.message);
err.stack && debug(err.stack);
}
throw new Error(`Failed to upload secrets.`);
} finally {
endGroup(config);
}
}
async function wranglerCommands(
config: WranglerActionConfig,
packageManager: PackageManager,
) {
startGroup(config, "🚀 Running Wrangler Commands");
try {
const commands = config["COMMANDS"];
const environment = config["ENVIRONMENT"];
if (!commands.length) {
const wranglerVersion = config["WRANGLER_VERSION"];
const deployCommand = semverCompare("2.20.0", wranglerVersion)
? "deploy"
: "publish";
commands.push(deployCommand);
}
for (let command of commands) {
const args = [];
if (environment && !command.includes("--env")) {
args.push("--env", environment);
}
if (
config["VARS"].length &&
(command.startsWith("deploy") || command.startsWith("publish")) &&
!command.includes("--var")
) {
args.push("--var");
for (const v of config["VARS"]) {
args.push(`${v}:${getEnvVar(v)}`);
}
}
// Used for saving the wrangler output
let stdOut = "";
let stdErr = "";
// set WRANGLER_OUTPUT_FILE_DIRECTORY env for exec
process.env.WRANGLER_OUTPUT_FILE_DIRECTORY = config.WRANGLER_OUTPUT_DIR;
const options = {
cwd: config["workingDirectory"],
silent: config["QUIET_MODE"],
listeners: {
stdout: (data: Buffer) => {
stdOut += data.toString();
},
stderr: (data: Buffer) => {
stdErr += data.toString();
},
},
};
// Execute the wrangler command
await exec(`${packageManager.exec} wrangler ${command}`, args, options);
// Set the outputs for the command
setOutput("command-output", stdOut);
setOutput("command-stderr", stdErr);
// Handles setting github action outputs and creating github deployment and job summary
await handleCommandOutputParsing(config, command, stdOut);
}
} finally {
endGroup(config);
}
}
export {
authenticationSetup,
execCommands,
info,
installWrangler,
main,
uploadSecrets,
wranglerCommands,
};

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",
});
});
});
});
});

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;

View file

@ -15,6 +15,6 @@
"lib": ["ESNext"],
"types": ["node", "@cloudflare/workers-types"]
},
"exclude": ["node_modules", "**/*.test.ts"],
"exclude": ["node_modules"],
"include": ["src"]
}