Skip to content

Commit 3ec7f89

Browse files
committed
Add parity with pages-action for pages deploy outputs
1 parent cd8a317 commit 3ec7f89

File tree

6 files changed

+237
-15
lines changed

6 files changed

+237
-15
lines changed

.changeset/fast-experts-shop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler-action": minor
3+
---
4+
5+
Support id, environment, url, and alias outputs for Pages deploys.

package-lock.json

Lines changed: 24 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
},
3131
"dependencies": {
3232
"@actions/core": "^1.10.1",
33-
"@actions/exec": "^1.1.1"
33+
"@actions/exec": "^1.1.1",
34+
"zod": "^3.23.8"
3435
},
3536
"devDependencies": {
3637
"@changesets/changelog-github": "^0.4.8",
@@ -39,6 +40,7 @@
3940
"@types/node": "^20.10.4",
4041
"@vercel/ncc": "^0.38.1",
4142
"prettier": "^3.1.0",
43+
"mock-fs": "^5.4.0",
4244
"semver": "^7.5.4",
4345
"typescript": "^5.3.3",
4446
"vitest": "^1.0.3"

src/index.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
import {
2+
debug,
23
getBooleanInput,
34
getInput,
45
getMultilineInput,
56
endGroup as originalEndGroup,
67
error as originalError,
78
info as originalInfo,
8-
debug,
99
startGroup as originalStartGroup,
1010
setFailed,
1111
setOutput,
1212
} from "@actions/core";
1313
import { getExecOutput } from "@actions/exec";
1414
import semverEq from "semver/functions/eq";
1515
import { exec, execShell } from "./exec";
16-
import { checkWorkingDirectory, semverCompare } from "./utils";
1716
import { getPackageManager } from "./packageManagers";
17+
import { checkWorkingDirectory, semverCompare } from "./utils";
18+
import { getDetailedPagesDeployOutput } from "./wranglerArtifactManager";
1819

19-
const DEFAULT_WRANGLER_VERSION = "3.78.10";
20+
const DEFAULT_WRANGLER_VERSION = "3.81.0";
2021

2122
/**
2223
* A configuration object that contains all the inputs & immutable state for the action.
@@ -313,6 +314,9 @@ async function wranglerCommands() {
313314
let stdErr = "";
314315

315316
// Construct the options for the exec command
317+
const wranglerOutputDir = "/opt/wranglerArtifacts";
318+
process.env.WRANGLER_OUTPUT_FILE_DIRECTORY = wranglerOutputDir;
319+
316320
const options = {
317321
cwd: config["workingDirectory"],
318322
silent: config["QUIET_MODE"],
@@ -333,14 +337,9 @@ async function wranglerCommands() {
333337
setOutput("command-output", stdOut);
334338
setOutput("command-stderr", stdErr);
335339

336-
// Check if this command is a workers or pages deployment
337-
if (
338-
command.startsWith("deploy") ||
339-
command.startsWith("publish") ||
340-
command.startsWith("pages publish") ||
341-
command.startsWith("pages deploy")
342-
) {
343-
// If this is a workers or pages deployment, try to extract the deployment URL
340+
// Check if this command is a workers deployment
341+
if (command.startsWith("deploy") || command.startsWith("publish")) {
342+
// Try to extract the deployment URL
344343
let deploymentUrl = "";
345344
const deploymentUrlMatch = stdOut.match(/https?:\/\/[a-zA-Z0-9-./]+/);
346345
if (deploymentUrlMatch && deploymentUrlMatch[0]) {
@@ -357,6 +356,26 @@ async function wranglerCommands() {
357356
setOutput("deployment-alias-url", aliasUrl);
358357
}
359358
}
359+
// Check if this command is a pages deployment
360+
if (
361+
command.startsWith("pages publish") ||
362+
command.startsWith("pages deploy")
363+
) {
364+
const pagesArtifactFields =
365+
await getDetailedPagesDeployOutput(wranglerOutputDir);
366+
367+
if (pagesArtifactFields) {
368+
setOutput("id", pagesArtifactFields.deployment_id);
369+
setOutput("url", pagesArtifactFields.url);
370+
// To ensure parity with pages-action, display url for alias if there is no alias
371+
setOutput("alias", pagesArtifactFields.alias);
372+
setOutput("environment", pagesArtifactFields.environment);
373+
} else {
374+
info(
375+
"No fields available for output. Have you updated wrangler to version >=3.81.0?",
376+
);
377+
}
378+
}
360379
}
361380
} finally {
362381
endGroup();
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import mock from "mock-fs";
2+
import { afterEach, describe, expect, it } from "vitest";
3+
import {
4+
getDetailedPagesDeployOutput,
5+
getWranglerArtifacts,
6+
} from "./wranglerArtifactManager";
7+
8+
afterEach(async () => {
9+
mock.restore();
10+
});
11+
describe("wranglerArtifactsManager", () => {
12+
describe("getWranglerArtifacts()", async () => {
13+
it("Returns only wrangler output files from a given directory", async () => {
14+
mock({
15+
testOutputDir: {
16+
"wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": `
17+
{"version": 1, "type":"wrangler-session", "wrangler_version":"3.81.0", "command_line_args":["what's up"], "log_file_path": "/here"}
18+
{"version": 1, "type":"pages-deploy-detailed", "environment":"production", "alias":"test.com", "deployment_id": "123", "url":"url.com"}`,
19+
"not-wrangler-output.json": "test",
20+
},
21+
});
22+
23+
const artifacts = await getWranglerArtifacts("./testOutputDir");
24+
25+
expect(artifacts).toEqual([
26+
"./testOutputDir/wrangler-output-2024-10-17_18-48-40_463-2e6e83.json",
27+
]);
28+
//mock.restore();
29+
});
30+
it("Returns an empty list when the output directory doesn't exist", async () => {
31+
mock({
32+
notTheDirWeWant: {},
33+
});
34+
35+
const artifacts = await getWranglerArtifacts("./testOutputDir");
36+
expect(artifacts).toEqual([]);
37+
//mock.restore();
38+
});
39+
});
40+
41+
describe("getDetailedPagesDeployOutput()", async () => {
42+
it("Returns only detailed pages deploy output from wrangler artifacts", async () => {
43+
mock({
44+
testOutputDir: {
45+
"wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": `
46+
{"version": 1, "type":"wrangler-session", "wrangler_version":"3.81.0", "command_line_args":["what's up"], "log_file_path": "/here"}
47+
{"version": 1, "type":"pages-deploy-detailed", "pages_project": "project", "environment":"production", "alias":"test.com", "deployment_id": "123", "url":"url.com"}`,
48+
"not-wrangler-output.json": "test",
49+
},
50+
});
51+
52+
const artifacts = await getDetailedPagesDeployOutput("./testOutputDir");
53+
54+
expect(artifacts).toEqual({
55+
version: 1,
56+
pages_project: "project",
57+
type: "pages-deploy-detailed",
58+
url: "url.com",
59+
environment: "production",
60+
deployment_id: "123",
61+
alias: "test.com",
62+
});
63+
//mock.restore();
64+
}),
65+
it("Skips artifact entries that are not parseable", async () => {
66+
mock({
67+
testOutputDir: {
68+
"wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": `
69+
this line is invalid json.
70+
{"version": 1, "type":"pages-deploy-detailed", "pages_project": "project", "environment":"production", "alias":"test.com", "deployment_id": "123", "url":"url.com"}`,
71+
"not-wrangler-output.json": "test",
72+
},
73+
});
74+
75+
const artifacts = await getDetailedPagesDeployOutput("./testOutputDir");
76+
77+
expect(artifacts).toEqual({
78+
version: 1,
79+
type: "pages-deploy-detailed",
80+
pages_project: "project",
81+
url: "url.com",
82+
environment: "production",
83+
deployment_id: "123",
84+
alias: "test.com",
85+
});
86+
//mock.restore();
87+
});
88+
});
89+
});

src/wranglerArtifactManager.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { access, open, readdir } from "fs/promises";
2+
import { z } from "zod";
3+
4+
const OutputEntryBase = z.object({
5+
version: z.literal(1),
6+
type: z.string(),
7+
});
8+
9+
const OutputEntryPagesDeployment = OutputEntryBase.merge(
10+
z.object({
11+
type: z.literal("pages-deploy-detailed"),
12+
pages_project: z.string().nullable(),
13+
deployment_id: z.string().nullable(),
14+
url: z.string().optional(),
15+
alias: z.string().optional(),
16+
environment: z.enum(["production", "preview"]),
17+
}),
18+
);
19+
20+
type OutputEntryPagesDeployment = z.infer<typeof OutputEntryPagesDeployment>;
21+
22+
/**
23+
* Parses file names in a directory to find wrangler artifact files
24+
*
25+
* @param artifactDirectory
26+
* @returns All artifact files from the directory
27+
*/
28+
export async function getWranglerArtifacts(
29+
artifactDirectory: string,
30+
): Promise<string[]> {
31+
try {
32+
await access(artifactDirectory);
33+
} catch {
34+
return [];
35+
}
36+
37+
// read files in asset directory
38+
const dirent = await readdir(artifactDirectory, {
39+
withFileTypes: true,
40+
recursive: false,
41+
});
42+
43+
// Match files to wrangler-output-<timestamp>-xxxxxx.json
44+
const regex = new RegExp(
45+
/^wrangler-output-[\d]{4}-[\d]{2}-[\d]{2}_[\d]{2}-[\d]{2}-[\d]{2}_[\d]{3}-[A-Fa-f0-9]{6}\.json$/,
46+
);
47+
const artifactFilePaths = dirent
48+
.filter((d) => d.name.match(regex))
49+
.map((d) => `${artifactDirectory}/${d.name}`);
50+
51+
return artifactFilePaths;
52+
}
53+
54+
/**
55+
* Searches for detailed wrangler output from a pages deploy
56+
*
57+
* @param artifactDirectory
58+
* @returns The first pages-output-detailed found within a wrangler artifact directory
59+
*/
60+
export async function getDetailedPagesDeployOutput(
61+
artifactDirectory: string,
62+
): Promise<OutputEntryPagesDeployment | null> {
63+
const artifactFilePaths = await getWranglerArtifacts(artifactDirectory);
64+
65+
for (let i = 0; i < artifactFilePaths.length; i++) {
66+
const file = await open(artifactFilePaths[i], "r");
67+
68+
for await (const line of file.readLines()) {
69+
try {
70+
const output = JSON.parse(line);
71+
const parsedOutput = OutputEntryPagesDeployment.parse(output);
72+
if (parsedOutput.type === "pages-deploy-detailed") {
73+
// Assume, in the context of the action, the first detailed deploy instance seen will suffice
74+
return parsedOutput;
75+
}
76+
} catch (err) {
77+
// If the line can't be parsed, skip it
78+
continue;
79+
}
80+
}
81+
82+
await file.close();
83+
}
84+
85+
return null;
86+
}

0 commit comments

Comments
 (0)