diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c63c9ac20c3..6eff2acf1021 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -235,6 +235,38 @@ When you open a PR to the `workers-sdk` repo, you should expect several checks t A summary of this repositories actions can be found [here](.github/workflows/README.md) +## Running e2e tests locally + +To run the e2e tests locally, you'll need a Cloudflare API Token and run: + +```sh +$ WRANGLER="node ~/path/to/workers-sdk/packages/wrangler/wrangler-dist/cli.js" CLOUDFLARE_ACCOUNT_ID=$CLOUDFLARE_TESTING_ACCOUNT_ID CLOUDFLARE_API_TOKEN=$CLOUDFLARE_TESTING_API_TOKEN pnpm run test:e2e +``` + +You may optionally want to append a filename pattern to limit which e2e tests are run. Also you may want to set `--bail=n` to limit the number of fails tests to show the error before the rest of the tests finish running and to limit the noise in that output: + +```sh +$ WRANGLER="node ~/path/to/workers-sdk/packages/wrangler/wrangler-dist/cli.js" CLOUDFLARE_ACCOUNT_ID=$CLOUDFLARE_TESTING_ACCOUNT_ID CLOUDFLARE_API_TOKEN=$CLOUDFLARE_TESTING_API_TOKEN pnpm run test:e2e [file-pattern] --bail=1 +``` + +### Creating an API Token + +1. Go to ["My Profile" > "User API Tokens"](https://dash.cloudflare.com/profile/api-tokens) +1. Click "Create API Token" +1. Use the "Edit Cloudflare Workers" template +1. Set "Account Resources" to "Include" "DevProd Testing" (you can use any account you have access to) +1. Set "Zone Resources" to "All zones from an account" and the same account as above +1. Click "Continue to summary" +1. Verify your token works by running the curl command provided +1. Set the environment variables in your terminal or in your profile file (e.g. ~/.zshrc, ~/.bashrc, ~/.profile, etc): + +```sh +export CLOUDFLARE_TESTING_ACCOUNT_ID="" +export CLOUDFLARE_TESTING_API_TOKEN="" +``` + +Note: Workers created in the e2e tests that fail might not always be cleaned up (deleted). Internal users with access to the "DevProd Testing" account can rely on an automated job to clean up the Workers based on the format of the name. If you use another account, please be aware you may want to manually delete the Workers yourself. + ## Changesets Every non-trivial change to the project - those that should appear in the changelog - must be captured in a "changeset". diff --git a/packages/cli/interactive.ts b/packages/cli/interactive.ts index 3c4508c1c35a..4daeeea1a39d 100644 --- a/packages/cli/interactive.ts +++ b/packages/cli/interactive.ts @@ -611,7 +611,9 @@ export const spinner = ( } clearLoop(); } else { - logUpdate(`\n${grayBar} ${msg}`); + if (msg) { + logUpdate(`\n${grayBar} ${msg}`); + } newline(); } }, diff --git a/packages/wrangler/e2e/versions.test.ts b/packages/wrangler/e2e/versions.test.ts new file mode 100644 index 000000000000..7d864b174c77 --- /dev/null +++ b/packages/wrangler/e2e/versions.test.ts @@ -0,0 +1,296 @@ +import crypto from "node:crypto"; +import path from "node:path"; +import shellac from "shellac"; +import { beforeAll, chai, describe, expect, it } from "vitest"; +import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id"; +import { normalizeOutput } from "./helpers/normalize"; +import { dedent, makeRoot, seed } from "./helpers/setup"; +import { WRANGLER } from "./helpers/wrangler-command"; + +chai.config.truncateThreshold = 1e6; + +function matchWhoamiEmail(stdout: string): string { + return stdout.match(/associated with the email (.+?@.+?)!/)?.[1] as string; +} +function matchVersionId(stdout: string): string { + return stdout.match(/Version ID:\s+([a-f\d-]+)/)?.[1] as string; +} + +describe("versions deploy", () => { + let root: string; + let workerName: string; + let workerPath: string; + let runInRoot: typeof shellac; + let runInWorker: typeof shellac; + let normalize: (str: string) => string; + let versionId1: string; + let versionId2: string; + + beforeAll(async () => { + root = await makeRoot(); + workerName = `tmp-e2e-wrangler-${crypto.randomBytes(4).toString("hex")}`; + workerPath = path.join(root, workerName); + runInRoot = shellac.in(root).env(process.env); + runInWorker = shellac.in(workerPath).env(process.env); + const email = matchWhoamiEmail( + (await runInRoot`$ ${WRANGLER} whoami`).stdout + ); + normalize = (str) => + normalizeOutput(str, { + [workerName]: "tmp-e2e-wrangler", + [email]: "person@example.com", + [CLOUDFLARE_ACCOUNT_ID]: "CLOUDFLARE_ACCOUNT_ID", + }); + }, 50_000); + + it("init worker", async () => { + const init = + await runInRoot`$ ${WRANGLER} init --yes --no-delegate-c3 ${workerName}`; + + expect(normalize(init.stdout)).toContain( + "To publish your Worker to the Internet, run `npm run deploy`" + ); + + // TEMP: wrangler deploy needed for the first time to *create* the worker (will create 1 extra version + deployment in snapshots below) + await runInWorker`$ ${WRANGLER} deploy`; + }); + + it("upload worker version (with message and tag)", async () => { + const upload = + await runInWorker`$ ${WRANGLER} versions upload --message "Upload via e2e test" --tag "e2e-upload" --x-versions`; + + versionId1 = matchVersionId(upload.stdout); + + expect(normalize(upload.stdout)).toMatchInlineSnapshot(` + "Total Upload: xx KiB / gzip: xx KiB + Worker Version ID: 00000000-0000-0000-0000-000000000000 + Uploaded tmp-e2e-wrangler (TIMINGS)" + `); + }); + + it("list 1 version", async () => { + const list = await runInWorker`$ ${WRANGLER} versions list --x-versions`; + + expect(normalize(list.stdout)).toMatchInlineSnapshot(` + "Version ID: 00000000-0000-0000-0000-000000000000 + Created: TIMESTAMP + Author: person@example.com + Source: Unknown (version_upload) + Tag: e2e-upload + Message: Upload via e2e test + Version ID: 00000000-0000-0000-0000-000000000000 + Created: TIMESTAMP + Author: person@example.com + Source: Upload + Tag: - + Message: -" + `); + + expect(list.stdout).toMatch(/Message:\s+Upload via e2e test/); + expect(list.stdout).toMatch(/Tag:\s+e2e-upload/); + }); + + it("deploy worker version", async () => { + const deploy = + await runInWorker`$ ${WRANGLER} versions deploy ${versionId1}@100% --message "Deploy via e2e test" --yes --x-versions`; + + expect(normalize(deploy.stdout)).toMatchInlineSnapshot(` + "╭ Deploy Worker Versions by splitting traffic between multiple versions + │ + ├ Fetching latest deployment + │ + ├ Your current deployment has 1 version(s): + │ + │ (100%) 00000000-0000-0000-0000-000000000000 + │ Created: TIMESTAMP + │ Tag: - + │ Message: - + │ + ├ Fetching deployable versions + │ + ├ Which version(s) do you want to deploy? + ├ 1 Worker Version(s) selected + │ + ├ Worker Version 1: 00000000-0000-0000-0000-000000000000 + │ Created: TIMESTAMP + │ Tag: e2e-upload + │ Message: Upload via e2e test + │ + ├ What percentage of traffic should Worker Version 1 receive? + ├ 100% of traffic + ├ + ├ Add a deployment message + │ Deployment message Deploy via e2e test + │ + ├ Deploying 1 version(s) + │ + │ + ╰ SUCCESS Deployed tmp-e2e-wrangler version 00000000-0000-0000-0000-000000000000 at 100% (TIMINGS)" + `); + }); + + it("list 1 deployment", async () => { + const list = + await runInWorker`$ ${WRANGLER} deployments list --x-versions`; + + expect(normalize(list.stdout)).toMatchInlineSnapshot(` + "Created: TIMESTAMP + Author: person@example.com + Source: Unknown (deployment) + Message: Deploy via e2e test + Version(s): (100%) 00000000-0000-0000-0000-000000000000 + Created: TIMESTAMP + Tag: e2e-upload + Message: Upload via e2e test + Created: TIMESTAMP + Author: person@example.com + Source: Upload + Message: Automatic deployment on upload. + Version(s): (100%) 00000000-0000-0000-0000-000000000000 + Created: TIMESTAMP + Tag: - + Message: -" + `); + expect(list.stderr).toMatchInlineSnapshot('""'); + + expect(list.stdout).toContain(versionId1); + }); + + it("modify & upload worker version", async () => { + await seed(workerPath, { + "src/index.ts": dedent` + export default { + fetch(request) { + return new Response("Hello World AGAIN!") + } + }`, + }); + + const upload = + await runInWorker`$ ${WRANGLER} versions upload --message "Upload AGAIN via e2e test" --tag "e2e-upload-AGAIN" --x-versions`; + + versionId2 = matchVersionId(upload.stdout); + + expect(normalize(upload.stdout)).toMatchInlineSnapshot(` + "Total Upload: xx KiB / gzip: xx KiB + Worker Version ID: 00000000-0000-0000-0000-000000000000 + Uploaded tmp-e2e-wrangler (TIMINGS)" + `); + }); + + it("list 2 versions", async () => { + const list = await runInWorker`$ ${WRANGLER} versions list --x-versions`; + + expect(normalize(list.stdout)).toMatchInlineSnapshot(` + "Version ID: 00000000-0000-0000-0000-000000000000 + Created: TIMESTAMP + Author: person@example.com + Source: Unknown (version_upload) + Tag: e2e-upload-AGAIN + Message: Upload AGAIN via e2e test + Version ID: 00000000-0000-0000-0000-000000000000 + Created: TIMESTAMP + Author: person@example.com + Source: Unknown (version_upload) + Tag: e2e-upload + Message: Upload via e2e test + Version ID: 00000000-0000-0000-0000-000000000000 + Created: TIMESTAMP + Author: person@example.com + Source: Upload + Tag: - + Message: -" + `); + + expect(list.stdout).toMatch(/Message:\s+Upload AGAIN via e2e test/); + expect(list.stdout).toMatch(/Tag:\s+e2e-upload-AGAIN/); + }); + + it("deploy worker version", async () => { + const deploy = + await runInWorker`$ ${WRANGLER} versions deploy ${versionId2}@100% --message "Deploy AGAIN via e2e test" --yes --x-versions`; + + expect(normalize(deploy.stdout)).toMatchInlineSnapshot(` + "╭ Deploy Worker Versions by splitting traffic between multiple versions + │ + ├ Fetching latest deployment + │ + ├ Your current deployment has 1 version(s): + │ + │ (100%) 00000000-0000-0000-0000-000000000000 + │ Created: TIMESTAMP + │ Tag: e2e-upload + │ Message: Upload via e2e test + │ + ├ Fetching deployable versions + │ + ├ Which version(s) do you want to deploy? + ├ 1 Worker Version(s) selected + │ + ├ Worker Version 1: 00000000-0000-0000-0000-000000000000 + │ Created: TIMESTAMP + │ Tag: e2e-upload-AGAIN + │ Message: Upload AGAIN via e2e test + │ + ├ What percentage of traffic should Worker Version 1 receive? + ├ 100% of traffic + ├ + ├ Add a deployment message + │ Deployment message Deploy AGAIN via e2e test + │ + ├ Deploying 1 version(s) + │ + │ + ╰ SUCCESS Deployed tmp-e2e-wrangler version 00000000-0000-0000-0000-000000000000 at 100% (TIMINGS)" + `); + }); + + it("list 2 deployments", async () => { + const list = + await runInWorker`$ ${WRANGLER} deployments list --x-versions`; + + expect(normalize(list.stdout)).toMatchInlineSnapshot(` + "Created: TIMESTAMP + Author: person@example.com + Source: Unknown (deployment) + Message: Deploy AGAIN via e2e test + Version(s): (100%) 00000000-0000-0000-0000-000000000000 + Created: TIMESTAMP + Tag: e2e-upload-AGAIN + Message: Upload AGAIN via e2e test + Created: TIMESTAMP + Author: person@example.com + Source: Unknown (deployment) + Message: Deploy via e2e test + Version(s): (100%) 00000000-0000-0000-0000-000000000000 + Created: TIMESTAMP + Tag: e2e-upload + Message: Upload via e2e test + Created: TIMESTAMP + Author: person@example.com + Source: Upload + Message: Automatic deployment on upload. + Version(s): (100%) 00000000-0000-0000-0000-000000000000 + Created: TIMESTAMP + Tag: - + Message: -" + `); + expect(list.stderr).toMatchInlineSnapshot('""'); + + expect(list.stdout).toContain(versionId1); + expect(list.stdout).toContain(versionId2); + }); + + // TODO: rollback, when supported for --x-versions + + it("delete worker", async () => { + const { stdout, stderr } = await runInWorker`$ ${WRANGLER} delete`; + + expect(normalize(stdout)).toMatchInlineSnapshot(` + "? Are you sure you want to delete tmp-e2e-wrangler? This action cannot be undone. + 🤖 Using fallback value in non-interactive context: yes + Successfully deleted tmp-e2e-wrangler" + `); + expect(stderr).toMatchInlineSnapshot('""'); + }); +});