From 15a73434529c25953cf4894e41ce76a53c6dee91 Mon Sep 17 00:00:00 2001 From: Sid Chatterjee Date: Mon, 4 Apr 2022 10:20:25 +0100 Subject: [PATCH 1/7] feat: Add wrangler pages deployment list to list deployments in a Cloudflare Pages project --- package-lock.json | 14 ++++++- packages/wrangler/package.json | 1 + packages/wrangler/src/index.tsx | 23 +---------- packages/wrangler/src/pages.tsx | 73 +++++++++++++++++++++++++++++++++ packages/wrangler/src/utils.ts | 23 +++++++++++ 5 files changed, 111 insertions(+), 23 deletions(-) create mode 100644 packages/wrangler/src/utils.ts diff --git a/package-lock.json b/package-lock.json index 6b42b652c55b..54bde5787454 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16129,6 +16129,11 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==" }, + "node_modules/timeago.js": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", + "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==" + }, "node_modules/tmp": { "version": "0.0.33", "license": "MIT", @@ -17298,6 +17303,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": { @@ -28232,6 +28238,11 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==" }, + "timeago.js": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", + "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==" + }, "tmp": { "version": "0.0.33", "requires": { @@ -28830,10 +28841,11 @@ "prompts": "^2.4.2", "react": "^17.0.2", "react-error-boundary": "^3.1.4", - "selfsigned": "*", + "selfsigned": "^2.0.0", "semiver": "^1.1.0", "serve-static": "^1.14.1", "signal-exit": "^3.0.6", + "timeago.js": "^4.0.2", "tmp-promise": "^3.0.3", "undici": "4.13.0", "ws": "^8.3.0", diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 887ddbe957cc..eeed2120713d 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/index.tsx b/packages/wrangler/src/index.tsx index ede2d39a41f8..076e75521423 100644 --- a/packages/wrangler/src/index.tsx +++ b/packages/wrangler/src/index.tsx @@ -46,11 +46,10 @@ import { logout, listScopes, initialise as initialiseUserConfig, - loginOrRefreshIfRequired, - getAccountId, validateScopeKeys, } from "./user"; import { whoami } from "./whoami"; +import { requireAuth } from "./utils"; import type { Config } from "./config"; import type { TailCLIFilters } from "./tail"; @@ -103,26 +102,6 @@ function getScriptName( : shortScriptName; } -/** - * Ensure that a user is logged in, and a valid account_id is available. - */ -async function requireAuth( - config: Config, - isInteractive = true -): Promise { - const loggedIn = await loginOrRefreshIfRequired(isInteractive); - if (!loggedIn) { - // didn't login, let's just quit - throw new Error("Did not login, quitting..."); - } - const accountId = config.account_id || (await getAccountId(isInteractive)); - if (!accountId) { - throw new Error("No account id found, quitting..."); - } - - return accountId; -} - /** * Get a promise to the streamed input from stdin. * diff --git a/packages/wrangler/src/pages.tsx b/packages/wrangler/src/pages.tsx index 4731e023cb42..b267baf35570 100644 --- a/packages/wrangler/src/pages.tsx +++ b/packages/wrangler/src/pages.tsx @@ -17,6 +17,15 @@ import type { Headers, Request, fetch } from "@miniflare/core"; import type { BuildResult } from "esbuild"; import type { MiniflareOptions } from "miniflare"; import type { BuilderCallback } from "yargs"; +import { fetchResult } from "./cfetch"; +import React from "react"; +import { render } from "ink"; +import Table from "ink-table"; +import { requireAuth } from "./utils"; +import { readConfig } from "./config"; +import { format } from "timeago.js"; + +type ConfigPath = string | undefined; // Defer importing miniflare until we really need it. This takes ~0.5s // and also modifies some `stream/web` and `undici` prototypes, so we @@ -1035,6 +1044,70 @@ export const pages: BuilderCallback = (yargs) => { }); } ) + ) + .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); + const accountId = await requireAuth(config); + + type Deployment = { + environment: string; + deployment_trigger: { + metadata: { + commit_hash: string; + }; + }; + url: string; + latest_stage: { + status: string; + ended_on: string; + }; + project_name: string; + }; + + 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), + Source: shortSha( + deployment.deployment_trigger.metadata.commit_hash + ), + Deployment: deployment.url, + Status: getStatus(deployment), + Build: `https://dash.cloudflare.com/${accountId}/pages/view/${deployment.project_name}/${deployment.id}`, + }; + }); + return render(
); + } + ) ); }; diff --git a/packages/wrangler/src/utils.ts b/packages/wrangler/src/utils.ts new file mode 100644 index 000000000000..ba9c1933852d --- /dev/null +++ b/packages/wrangler/src/utils.ts @@ -0,0 +1,23 @@ +import { loginOrRefreshIfRequired, getAccountId } from "./user"; + +import type { Config } from "./config"; + +/** + * Ensure that a user is logged in, and a valid account_id is available. + */ +export async function requireAuth( + config: Config, + isInteractive = true +): Promise { + const loggedIn = await loginOrRefreshIfRequired(isInteractive); + if (!loggedIn) { + // didn't login, let's just quit + throw new Error("Did not login, quitting..."); + } + const accountId = config.account_id || (await getAccountId(isInteractive)); + if (!accountId) { + throw new Error("No account id found, quitting..."); + } + + return accountId; +} From 9242746651656307b307bfe773fcdef0d595790f Mon Sep 17 00:00:00 2001 From: Sid Chatterjee Date: Mon, 4 Apr 2022 10:22:36 +0100 Subject: [PATCH 2/7] Add changeset --- .changeset/thin-paws-dance.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/thin-paws-dance.md 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 From 6c979ffb831f696925f4955b70c8a1fd21464bee Mon Sep 17 00:00:00 2001 From: Sid Chatterjee Date: Wed, 6 Apr 2022 13:06:07 +0100 Subject: [PATCH 3/7] Add branch --- packages/wrangler/src/pages.tsx | 37 ++++++++++++++++++--------------- packages/wrangler/src/utils.ts | 23 -------------------- 2 files changed, 20 insertions(+), 40 deletions(-) delete mode 100644 packages/wrangler/src/utils.ts diff --git a/packages/wrangler/src/pages.tsx b/packages/wrangler/src/pages.tsx index a3e7e68895d4..353411d5c1bf 100644 --- a/packages/wrangler/src/pages.tsx +++ b/packages/wrangler/src/pages.tsx @@ -39,6 +39,22 @@ export type Project = { }; }; +export type Deployment = { + 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. @@ -1117,24 +1133,9 @@ export const pages: BuilderCallback = (yargs) => { }, }), async (args) => { - const config = readConfig(args.config as ConfigPath); + const config = readConfig(args.config as ConfigPath, args); const accountId = await requireAuth(config); - type Deployment = { - environment: string; - deployment_trigger: { - metadata: { - commit_hash: string; - }; - }; - url: string; - latest_stage: { - status: string; - ended_on: string; - }; - project_name: string; - }; - const deployments: Array = await fetchResult( `/accounts/${accountId}/pages/projects/${args.project}/deployments` ); @@ -1155,15 +1156,17 @@ export const pages: BuilderCallback = (yargs) => { 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}`, }; }); - return render(
); + render(
); } ) ); diff --git a/packages/wrangler/src/utils.ts b/packages/wrangler/src/utils.ts deleted file mode 100644 index ba9c1933852d..000000000000 --- a/packages/wrangler/src/utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { loginOrRefreshIfRequired, getAccountId } from "./user"; - -import type { Config } from "./config"; - -/** - * Ensure that a user is logged in, and a valid account_id is available. - */ -export async function requireAuth( - config: Config, - isInteractive = true -): Promise { - const loggedIn = await loginOrRefreshIfRequired(isInteractive); - if (!loggedIn) { - // didn't login, let's just quit - throw new Error("Did not login, quitting..."); - } - const accountId = config.account_id || (await getAccountId(isInteractive)); - if (!accountId) { - throw new Error("No account id found, quitting..."); - } - - return accountId; -} From cc60f3c0b9514f45b2b637df4c1cc078ce922b78 Mon Sep 17 00:00:00 2001 From: Sid Chatterjee Date: Wed, 6 Apr 2022 13:07:41 +0100 Subject: [PATCH 4/7] Add id to Deployment type --- packages/wrangler/src/pages.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wrangler/src/pages.tsx b/packages/wrangler/src/pages.tsx index 353411d5c1bf..07b077536bdb 100644 --- a/packages/wrangler/src/pages.tsx +++ b/packages/wrangler/src/pages.tsx @@ -40,6 +40,7 @@ export type Project = { }; export type Deployment = { + id: string; environment: string; deployment_trigger: { metadata: { From 138977f12f9f9f4817d88991a9c76f0781d5a805 Mon Sep 17 00:00:00 2001 From: Sid Chatterjee Date: Wed, 6 Apr 2022 13:17:09 +0100 Subject: [PATCH 5/7] Add a test for deployment list --- packages/wrangler/src/__tests__/pages.test.ts | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/wrangler/src/__tests__/pages.test.ts b/packages/wrangler/src/__tests__/pages.test.ts index 3d8202790865..ab343c96d240 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 deployments list --project=images"); + + expect(requests.count).toBe(1); + }); + }); }); From df614e8b8e50fca1c7c2a6ec0d0f1244ff099dda Mon Sep 17 00:00:00 2001 From: Sid Chatterjee Date: Wed, 6 Apr 2022 13:18:30 +0100 Subject: [PATCH 6/7] Remove timeago.js from project root --- package-lock.json | 3 +-- package.json | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6cce7ce5922..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": { @@ -29084,8 +29083,8 @@ "semiver": "^1.1.0", "serve-static": "^1.14.1", "signal-exit": "^3.0.6", - "timeago.js": "^4.0.2", "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": { From 40727cc6a7b61690f268d97c3840d81d41f3fb60 Mon Sep 17 00:00:00 2001 From: Sid Chatterjee Date: Wed, 6 Apr 2022 13:23:40 +0100 Subject: [PATCH 7/7] Fix test --- packages/wrangler/src/__tests__/pages.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wrangler/src/__tests__/pages.test.ts b/packages/wrangler/src/__tests__/pages.test.ts index ab343c96d240..811b27b0e28e 100644 --- a/packages/wrangler/src/__tests__/pages.test.ts +++ b/packages/wrangler/src/__tests__/pages.test.ts @@ -172,7 +172,7 @@ describe("subcommand implicit help ran on incomplete command execution", () => { ]; const requests = mockListRequest(deployments); - await runWrangler("pages deployments list --project=images"); + await runWrangler("pages deployment list --project=images"); expect(requests.count).toBe(1); });