Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parity with pages-action for Pages deploy outputs #303

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fast-experts-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler-action": minor
---

Support id, environment, url, and alias outputs for Pages deploys.
27 changes: 24 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
},
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1"
"@actions/exec": "^1.1.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@changesets/changelog-github": "^0.4.8",
Expand All @@ -39,6 +40,7 @@
"@types/node": "^20.10.4",
"@vercel/ncc": "^0.38.1",
"prettier": "^3.1.0",
"mock-fs": "^5.4.0",
Maximo-Guk marked this conversation as resolved.
Show resolved Hide resolved
"semver": "^7.5.4",
"typescript": "^5.3.3",
"vitest": "^1.0.3"
Expand Down
41 changes: 30 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import {
debug,
getBooleanInput,
getInput,
getMultilineInput,
endGroup as originalEndGroup,
error as originalError,
info as originalInfo,
debug,
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 { checkWorkingDirectory, semverCompare } from "./utils";
import { getPackageManager } from "./packageManagers";
import { checkWorkingDirectory, semverCompare } from "./utils";
import { getDetailedPagesDeployOutput } from "./wranglerArtifactManager";

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.
Expand Down Expand Up @@ -313,6 +314,9 @@ async function wranglerCommands() {
let stdErr = "";

// Construct the options for the exec command
const wranglerOutputDir = "/opt/wranglerArtifacts";
process.env.WRANGLER_OUTPUT_FILE_DIRECTORY = wranglerOutputDir;

const options = {
cwd: config["workingDirectory"],
silent: config["QUIET_MODE"],
Expand All @@ -333,14 +337,9 @@ async function wranglerCommands() {
setOutput("command-output", stdOut);
setOutput("command-stderr", stdErr);

// Check if this command is a workers or pages deployment
if (
command.startsWith("deploy") ||
command.startsWith("publish") ||
command.startsWith("pages publish") ||
command.startsWith("pages deploy")
) {
// If this is a workers or pages deployment, try to extract the deployment URL
// Check if this command is a workers deployment
if (command.startsWith("deploy") || command.startsWith("publish")) {
// Try to extract the deployment URL
let deploymentUrl = "";
const deploymentUrlMatch = stdOut.match(/https?:\/\/[a-zA-Z0-9-./]+/);
if (deploymentUrlMatch && deploymentUrlMatch[0]) {
Expand All @@ -357,6 +356,26 @@ async function wranglerCommands() {
setOutput("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(wranglerOutputDir);

if (pagesArtifactFields) {
Maximo-Guk marked this conversation as resolved.
Show resolved Hide resolved
setOutput("id", pagesArtifactFields.deployment_id);
setOutput("url", pagesArtifactFields.url);
// To ensure parity with pages-action, display url for alias if there is no alias
setOutput("alias", pagesArtifactFields.alias);
setOutput("environment", pagesArtifactFields.environment);
} else {
info(
"No fields available for output. Have you updated wrangler to version >=3.81.0?",
);
}
}
}
} finally {
endGroup();
Expand Down
87 changes: 87 additions & 0 deletions src/wranglerArtifactManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, expect, it } from "vitest";
import {
getDetailedPagesDeployOutput,
getWranglerArtifacts,
} from "./wranglerArtifactManager";

describe("wranglerArtifactsManager", () => {
const mock = require("mock-fs");
Copy link
Member

@Maximo-Guk Maximo-Guk Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably be using an es6 style import here


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",
]);
mock.restore();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably move mock.restore() in to an afterEach()

});
it("Returns an empty list when the output directory doesn't exist", async () => {
mock({
notTheDirWeWant: {},
});

const artifacts = await getWranglerArtifacts("./testOutputDir");
expect(artifacts).toEqual([]);
mock.restore();
});
});

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",
});
mock.restore();
}),
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",
});
mock.restore();
});
});
});
86 changes: 86 additions & 0 deletions src/wranglerArtifactManager.ts
Original file line number Diff line number Diff line change
@@ -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
Maximo-Guk marked this conversation as resolved.
Show resolved Hide resolved
continue;
}
}

await file.close();
}

return null;
}
Loading