diff --git a/.changeset/preview-pull-request-title.md b/.changeset/preview-pull-request-title.md new file mode 100644 index 0000000000..9567de0d18 --- /dev/null +++ b/.changeset/preview-pull-request-title.md @@ -0,0 +1,10 @@ +--- +"@cloudflare/deploy-helpers": minor +"wrangler": minor +--- + +Add pull request title to `wrangler preview` deployment annotations + +`wrangler preview` now also detects the title of the pull/merge request associated with the current CI run (GitHub Actions and GitLab CI, plus a generic `PULL_REQUEST_TITLE` fallback) and attaches it to the preview deployment as the `workers/pull_request_title` annotation, alongside the existing pull request number/URL, repository URL, and commit SHA annotations. + +This is best effort: if no pull request title can be detected, nothing changes. diff --git a/packages/deploy-helpers/src/preview/api.ts b/packages/deploy-helpers/src/preview/api.ts index e59f61434c..038f0c6bcf 100644 --- a/packages/deploy-helpers/src/preview/api.ts +++ b/packages/deploy-helpers/src/preview/api.ts @@ -83,6 +83,7 @@ export interface DeploymentResource { "workers/commit_sha"?: string; "workers/message"?: string; "workers/pull_request_number"?: string; + "workers/pull_request_title"?: string; "workers/pull_request_url"?: string; "workers/repository_url"?: string; "workers/tag"?: string; @@ -112,6 +113,7 @@ export type CreatePreviewDeploymentRequestParams = { "workers/commit_sha"?: string; "workers/message"?: string; "workers/pull_request_number"?: string; + "workers/pull_request_title"?: string; "workers/pull_request_url"?: string; "workers/repository_url"?: string; "workers/tag"?: string; diff --git a/packages/deploy-helpers/src/preview/preview.ts b/packages/deploy-helpers/src/preview/preview.ts index 973b0dd272..696f49df60 100644 --- a/packages/deploy-helpers/src/preview/preview.ts +++ b/packages/deploy-helpers/src/preview/preview.ts @@ -466,6 +466,9 @@ async function assemblePreviewDeploymentSettings( ...(pullRequest?.number && { "workers/pull_request_number": pullRequest.number, }), + ...(pullRequest?.title && { + "workers/pull_request_title": pullRequest.title, + }), ...(pullRequest?.url && { "workers/pull_request_url": pullRequest.url }), ...(repositoryUrl && { "workers/repository_url": repositoryUrl }), ...(options.tag && { "workers/tag": options.tag }), diff --git a/packages/deploy-helpers/src/preview/shared.ts b/packages/deploy-helpers/src/preview/shared.ts index 46a8c90450..c4bc444248 100644 --- a/packages/deploy-helpers/src/preview/shared.ts +++ b/packages/deploy-helpers/src/preview/shared.ts @@ -174,13 +174,14 @@ export function getRepositoryUrl(): string | undefined { } /** - * The pull/merge request number and URL detected from the current CI - * environment. Either field may be missing depending on what the detected - * CI provider exposes. + * The pull/merge request number, URL, and title detected from the current CI + * environment. Any field may be missing depending on what the detected CI + * provider exposes. */ export type PullRequestMetadata = { number?: string; url?: string; + title?: string; }; /** @@ -197,15 +198,31 @@ function normalizePullRequestNumber(number: string | number | undefined) { return normalizedNumber ? normalizedNumber : undefined; } +/** + * Trims a pull/merge request title, treating a blank value the same as a + * missing one. + */ +function normalizePullRequestTitle( + title: string | undefined +): string | undefined { + if (title === undefined) { + return undefined; + } + + const trimmedTitle = title.trim(); + return trimmedTitle || undefined; +} + /** * Detects pull request metadata from a GitHub Actions environment. * * Prefers the `pull_request` event payload at `GITHUB_EVENT_PATH` (available * for `pull_request`/`pull_request_target`-triggered workflows), which - * directly provides the PR number and URL. Falls back to parsing the PR - * number out of `GITHUB_REF` (formatted `refs/pull//merge`) and + * directly provides the PR number, URL, and title. Falls back to parsing the + * PR number out of `GITHUB_REF` (formatted `refs/pull//merge`) and * building the URL from `GITHUB_REPOSITORY`/`GITHUB_SERVER_URL`, which covers - * other trigger types where a `pull_request` payload isn't available. + * other trigger types where a `pull_request` payload isn't available — this + * fallback path can't recover a title, since that isn't encoded in the ref. */ function getGitHubPullRequestMetadata(): PullRequestMetadata | undefined { if (process.env.GITHUB_EVENT_PATH) { @@ -213,14 +230,19 @@ function getGitHubPullRequestMetadata(): PullRequestMetadata | undefined { const event = JSON.parse( readFileSync(process.env.GITHUB_EVENT_PATH, "utf8") ) as { - pull_request?: { html_url?: string; number?: number }; + pull_request?: { + html_url?: string; + number?: number; + title?: string; + }; }; const number = normalizePullRequestNumber(event.pull_request?.number); const url = event.pull_request?.html_url ? normalizeRepositoryUrl(event.pull_request.html_url) : undefined; - if (number || url) { - return { number, url }; + const title = normalizePullRequestTitle(event.pull_request?.title); + if (number || url || title) { + return { number, url, title }; } } catch { // Fall back to environment-derived metadata below. @@ -245,9 +267,9 @@ function getGitHubPullRequestMetadata(): PullRequestMetadata | undefined { /** * Detects merge request metadata from a GitLab CI merge request pipeline, - * using `CI_MERGE_REQUEST_IID` for the number and + * using `CI_MERGE_REQUEST_IID` for the number, * `CI_MERGE_REQUEST_PROJECT_URL` (or `CI_PROJECT_URL` as a fallback) to build - * the merge request URL. + * the merge request URL, and `CI_MERGE_REQUEST_TITLE` for the title. */ function getGitLabPullRequestMetadata(): PullRequestMetadata | undefined { const number = normalizePullRequestNumber(process.env.CI_MERGE_REQUEST_IID); @@ -265,16 +287,18 @@ function getGitLabPullRequestMetadata(): PullRequestMetadata | undefined { url: normalizeRepositoryUrl( `${normalizedProjectUrl}/-/merge_requests/${number}` ), + title: normalizePullRequestTitle(process.env.CI_MERGE_REQUEST_TITLE), }; } /** * Detects pull request metadata from generic, provider-agnostic env vars * (`PULL_REQUEST_URL`/`PR_URL`/`CHANGE_URL`/`CIRCLE_PULL_REQUEST` for the URL, - * `PULL_REQUEST_NUMBER`/`PR_NUMBER`/`CHANGE_ID` for the number). These are - * conventions used by some CI providers and custom pipelines, but aren't - * officially documented, so this is a lower-confidence, best-effort fallback - * checked before the provider-specific detectors. + * `PULL_REQUEST_NUMBER`/`PR_NUMBER`/`CHANGE_ID` for the number, + * `PULL_REQUEST_TITLE` for the title). These are conventions used by some CI + * providers and custom pipelines, but aren't officially documented, so this + * is a lower-confidence, best-effort fallback checked before the + * provider-specific detectors. */ function getDirectPullRequestMetadata(): PullRequestMetadata | undefined { const directUrl = @@ -288,9 +312,10 @@ function getDirectPullRequestMetadata(): PullRequestMetadata | undefined { process.env.CHANGE_ID ); const url = directUrl ? normalizeRepositoryUrl(directUrl) : undefined; + const title = normalizePullRequestTitle(process.env.PULL_REQUEST_TITLE); - if (number || url) { - return { number, url }; + if (number || url || title) { + return { number, url, title }; } return undefined; diff --git a/packages/wrangler/src/__tests__/preview.test.ts b/packages/wrangler/src/__tests__/preview.test.ts index d74e2ebc4a..a545a390fc 100644 --- a/packages/wrangler/src/__tests__/preview.test.ts +++ b/packages/wrangler/src/__tests__/preview.test.ts @@ -169,6 +169,8 @@ function clearPreviewMetadataEnvs() { vi.stubEnv("CI_COMMIT_SHA", ""); vi.stubEnv("CIRCLE_SHA1", ""); vi.stubEnv("COMMIT_SHA", ""); + vi.stubEnv("CI_MERGE_REQUEST_TITLE", ""); + vi.stubEnv("PULL_REQUEST_TITLE", ""); } describe("wrangler preview", () => { @@ -388,10 +390,12 @@ describe("wrangler preview", () => { "https://git.example.com/acme/worker-project/pulls/13" ); vi.stubEnv("PULL_REQUEST_NUMBER", "13"); + vi.stubEnv("PULL_REQUEST_TITLE", "Add a cool new feature"); expect(getPullRequestMetadata()).toEqual({ number: "13", url: "https://git.example.com/acme/worker-project/pulls/13", + title: "Add a cool new feature", }); }); @@ -402,11 +406,45 @@ describe("wrangler preview", () => { pull_request: { number: 13, html_url: "https://github.com/acme/worker-project/pull/13", + title: "Add a cool new feature", }, }) ); vi.stubEnv("GITHUB_EVENT_PATH", "github-event.json"); + expect(getPullRequestMetadata()).toEqual({ + number: "13", + url: "https://github.com/acme/worker-project/pull/13", + title: "Add a cool new feature", + }); + }); + + test("should not fail when the GitHub event pull request has no title", ({ + expect, + }) => { + writeFileSync( + "github-event.json", + JSON.stringify({ + pull_request: { + number: 13, + html_url: "https://github.com/acme/worker-project/pull/13", + }, + }) + ); + vi.stubEnv("GITHUB_EVENT_PATH", "github-event.json"); + + expect(getPullRequestMetadata()).toEqual({ + number: "13", + url: "https://github.com/acme/worker-project/pull/13", + }); + }); + + test("should not recover a title from the GITHUB_REF fallback", ({ + expect, + }) => { + vi.stubEnv("GITHUB_REF", "refs/pull/13/merge"); + vi.stubEnv("GITHUB_REPOSITORY", "acme/worker-project"); + expect(getPullRequestMetadata()).toEqual({ number: "13", url: "https://github.com/acme/worker-project/pull/13", @@ -419,6 +457,24 @@ describe("wrangler preview", () => { "https://gitlab.example.com/acme/worker-project" ); vi.stubEnv("CI_MERGE_REQUEST_IID", "13"); + vi.stubEnv("CI_MERGE_REQUEST_TITLE", "Add a cool new feature"); + + expect(getPullRequestMetadata()).toEqual({ + number: "13", + url: "https://gitlab.example.com/acme/worker-project/-/merge_requests/13", + title: "Add a cool new feature", + }); + }); + + test("should treat a blank title the same as a missing one", ({ + expect, + }) => { + vi.stubEnv( + "CI_PROJECT_URL", + "https://gitlab.example.com/acme/worker-project" + ); + vi.stubEnv("CI_MERGE_REQUEST_IID", "13"); + vi.stubEnv("CI_MERGE_REQUEST_TITLE", " "); expect(getPullRequestMetadata()).toEqual({ number: "13", @@ -4691,6 +4747,7 @@ describe("wrangler preview", () => { "https://gitlab.example.com/acme/worker-project.git" ); vi.stubEnv("CI_MERGE_REQUEST_IID", "13"); + vi.stubEnv("CI_MERGE_REQUEST_TITLE", "Add a cool new feature"); vi.stubEnv("CI_COMMIT_SHA", "abc123def456"); let deploymentRequestBody: @@ -4699,6 +4756,7 @@ describe("wrangler preview", () => { "workers/commit_sha"?: string; "workers/message"?: string; "workers/pull_request_number"?: string; + "workers/pull_request_title"?: string; "workers/pull_request_url"?: string; "workers/repository_url"?: string; "workers/tag"?: string; @@ -4769,6 +4827,7 @@ describe("wrangler preview", () => { "workers/commit_sha": "abc123def456", "workers/message": "preview note", "workers/pull_request_number": "13", + "workers/pull_request_title": "Add a cool new feature", "workers/pull_request_url": "https://gitlab.example.com/acme/worker-project/-/merge_requests/13", "workers/repository_url": @@ -4780,6 +4839,8 @@ describe("wrangler preview", () => { "https://gitlab.example.com/acme/worker-project/-/merge_requests/13" ); expect(std.out).not.toContain("repository_url"); + expect(std.out).not.toContain("pull_request_title"); + expect(std.out).not.toContain("Add a cool new feature"); }); test("should fall back to HEAD commit metadata for annotations in CI", async ({ diff --git a/turbo.json b/turbo.json index d294c9466a..7c6d094c65 100644 --- a/turbo.json +++ b/turbo.json @@ -44,6 +44,7 @@ "GITHUB_REF", "CI_MERGE_REQUEST_IID", "CI_MERGE_REQUEST_PROJECT_URL", + "CI_MERGE_REQUEST_TITLE", "PULL_REQUEST_URL", "PR_URL", "CHANGE_URL", @@ -51,6 +52,7 @@ "PULL_REQUEST_NUMBER", "PR_NUMBER", "CHANGE_ID", + "PULL_REQUEST_TITLE", "GITHUB_SHA", "CI_COMMIT_SHA", "CIRCLE_SHA1",