Merge pull request #312 from cloudflare/maximo/unrevert-add-parity-with-wrangler-action

Unrevert add parity with wrangler action
This commit is contained in:
Maximo Guk 2024-11-01 15:40:25 -03:00 committed by GitHub
commit 4f4ff59d17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 277 additions and 30 deletions

View file

@ -0,0 +1,5 @@
---
"wrangler-action": minor
---
This reapplies [303](https://github.com/cloudflare/wrangler-action/pull/303) add parity with pages-action for pages deploy outputs. Thanks @courtney-sims! - Support pages-deployment-id, pages-environment, pages-deployment-alias-url and deployment-url outputs for Pages deploys when wrangler version is >=3.81.0. deployment-alias-url was also deprecated in favour of pages-deployment-alias.

View file

@ -262,7 +262,7 @@ Now when you run your workflow, you will see the full output of the Wrangler com
> Note: the `command-stderr` output variable is also available if you need to parse the standard error output of the Wrangler command. > Note: the `command-stderr` output variable is also available if you need to parse the standard error output of the Wrangler command.
### Using the `deployment-url` and `deployment-alias-url` Output Variables ### Using the `deployment-url` and `pages-deployment-alias-url` Output Variables
If you are executing a Wrangler command that results in either a Workers or Pages deployment, you can utilize the `deployment-url` output variable to get the URL of the deployment. For example, if you want to print the deployment URL after deploying your application, you can do the following: If you are executing a Wrangler command that results in either a Workers or Pages deployment, you can utilize the `deployment-url` output variable to get the URL of the deployment. For example, if you want to print the deployment URL after deploying your application, you can do the following:
@ -287,14 +287,14 @@ The resulting output will look something like this:
https://<your_pages_site>.pages.dev https://<your_pages_site>.pages.dev
``` ```
Pages deployments will also provide their alias URL (since Wrangler v3.78.0). You can use the `deployment-alias-url` output variable to get the URL of the deployment alias. This is useful for, for example, branch aliases for preview deployments. Pages deployments will also provide their alias URL (since Wrangler v3.78.0). You can use the `pages-deployment-alias-url` output variable to get the URL of the deployment alias. This is useful for, for example, branch aliases for preview deployments.
If the sample action above was used to deploy a branch other than main, you could use the following to get the branch URL: If the sample action above was used to deploy a branch other than main, you could use the following to get the branch URL:
```yaml ```yaml
- name: print deployment-alias-url - name: print pages-deployment-alias-url
env: env:
DEPLOYMENT_ALIAS_URL: ${{ steps.deploy.outputs.deployment-alias-url }} DEPLOYMENT_ALIAS_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
run: echo $DEPLOYMENT_ALIAS_URL run: echo $DEPLOYMENT_ALIAS_URL
``` ```

View file

@ -51,5 +51,9 @@ outputs:
description: "The error output of the Wrangler command (comes from stderr)" description: "The error output of the Wrangler command (comes from stderr)"
deployment-url: deployment-url:
description: "If the command was a Workers or Pages deployment, this will be the URL of the deployment" description: "If the command was a Workers or Pages deployment, this will be the URL of the deployment"
deployment-alias-url: pages-deployment-alias-url:
description: "If the command was a Workers or Pages deployment, this can be the URL of the deployment alias (if it exists) - needs wrangler >= 3.78.0" description: "If the command was a Pages deployment, this will be the URL of the deployment alias (if it exists) - needs wrangler >= 3.78.0"
pages-deployment-id:
description: "If the command was a Pages deployment, this will be the ID of the deployment - needs wrangler >= 3.81.0"
pages-environment:
description: "If the command was a Pages deployment, this will be the environment of the deployment - needs wrangler >= 3.81.0"

27
package-lock.json generated
View file

@ -1,16 +1,17 @@
{ {
"name": "wrangler-action", "name": "wrangler-action",
"version": "3.7.0", "version": "3.11.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "wrangler-action", "name": "wrangler-action",
"version": "3.7.0", "version": "3.11.0",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1" "@actions/exec": "^1.1.1",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@changesets/changelog-github": "^0.4.8", "@changesets/changelog-github": "^0.4.8",
@ -18,6 +19,7 @@
"@cloudflare/workers-types": "^4.20231121.0", "@cloudflare/workers-types": "^4.20231121.0",
"@types/node": "^20.10.4", "@types/node": "^20.10.4",
"@vercel/ncc": "^0.38.1", "@vercel/ncc": "^0.38.1",
"mock-fs": "^5.4.0",
"prettier": "^3.1.0", "prettier": "^3.1.0",
"semver": "^7.5.4", "semver": "^7.5.4",
"typescript": "^5.3.3", "typescript": "^5.3.3",
@ -3110,6 +3112,16 @@
"ufo": "^1.3.0" "ufo": "^1.3.0"
} }
}, },
"node_modules/mock-fs": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.4.0.tgz",
"integrity": "sha512-3ROPnEMgBOkusBMYQUW2rnT3wZwsgfOKzJDLvx/TZ7FL1WmWvwSwn3j4aDR5fLDGtgcc1WF0Z1y0di7c9L4FKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -5011,6 +5023,15 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }

View file

@ -30,7 +30,8 @@
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1" "@actions/exec": "^1.1.1",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@changesets/changelog-github": "^0.4.8", "@changesets/changelog-github": "^0.4.8",
@ -39,6 +40,7 @@
"@types/node": "^20.10.4", "@types/node": "^20.10.4",
"@vercel/ncc": "^0.38.1", "@vercel/ncc": "^0.38.1",
"prettier": "^3.1.0", "prettier": "^3.1.0",
"mock-fs": "^5.4.0",
"semver": "^7.5.4", "semver": "^7.5.4",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vitest": "^1.0.3" "vitest": "^1.0.3"

View file

@ -1,11 +1,11 @@
import { import {
debug,
getBooleanInput, getBooleanInput,
getInput, getInput,
getMultilineInput, getMultilineInput,
endGroup as originalEndGroup, endGroup as originalEndGroup,
error as originalError, error as originalError,
info as originalInfo, info as originalInfo,
debug,
startGroup as originalStartGroup, startGroup as originalStartGroup,
setFailed, setFailed,
setOutput, setOutput,
@ -13,10 +13,13 @@ import {
import { getExecOutput } from "@actions/exec"; import { getExecOutput } from "@actions/exec";
import semverEq from "semver/functions/eq"; import semverEq from "semver/functions/eq";
import { exec, execShell } from "./exec"; import { exec, execShell } from "./exec";
import { checkWorkingDirectory, semverCompare } from "./utils";
import { getPackageManager } from "./packageManagers"; import { getPackageManager } from "./packageManagers";
import { checkWorkingDirectory, semverCompare } from "./utils";
import { getDetailedPagesDeployOutput } from "./wranglerArtifactManager";
import { join } from "path";
import { tmpdir } from "os";
const DEFAULT_WRANGLER_VERSION = "3.78.10"; const DEFAULT_WRANGLER_VERSION = "3.81.0";
/** /**
* A configuration object that contains all the inputs & immutable state for the action. * A configuration object that contains all the inputs & immutable state for the action.
@ -33,6 +36,7 @@ const config = {
COMMANDS: getMultilineInput("command"), COMMANDS: getMultilineInput("command"),
QUIET_MODE: getBooleanInput("quiet"), QUIET_MODE: getBooleanInput("quiet"),
PACKAGE_MANAGER: getInput("packageManager"), PACKAGE_MANAGER: getInput("packageManager"),
WRANGLER_OUTPUT_DIR: `${join(tmpdir(), "wranglerArtifacts")}`,
} as const; } as const;
const packageManager = getPackageManager(config.PACKAGE_MANAGER, { const packageManager = getPackageManager(config.PACKAGE_MANAGER, {
@ -276,6 +280,29 @@ async function uploadSecrets() {
} }
} }
// fallback to trying to extract the deployment-url and pages-deployment-alias-url from stdout for wranglerVersion < 3.81.0
function extractDeploymentUrlsFromStdout(stdOut: string): {
deploymentUrl?: string;
aliasUrl?: string;
} {
let deploymentUrl = "";
let aliasUrl = "";
// Try to extract the deployment URL
const deploymentUrlMatch = stdOut.match(/https?:\/\/[a-zA-Z0-9-./]+/);
if (deploymentUrlMatch && deploymentUrlMatch[0]) {
deploymentUrl = deploymentUrlMatch[0].trim();
}
// And also try to extract the alias URL (since wrangler@3.78.0)
const aliasUrlMatch = stdOut.match(/alias URL: (https?:\/\/[a-zA-Z0-9-./]+)/);
if (aliasUrlMatch && aliasUrlMatch[1]) {
aliasUrl = aliasUrlMatch[1].trim();
}
return { deploymentUrl, aliasUrl };
}
async function wranglerCommands() { async function wranglerCommands() {
startGroup("🚀 Running Wrangler Commands"); startGroup("🚀 Running Wrangler Commands");
try { try {
@ -312,7 +339,9 @@ async function wranglerCommands() {
let stdOut = ""; let stdOut = "";
let stdErr = ""; let stdErr = "";
// Construct the options for the exec command // set WRANGLER_OUTPUT_FILE_DIRECTORY env for exec
process.env.WRANGLER_OUTPUT_FILE_DIRECTORY = config.WRANGLER_OUTPUT_DIR;
const options = { const options = {
cwd: config["workingDirectory"], cwd: config["workingDirectory"],
silent: config["QUIET_MODE"], silent: config["QUIET_MODE"],
@ -333,28 +362,43 @@ async function wranglerCommands() {
setOutput("command-output", stdOut); setOutput("command-output", stdOut);
setOutput("command-stderr", stdErr); setOutput("command-stderr", stdErr);
// Check if this command is a workers or pages deployment // 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 ( if (
command.startsWith("deploy") ||
command.startsWith("publish") ||
command.startsWith("pages publish") || command.startsWith("pages publish") ||
command.startsWith("pages deploy") command.startsWith("pages deploy")
) { ) {
// If this is a workers or pages deployment, try to extract the deployment URL const pagesArtifactFields = await getDetailedPagesDeployOutput(
let deploymentUrl = ""; config.WRANGLER_OUTPUT_DIR,
const deploymentUrlMatch = stdOut.match(/https?:\/\/[a-zA-Z0-9-./]+/);
if (deploymentUrlMatch && deploymentUrlMatch[0]) {
deploymentUrl = deploymentUrlMatch[0].trim();
setOutput("deployment-url", deploymentUrl);
}
// 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.length == 2 && aliasUrlMatch[1]) {
const aliasUrl = aliasUrlMatch[1].trim(); 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("deployment-alias-url", aliasUrl);
setOutput("pages-deployment-alias-url", aliasUrl);
} }
} }
} }

View file

@ -0,0 +1,85 @@
import mock from "mock-fs";
import { afterEach, describe, expect, it } from "vitest";
import {
getDetailedPagesDeployOutput,
getWranglerArtifacts,
} from "./wranglerArtifactManager";
afterEach(async () => {
mock.restore();
});
describe("wranglerArtifactsManager", () => {
describe("getWranglerArtifacts()", async () => {
it("Returns only wrangler output files from a given directory", 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", "environment":"production", "alias":"test.com", "deployment_id": "123", "url":"url.com"}`,
"not-wrangler-output.json": "test",
},
});
const artifacts = await getWranglerArtifacts("./testOutputDir");
expect(artifacts).toEqual([
"./testOutputDir/wrangler-output-2024-10-17_18-48-40_463-2e6e83.json",
]);
});
it("Returns an empty list when the output directory doesn't exist", async () => {
mock({
notTheDirWeWant: {},
});
const artifacts = await getWranglerArtifacts("./testOutputDir");
expect(artifacts).toEqual([]);
});
});
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({
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 artifacts = await getDetailedPagesDeployOutput("./testOutputDir");
expect(artifacts).toEqual({
version: 1,
type: "pages-deploy-detailed",
pages_project: "project",
url: "url.com",
environment: "production",
deployment_id: "123",
alias: "test.com",
});
});
});
});

View file

@ -0,0 +1,86 @@
import { access, open, readdir } from "fs/promises";
import { z } from "zod";
const OutputEntryBase = z.object({
version: z.literal(1),
type: z.string(),
});
const OutputEntryPagesDeployment = OutputEntryBase.merge(
z.object({
type: z.literal("pages-deploy-detailed"),
pages_project: z.string().nullable(),
deployment_id: z.string().nullable(),
url: z.string().optional(),
alias: z.string().optional(),
environment: z.enum(["production", "preview"]),
}),
);
type OutputEntryPagesDeployment = z.infer<typeof OutputEntryPagesDeployment>;
/**
* Parses file names in a directory to find wrangler artifact files
*
* @param artifactDirectory
* @returns All artifact files from the directory
*/
export async function getWranglerArtifacts(
artifactDirectory: string,
): Promise<string[]> {
try {
await access(artifactDirectory);
} catch {
return [];
}
// read files in asset directory
const dirent = await readdir(artifactDirectory, {
withFileTypes: true,
recursive: false,
});
// Match files to wrangler-output-<timestamp>-xxxxxx.json
const regex = new RegExp(
/^wrangler-output-[\d]{4}-[\d]{2}-[\d]{2}_[\d]{2}-[\d]{2}-[\d]{2}_[\d]{3}-[A-Fa-f0-9]{6}\.json$/,
);
const artifactFilePaths = dirent
.filter((d) => d.name.match(regex))
.map((d) => `${artifactDirectory}/${d.name}`);
return artifactFilePaths;
}
/**
* Searches for detailed wrangler output from a pages deploy
*
* @param artifactDirectory
* @returns The first pages-output-detailed found within a wrangler artifact directory
*/
export async function getDetailedPagesDeployOutput(
artifactDirectory: string,
): Promise<OutputEntryPagesDeployment | 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;
}
} catch (err) {
// If the line can't be parsed, skip it
continue;
}
}
await file.close();
}
return null;
}