diff --git a/.changeset/thin-paws-dance.md b/.changeset/thin-paws-dance.md new file mode 100644 index 000000000000..47ae1388da4b --- /dev/null +++ b/.changeset/thin-paws-dance.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +feature: Add wrangler pages deployment list + +Renders a list of deployments in a Cloudflare Pages project diff --git a/package-lock.json b/package-lock.json index e55c3fcff94a..bbdf93ff35ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "jest": "^27.5.1", "npm-run-all": "^4.1.5", "prettier": "^2.5.1", - "timeago.js": "^4.0.2", "typescript": "^4.6.2" }, "engines": { @@ -17457,6 +17456,7 @@ "path-to-regexp": "^6.2.0", "selfsigned": "^2.0.0", "semiver": "^1.1.0", + "timeago.js": "^4.0.2", "xxhash-wasm": "^1.0.1" }, "bin": { @@ -29084,6 +29084,7 @@ "serve-static": "^1.14.1", "signal-exit": "^3.0.6", "supports-color": "^9.2.1", + "timeago.js": "^4.0.2", "tmp-promise": "^3.0.3", "undici": "^4.15.1", "ws": "^8.3.0", diff --git a/package.json b/package.json index 466f4ab92d30..26032a7c65f6 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "jest": "^27.5.1", "npm-run-all": "^4.1.5", "prettier": "^2.5.1", - "timeago.js": "^4.0.2", "typescript": "^4.6.2" }, "scripts": { diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index c6a3426e9a4a..0e3044461ce4 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -41,6 +41,7 @@ "path-to-regexp": "^6.2.0", "selfsigned": "^2.0.0", "semiver": "^1.1.0", + "timeago.js": "^4.0.2", "xxhash-wasm": "^1.0.1" }, "optionalDependencies": { diff --git a/packages/wrangler/src/__tests__/pages.test.ts b/packages/wrangler/src/__tests__/pages.test.ts index 3d8202790865..811b27b0e28e 100644 --- a/packages/wrangler/src/__tests__/pages.test.ts +++ b/packages/wrangler/src/__tests__/pages.test.ts @@ -3,7 +3,7 @@ import { setMockResponse, unsetAllMocks } from "./helpers/mock-cfetch"; import { mockConsoleMethods } from "./helpers/mock-console"; import { runInTempDir } from "./helpers/run-in-tmp"; import { runWrangler } from "./helpers/run-wrangler"; -import type { Project } from "../pages"; +import type { Project, Deployment } from "../pages"; describe("subcommand implicit help ran on incomplete command execution", () => { runInTempDir(); @@ -129,4 +129,52 @@ describe("subcommand implicit help ran on incomplete command execution", () => { expect(requests.count).toEqual(2); }); }); + + describe("deployment list", () => { + mockAccountId(); + mockApiToken(); + + afterEach(() => { + unsetAllMocks(); + }); + function mockListRequest(deployments: unknown[]) { + const requests = { count: 0 }; + setMockResponse( + "/accounts/:accountId/pages/projects/:project/deployments", + ([_url, accountId, project]) => { + requests.count++; + expect(project).toEqual("images"); + expect(accountId).toEqual("some-account-id"); + return deployments; + } + ); + return requests; + } + + it("should make request to list deployments", async () => { + const deployments: Deployment[] = [ + { + id: "87bbc8fe-16be-45cd-81e0-63d722e82cdf", + url: "https://87bbc8fe.images.pages.dev", + environment: "preview", + latest_stage: { + ended_on: "2021-11-17T14:52:26.133835Z", + status: "success", + }, + deployment_trigger: { + metadata: { + branch: "main", + commit_hash: "c7649364c4cb32ad4f65b530b9424e8be5bec9d6", + }, + }, + project_name: "images", + }, + ]; + + const requests = mockListRequest(deployments); + await runWrangler("pages deployment list --project=images"); + + expect(requests.count).toBe(1); + }); + }); }); diff --git a/packages/wrangler/src/pages.tsx b/packages/wrangler/src/pages.tsx index ace4df8f13d9..07b077536bdb 100644 --- a/packages/wrangler/src/pages.tsx +++ b/packages/wrangler/src/pages.tsx @@ -39,6 +39,23 @@ export type Project = { }; }; +export type Deployment = { + id: string; + environment: string; + deployment_trigger: { + metadata: { + commit_hash: string; + branch: string; + }; + }; + url: string; + latest_stage: { + status: string; + ended_on: string; + }; + project_name: string; +}; + // Defer importing miniflare until we really need it. This takes ~0.5s // and also modifies some `stream/web` and `undici` prototypes, so we // don't want to do this if pages commands aren't being called. @@ -1102,6 +1119,57 @@ export const pages: BuilderCallback = (yargs) => { render(
); } ) + ) + .command("deployment", false, (yargs) => + yargs.command( + "list", + "List deployments in your Cloudflare Pages project", + (yargs) => + yargs.options({ + project: { + type: "string", + demandOption: true, + description: + "The name of the project you would like to list deployments for", + }, + }), + async (args) => { + const config = readConfig(args.config as ConfigPath, args); + const accountId = await requireAuth(config); + + const deployments: Array = await fetchResult( + `/accounts/${accountId}/pages/projects/${args.project}/deployments` + ); + + const titleCase = (word: string) => + word.charAt(0).toUpperCase() + word.slice(1); + + const shortSha = (sha: string) => sha.slice(0, 7); + + const getStatus = (deployment: Deployment) => { + // Return a pretty time since timestamp if successful otherwise the status + if (deployment.latest_stage.status === `success`) { + return format(deployment.latest_stage.ended_on); + } + return titleCase(deployment.latest_stage.status); + }; + + const data = deployments.map((deployment) => { + return { + Environment: titleCase(deployment.environment), + Branch: deployment.deployment_trigger.metadata.branch, + Source: shortSha( + deployment.deployment_trigger.metadata.commit_hash + ), + Deployment: deployment.url, + Status: getStatus(deployment), + // TODO: Use a url shortener + Build: `https://dash.cloudflare.com/${accountId}/pages/view/${deployment.project_name}/${deployment.id}`, + }; + }); + render(
); + } + ) ); };