diff --git a/package-lock.json b/package-lock.json index 1fcfcf9..39a3c4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.9", "@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", @@ -1375,6 +1376,15 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, + "node_modules/@types/mock-fs": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", + "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "22.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", diff --git a/package.json b/package.json index 7c876cd..ca5a9d0 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.9", "@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", diff --git a/src/service/github.spec.ts b/src/service/github.spec.ts new file mode 100644 index 0000000..1eaf439 --- /dev/null +++ b/src/service/github.spec.ts @@ -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 | + " + `); + }); +}); diff --git a/src/service/github.ts b/src/service/github.ts new file mode 100644 index 0000000..9ca4d96 --- /dev/null +++ b/src/service/github.ts @@ -0,0 +1,117 @@ +import { summary } from "@actions/core"; +import { context, getOctokit } from "@actions/github"; +import { env } from "process"; +import { WranglerActionConfig } from "../wranglerAction"; +import { OutputEntryPagesDeployment } from "../wranglerArtifactManager"; + +type Octokit = ReturnType; + +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) { + 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.project_name && + pagesArtifactFields.deployment_trigger && + pagesArtifactFields.stages + ) { + const octokit = getOctokit(config.GITHUB_TOKEN); + await Promise.all([ + createGitHubDeployment({ + config, + octokit, + deploymentUrl: pagesArtifactFields.url, + productionBranch: pagesArtifactFields.production_branch, + environment: pagesArtifactFields.environment, + deploymentId: pagesArtifactFields.deployment_id, + projectName: pagesArtifactFields.project_name, + }), + createJobSummary({ + commitHash: + pagesArtifactFields.deployment_trigger.metadata.commit_hash.substring( + 0, + 8, + ), + deploymentUrl: pagesArtifactFields.url, + aliasUrl: pagesArtifactFields.alias, + }), + ]); + } +} diff --git a/src/test/mocks.ts b/src/test/mocks.ts index bfa20e5..b64a696 100644 --- a/src/test/mocks.ts +++ b/src/test/mocks.ts @@ -1,4 +1,5 @@ import { http, HttpResponse } from "msw"; +import { z } from "zod"; export function mockGithubDeployments({ githubUser, @@ -15,6 +16,15 @@ export function mockGithubDeployments({ 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); }, diff --git a/src/wranglerAction.ts b/src/wranglerAction.ts index 23ec4ba..89557c5 100644 --- a/src/wranglerAction.ts +++ b/src/wranglerAction.ts @@ -15,8 +15,7 @@ import { exec, execShell } from "./exec"; import { PackageManager } from "./packageManagers"; import { semverCompare } from "./utils"; import { getDetailedPagesDeployOutput } from "./wranglerArtifactManager"; -import { createGitHubDeployment, createJobSummary } from "./service/github"; -import { getOctokit } from "@actions/github"; +import { createGitHubDeploymentAndJobSummary } from "./service/github"; export type WranglerActionConfig = z.infer; export const wranglerActionConfig = z.object({ @@ -423,36 +422,11 @@ async function wranglerCommands( setOutput("pages-deployment-alias-url", pagesArtifactFields.alias); setOutput("pages-deployment-id", pagesArtifactFields.deployment_id); setOutput("pages-environment", pagesArtifactFields.environment); - // create github deployment, if GITHUB_TOKEN is provided - if ( - config.GITHUB_TOKEN && - pagesArtifactFields.production_branch && - pagesArtifactFields.project_name && - pagesArtifactFields.deployment_trigger && - pagesArtifactFields.stages - ) { - const octokit = getOctokit(config.GITHUB_TOKEN); - await Promise.all([ - createGitHubDeployment({ - config, - octokit, - deploymentUrl: pagesArtifactFields.url, - productionBranch: pagesArtifactFields.production_branch, - environment: pagesArtifactFields.environment, - deploymentId: pagesArtifactFields.deployment_id, - projectName: pagesArtifactFields.project_name, - }), - createJobSummary({ - commitHash: - pagesArtifactFields.deployment_trigger.metadata.commit_hash.substring( - 0, - 8, - ), - deploymentUrl: pagesArtifactFields.url, - aliasUrl: pagesArtifactFields.alias, - }), - ]); - } + // Create github deployment, if GITHUB_TOKEN is present in config + await createGitHubDeploymentAndJobSummary( + config, + pagesArtifactFields, + ); } else { info( config, diff --git a/src/wranglerArtifactManager.test.ts b/src/wranglerArtifactManager.test.ts index 00a8840..cff15d9 100644 --- a/src/wranglerArtifactManager.test.ts +++ b/src/wranglerArtifactManager.test.ts @@ -1,17 +1,17 @@ -import mock from "mock-fs"; +import mockfs from "mock-fs"; import { afterEach, describe, expect, it } from "vitest"; import { getDetailedPagesDeployOutput, 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: {}, }); @@ -38,7 +38,7 @@ describe("wranglerArtifactsManager", () => { describe("getDetailedPagesDeployOutput()", async () => { it("Returns only detailed pages deploy output from wrangler artifacts", 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"} @@ -60,7 +60,7 @@ describe("wranglerArtifactsManager", () => { }); }), it("Skips artifact entries that are not parseable", async () => { - mock({ + mockfs({ testOutputDir: { "wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": ` this line is invalid json. diff --git a/src/wranglerArtifactManager.ts b/src/wranglerArtifactManager.ts index 46882d8..120b7b1 100644 --- a/src/wranglerArtifactManager.ts +++ b/src/wranglerArtifactManager.ts @@ -54,7 +54,7 @@ const OutputEntryPagesDeployment = OutputEntryBase.merge( }), ); -type OutputEntryPagesDeployment = z.infer; +export type OutputEntryPagesDeployment = z.infer; /** * Parses file names in a directory to find wrangler artifact files