diff --git a/__generated__/gql/gql.ts b/__generated__/gql/gql.ts index 86395ee1..bb411e88 100644 --- a/__generated__/gql/gql.ts +++ b/__generated__/gql/gql.ts @@ -20,6 +20,8 @@ type Documents = { "\n mutation AddComment($subjectId: ID!, $body: String!) {\n addComment(input: {subjectId: $subjectId, body: $body}) {\n commentEdge {\n node {\n id\n }\n }\n }\n }\n": typeof types.AddCommentDocument, "\n query PullRequestNodeId($owner: String!, $repo: String!, $number: Int!) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n id\n }\n }\n }\n": typeof types.PullRequestNodeIdDocument, "\n query PullRequestNodeIdByBranch(\n $owner: String!\n $repo: String!\n $headRefName: String!\n ) {\n repository(owner: $owner, name: $repo) {\n pullRequests(first: 1, states: [OPEN], headRefName: $headRefName) {\n nodes {\n id\n }\n }\n }\n }\n": typeof types.PullRequestNodeIdByBranchDocument, + "\n query PullRequestComments($prNodeId: ID!, $first: Int!) {\n node(id: $prNodeId) {\n ... on PullRequest {\n comments(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) {\n nodes {\n id\n body\n author {\n login\n }\n }\n }\n }\n }\n }\n": typeof types.PullRequestCommentsDocument, + "\n mutation UpdateComment($id: ID!, $body: String!) {\n updateIssueComment(input: {id: $id, body: $body}) {\n issueComment {\n id\n }\n }\n }\n": typeof types.UpdateCommentDocument, "\n mutation CreateGitHubDeployment(\n $repositoryId: ID!\n $environmentName: String!\n $refId: ID!\n $payload: String!\n $description: String\n ) {\n createDeployment(\n input: {\n autoMerge: false\n description: $description\n environment: $environmentName\n refId: $refId\n repositoryId: $repositoryId\n requiredContexts: []\n payload: $payload\n }\n ) {\n deployment {\n ...DeploymentFragment\n }\n }\n }\n": typeof types.CreateGitHubDeploymentDocument, "\n mutation DeleteGitHubDeployment($deploymentId: ID!) {\n deleteDeployment(input: {id: $deploymentId}) {\n clientMutationId\n }\n }\n": typeof types.DeleteGitHubDeploymentDocument, "\n mutation DeleteGitHubDeploymentAndComment(\n $deploymentId: ID!\n $commentId: ID!\n ) {\n deleteDeployment(input: {id: $deploymentId}) {\n clientMutationId\n }\n deleteIssueComment(input: {id: $commentId}) {\n clientMutationId\n }\n }\n": typeof types.DeleteGitHubDeploymentAndCommentDocument, @@ -35,6 +37,8 @@ const documents: Documents = { "\n mutation AddComment($subjectId: ID!, $body: String!) {\n addComment(input: {subjectId: $subjectId, body: $body}) {\n commentEdge {\n node {\n id\n }\n }\n }\n }\n": types.AddCommentDocument, "\n query PullRequestNodeId($owner: String!, $repo: String!, $number: Int!) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n id\n }\n }\n }\n": types.PullRequestNodeIdDocument, "\n query PullRequestNodeIdByBranch(\n $owner: String!\n $repo: String!\n $headRefName: String!\n ) {\n repository(owner: $owner, name: $repo) {\n pullRequests(first: 1, states: [OPEN], headRefName: $headRefName) {\n nodes {\n id\n }\n }\n }\n }\n": types.PullRequestNodeIdByBranchDocument, + "\n query PullRequestComments($prNodeId: ID!, $first: Int!) {\n node(id: $prNodeId) {\n ... on PullRequest {\n comments(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) {\n nodes {\n id\n body\n author {\n login\n }\n }\n }\n }\n }\n }\n": types.PullRequestCommentsDocument, + "\n mutation UpdateComment($id: ID!, $body: String!) {\n updateIssueComment(input: {id: $id, body: $body}) {\n issueComment {\n id\n }\n }\n }\n": types.UpdateCommentDocument, "\n mutation CreateGitHubDeployment(\n $repositoryId: ID!\n $environmentName: String!\n $refId: ID!\n $payload: String!\n $description: String\n ) {\n createDeployment(\n input: {\n autoMerge: false\n description: $description\n environment: $environmentName\n refId: $refId\n repositoryId: $repositoryId\n requiredContexts: []\n payload: $payload\n }\n ) {\n deployment {\n ...DeploymentFragment\n }\n }\n }\n": types.CreateGitHubDeploymentDocument, "\n mutation DeleteGitHubDeployment($deploymentId: ID!) {\n deleteDeployment(input: {id: $deploymentId}) {\n clientMutationId\n }\n }\n": types.DeleteGitHubDeploymentDocument, "\n mutation DeleteGitHubDeploymentAndComment(\n $deploymentId: ID!\n $commentId: ID!\n ) {\n deleteDeployment(input: {id: $deploymentId}) {\n clientMutationId\n }\n deleteIssueComment(input: {id: $commentId}) {\n clientMutationId\n }\n }\n": types.DeleteGitHubDeploymentAndCommentDocument, @@ -65,6 +69,14 @@ export function graphql(source: "\n query PullRequestNodeId($owner: String!, $r * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n query PullRequestNodeIdByBranch(\n $owner: String!\n $repo: String!\n $headRefName: String!\n ) {\n repository(owner: $owner, name: $repo) {\n pullRequests(first: 1, states: [OPEN], headRefName: $headRefName) {\n nodes {\n id\n }\n }\n }\n }\n"): typeof import('./graphql.js').PullRequestNodeIdByBranchDocument; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query PullRequestComments($prNodeId: ID!, $first: Int!) {\n node(id: $prNodeId) {\n ... on PullRequest {\n comments(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) {\n nodes {\n id\n body\n author {\n login\n }\n }\n }\n }\n }\n }\n"): typeof import('./graphql.js').PullRequestCommentsDocument; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n mutation UpdateComment($id: ID!, $body: String!) {\n updateIssueComment(input: {id: $id, body: $body}) {\n issueComment {\n id\n }\n }\n }\n"): typeof import('./graphql.js').UpdateCommentDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/__generated__/gql/graphql.ts b/__generated__/gql/graphql.ts index 290debb2..95f635d9 100644 --- a/__generated__/gql/graphql.ts +++ b/__generated__/gql/graphql.ts @@ -103,6 +103,31 @@ export type PullRequestNodeIdByBranchQueryVariables = Exact<{ export type PullRequestNodeIdByBranchQuery = { readonly repository: { readonly pullRequests: { readonly nodes: ReadonlyArray<{ readonly id: string } | null> | null } } | null }; +export type PullRequestCommentsQueryVariables = Exact<{ + prNodeId: string | number; + first: number; +}>; + + +export type PullRequestCommentsQuery = { readonly node: + | { readonly comments: { readonly nodes: ReadonlyArray<{ readonly id: string, readonly body: string, readonly author: + | { readonly login: string } + | { readonly login: string } + | { readonly login: string } + | { readonly login: string } + | { readonly login: string } + | null } | null> | null } } + | Record + | null }; + +export type UpdateCommentMutationVariables = Exact<{ + id: string | number; + body: string; +}>; + + +export type UpdateCommentMutation = { readonly updateIssueComment: { readonly issueComment: { readonly id: string } | null } | null }; + export type CreateGitHubDeploymentMutationVariables = Exact<{ repositoryId: string | number; environmentName: string; @@ -260,6 +285,32 @@ export const PullRequestNodeIdByBranchDocument = new TypedDocumentString(` } } `) as unknown as TypedDocumentString; +export const PullRequestCommentsDocument = new TypedDocumentString(` + query PullRequestComments($prNodeId: ID!, $first: Int!) { + node(id: $prNodeId) { + ... on PullRequest { + comments(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) { + nodes { + id + body + author { + login + } + } + } + } + } +} + `) as unknown as TypedDocumentString; +export const UpdateCommentDocument = new TypedDocumentString(` + mutation UpdateComment($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } +} + `) as unknown as TypedDocumentString; export const CreateGitHubDeploymentDocument = new TypedDocumentString(` mutation CreateGitHubDeployment($repositoryId: ID!, $environmentName: String!, $refId: ID!, $payload: String!, $description: String) { createDeployment( diff --git a/__tests__/common/github/comment.test.ts b/__tests__/common/github/comment.test.ts index 4225a022..ced414f0 100644 --- a/__tests__/common/github/comment.test.ts +++ b/__tests__/common/github/comment.test.ts @@ -7,6 +7,8 @@ import type {MockApi} from '@/tests/helpers/api.js' import { addComment, MutationAddComment, + MutationUpdateComment, + QueryPullRequestComments, QueryPullRequestNodeId, QueryPullRequestNodeIdByBranch } from '@/common/github/comment.js' @@ -40,7 +42,7 @@ describe(addComment, () => { query: MutationAddComment, variables: { subjectId: 'MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3', - body: '## Cloudflare Pages Deployment\n**Event Name:** pull_request\n**Environment:** production\n**Project:** cloudflare-pages-action\n**Built with commit:** mock-github-sha\n**Preview URL:** https://206e215c.cloudflare-pages-action-a5z.pages.dev\n**Branch Preview URL:** https://unknown-branch.cloudflare-pages-action-a5z.pages.dev\n\n### Wrangler Output\nsuccess' + body: '\n## Cloudflare Pages Deployment\n**Event Name:** pull_request\n**Environment:** production\n**Project:** cloudflare-pages-action\n**Built with commit:** mock-github-sha\n**Preview URL:** https://206e215c.cloudflare-pages-action-a5z.pages.dev\n**Branch Preview URL:** https://unknown-branch.cloudflare-pages-action-a5z.pages.dev\n\n### Wrangler Output\nsuccess' } }, { @@ -133,7 +135,7 @@ describe(addComment, () => { query: MutationAddComment, variables: { subjectId: 'MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3', - body: '## Cloudflare Pages Deployment\n**Event Name:** workflow_run\n**Environment:** production\n**Project:** cloudflare-pages-action\n**Built with commit:** 3484a3fb816e0859fd6e1cea078d76385ff50625\n**Preview URL:** https://206e215c.cloudflare-pages-action-a5z.pages.dev\n**Branch Preview URL:** https://unknown-branch.cloudflare-pages-action-a5z.pages.dev\n\n### Wrangler Output\nsuccess' + body: '\n## Cloudflare Pages Deployment\n**Event Name:** workflow_run\n**Environment:** production\n**Project:** cloudflare-pages-action\n**Built with commit:** 3484a3fb816e0859fd6e1cea078d76385ff50625\n**Preview URL:** https://206e215c.cloudflare-pages-action-a5z.pages.dev\n**Branch Preview URL:** https://unknown-branch.cloudflare-pages-action-a5z.pages.dev\n\n### Wrangler Output\nsuccess' } }, { @@ -217,7 +219,9 @@ describe(addComment, () => { gitHubApiToken: 'mock-github-token', gitHubEnvironment: undefined, prNumber: '123', - wranglerVersion: 'mock-wrangler-version' + wranglerVersion: 'mock-wrangler-version', + commentMode: 'new', + hideWranglerOutput: false }) vi.spyOn(Context, 'useContextEvent').mockReturnValue({ @@ -266,7 +270,7 @@ describe(addComment, () => { query: MutationAddComment, variables: { subjectId: 'MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3', - body: '## Cloudflare Pages Deployment\n**Event Name:** workflow_dispatch\n**Environment:** production\n**Project:** cloudflare-pages-action\n**Built with commit:** mock-github-sha\n**Preview URL:** https://206e215c.cloudflare-pages-action-a5z.pages.dev\n**Branch Preview URL:** https://unknown-branch.cloudflare-pages-action-a5z.pages.dev\n\n### Wrangler Output\nsuccess' + body: '\n## Cloudflare Pages Deployment\n**Event Name:** workflow_dispatch\n**Environment:** production\n**Project:** cloudflare-pages-action\n**Built with commit:** mock-github-sha\n**Preview URL:** https://206e215c.cloudflare-pages-action-a5z.pages.dev\n**Branch Preview URL:** https://unknown-branch.cloudflare-pages-action-a5z.pages.dev\n\n### Wrangler Output\nsuccess' } }, { @@ -295,7 +299,9 @@ describe(addComment, () => { gitHubApiToken: 'mock-github-token', gitHubEnvironment: undefined, prNumber: 'abc', - wranglerVersion: 'mock-wrangler-version' + wranglerVersion: 'mock-wrangler-version', + commentMode: 'new', + hideWranglerOutput: false }) await expect(addComment(mockData, 'success')).rejects.toThrow( @@ -354,7 +360,7 @@ describe(addComment, () => { query: MutationAddComment, variables: { subjectId: 'MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3', - body: '## Cloudflare Pages Deployment\n**Event Name:** workflow_dispatch\n**Environment:** production\n**Project:** cloudflare-pages-action\n**Built with commit:** mock-github-sha\n**Preview URL:** https://206e215c.cloudflare-pages-action-a5z.pages.dev\n**Branch Preview URL:** https://unknown-branch.cloudflare-pages-action-a5z.pages.dev\n\n### Wrangler Output\nsuccess' + body: '\n## Cloudflare Pages Deployment\n**Event Name:** workflow_dispatch\n**Environment:** production\n**Project:** cloudflare-pages-action\n**Built with commit:** mock-github-sha\n**Preview URL:** https://206e215c.cloudflare-pages-action-a5z.pages.dev\n**Branch Preview URL:** https://unknown-branch.cloudflare-pages-action-a5z.pages.dev\n\n### Wrangler Output\nsuccess' } }, { @@ -448,4 +454,173 @@ describe(addComment, () => { } ) }) + + describe('comment-mode: update', () => { + test('should update existing comment when comment-mode is update', async () => { + expect.assertions(1) + + vi.spyOn(CommonInputs, 'useCommonInputs').mockReturnValue({ + cloudflareApiToken: 'mock-cloudflare-api-token', + gitHubApiToken: 'mock-github-token', + gitHubEnvironment: undefined, + prNumber: undefined, + wranglerVersion: 'mock-wrangler-version', + commentMode: 'update', + hideWranglerOutput: false + }) + + // Mock finding existing comment + mockApi.interceptGithub( + { + query: QueryPullRequestComments, + variables: { + prNodeId: 'MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3', + first: 50 + } + }, + { + data: { + node: { + comments: { + nodes: [ + { + id: 'existing-comment-id', + body: '\n## Old deployment', + author: { + login: 'github-actions[bot]' + } + } + ] + } + } + } + } + ) + + // Mock updating the comment + mockApi.interceptGithub( + { + query: MutationUpdateComment, + variables: { + id: 'existing-comment-id', + body: '\n## Cloudflare Pages Deployment\n**Event Name:** pull_request\n**Environment:** production\n**Project:** cloudflare-pages-action\n**Built with commit:** mock-github-sha\n**Preview URL:** https://206e215c.cloudflare-pages-action-a5z.pages.dev\n**Branch Preview URL:** https://unknown-branch.cloudflare-pages-action-a5z.pages.dev\n\n### Wrangler Output\nsuccess' + } + }, + { + data: { + updateIssueComment: { + issueComment: { + id: 'existing-comment-id' + } + } + } + } + ) + + const comment = await addComment(mockData, 'success') + + expect(comment).toBe('existing-comment-id') + }) + + test('should create new comment when no existing comment found in update mode', async () => { + expect.assertions(1) + + vi.spyOn(CommonInputs, 'useCommonInputs').mockReturnValue({ + cloudflareApiToken: 'mock-cloudflare-api-token', + gitHubApiToken: 'mock-github-token', + gitHubEnvironment: undefined, + prNumber: undefined, + wranglerVersion: 'mock-wrangler-version', + commentMode: 'update', + hideWranglerOutput: false + }) + + // Mock finding no existing comments + mockApi.interceptGithub( + { + query: QueryPullRequestComments, + variables: { + prNodeId: 'MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3', + first: 50 + } + }, + { + data: { + node: { + comments: { + nodes: [] + } + } + } + } + ) + + // Mock creating new comment + mockApi.interceptGithub( + { + query: MutationAddComment, + variables: { + subjectId: 'MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3', + body: '\n## Cloudflare Pages Deployment\n**Event Name:** pull_request\n**Environment:** production\n**Project:** cloudflare-pages-action\n**Built with commit:** mock-github-sha\n**Preview URL:** https://206e215c.cloudflare-pages-action-a5z.pages.dev\n**Branch Preview URL:** https://unknown-branch.cloudflare-pages-action-a5z.pages.dev\n\n### Wrangler Output\nsuccess' + } + }, + { + data: { + addComment: { + commentEdge: { + node: { + id: 'new-comment-id' + } + } + } + } + } + ) + + const comment = await addComment(mockData, 'success') + + expect(comment).toBe('new-comment-id') + }) + }) + + describe('hide-wrangler-output', () => { + test('should hide wrangler output when hideWranglerOutput is true', async () => { + expect.assertions(1) + + vi.spyOn(CommonInputs, 'useCommonInputs').mockReturnValue({ + cloudflareApiToken: 'mock-cloudflare-api-token', + gitHubApiToken: 'mock-github-token', + gitHubEnvironment: undefined, + prNumber: undefined, + wranglerVersion: 'mock-wrangler-version', + commentMode: 'new', + hideWranglerOutput: true + }) + + mockApi.interceptGithub( + { + query: MutationAddComment, + variables: { + subjectId: 'MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3', + body: '\n## Cloudflare Pages Deployment\n**Event Name:** pull_request\n**Environment:** production\n**Project:** cloudflare-pages-action\n**Built with commit:** mock-github-sha\n**Preview URL:** https://206e215c.cloudflare-pages-action-a5z.pages.dev\n**Branch Preview URL:** https://unknown-branch.cloudflare-pages-action-a5z.pages.dev' + } + }, + { + data: { + addComment: { + commentEdge: { + node: { + id: '1' + } + } + } + } + } + ) + + const comment = await addComment(mockData, 'success') + + expect(comment).toBe('1') + }) + }) }) diff --git a/__tests__/common/inputs.test.ts b/__tests__/common/inputs.test.ts index 20ffdfb4..3bf81011 100644 --- a/__tests__/common/inputs.test.ts +++ b/__tests__/common/inputs.test.ts @@ -4,7 +4,9 @@ import { INPUT_KEY_CLOUDFLARE_API_TOKEN, INPUT_KEY_GITHUB_ENVIRONMENT, INPUT_KEY_GITHUB_TOKEN, - INPUT_KEY_WRANGLER_VERSION + INPUT_KEY_WRANGLER_VERSION, + INPUT_KEY_COMMENT_MODE, + INPUT_KEY_HIDE_WRANGLER_OUTPUT } from '@/input-keys' import {stubInputEnv} from '@/tests/helpers/inputs.js' @@ -56,7 +58,9 @@ describe('common', () => { gitHubApiToken: 'mock-github-token', gitHubEnvironment: 'mock-github-environment', prNumber: undefined, - wranglerVersion: 'mock-wrangler-version' + wranglerVersion: 'mock-wrangler-version', + commentMode: 'new', + hideWranglerOutput: false }) }) @@ -73,7 +77,9 @@ describe('common', () => { gitHubApiToken: 'mock-github-token', gitHubEnvironment: undefined, prNumber: undefined, - wranglerVersion: packageJson.devDependencies.wrangler + wranglerVersion: packageJson.devDependencies.wrangler, + commentMode: 'new', + hideWranglerOutput: false }) }) @@ -91,5 +97,68 @@ describe('common', () => { }) ) }) + + test('returns update comment mode when set', async () => { + expect.assertions(1) + + stubInputEnv(INPUT_KEY_CLOUDFLARE_API_TOKEN) + stubInputEnv(INPUT_KEY_GITHUB_TOKEN) + stubInputEnv(INPUT_KEY_COMMENT_MODE, 'update') + + const {useCommonInputs} = await setup() + + expect(useCommonInputs()).toStrictEqual( + expect.objectContaining({ + commentMode: 'update' + }) + ) + }) + + test('defaults to new comment mode for invalid values', async () => { + expect.assertions(1) + + stubInputEnv(INPUT_KEY_CLOUDFLARE_API_TOKEN) + stubInputEnv(INPUT_KEY_GITHUB_TOKEN) + stubInputEnv(INPUT_KEY_COMMENT_MODE, 'invalid') + + const {useCommonInputs} = await setup() + + expect(useCommonInputs()).toStrictEqual( + expect.objectContaining({ + commentMode: 'new' + }) + ) + }) + + test('hides wrangler output when set to true', async () => { + expect.assertions(1) + + stubInputEnv(INPUT_KEY_CLOUDFLARE_API_TOKEN) + stubInputEnv(INPUT_KEY_GITHUB_TOKEN) + stubInputEnv(INPUT_KEY_HIDE_WRANGLER_OUTPUT, 'true') + + const {useCommonInputs} = await setup() + + expect(useCommonInputs()).toStrictEqual( + expect.objectContaining({ + hideWranglerOutput: true + }) + ) + }) + + test('shows wrangler output by default', async () => { + expect.assertions(1) + + stubInputEnv(INPUT_KEY_CLOUDFLARE_API_TOKEN) + stubInputEnv(INPUT_KEY_GITHUB_TOKEN) + + const {useCommonInputs} = await setup() + + expect(useCommonInputs()).toStrictEqual( + expect.objectContaining({ + hideWranglerOutput: false + }) + ) + }) }) }) diff --git a/action.yml b/action.yml index f137d72b..e3faa920 100644 --- a/action.yml +++ b/action.yml @@ -36,6 +36,14 @@ inputs: branch: description: 'Branch name to use for Cloudflare Pages deployment. If not set, the branch is automatically detected from the GitHub context.' required: false + comment-mode: + description: 'Comment mode for PR comments. "new" creates a new comment for each deployment (default). "update" updates the same comment for all deployments on the PR.' + required: false + default: 'new' + hide-wrangler-output: + description: 'Hide Wrangler CLI output from PR comments. Set to "true" to hide the output.' + required: false + default: 'false' outputs: id: diff --git a/input-keys.ts b/input-keys.ts index 982547c7..9723984c 100644 --- a/input-keys.ts +++ b/input-keys.ts @@ -9,6 +9,8 @@ export const INPUT_KEY_WORKING_DIRECTORY = 'working-directory' export const INPUT_KEYS_KEEP_LATEST = 'keep-latest' export const INPUT_KEY_WRANGLER_VERSION = 'wrangler-version' export const INPUT_KEY_BRANCH = 'branch' +export const INPUT_KEY_COMMENT_MODE = 'comment-mode' +export const INPUT_KEY_HIDE_WRANGLER_OUTPUT = 'hide-wrangler-output' export const INPUT_KEYS_REQUIRED = [ INPUT_KEY_CLOUDFLARE_ACCOUNT_ID, diff --git a/src/common/github/comment.ts b/src/common/github/comment.ts index e1631059..114abd13 100644 --- a/src/common/github/comment.ts +++ b/src/common/github/comment.ts @@ -49,6 +49,70 @@ export const QueryPullRequestNodeIdByBranch = graphql(/* GraphQL */ ` } `) +export const QueryPullRequestComments = graphql(/* GraphQL */ ` + query PullRequestComments($prNodeId: ID!, $first: Int!) { + node(id: $prNodeId) { + ... on PullRequest { + comments(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) { + nodes { + id + body + author { + login + } + } + } + } + } + } +`) + +export const MutationUpdateComment = graphql(/* GraphQL */ ` + mutation UpdateComment($id: ID!, $body: String!) { + updateIssueComment(input: {id: $id, body: $body}) { + issueComment { + id + } + } + } +`) + +const COMMENT_MARKER = '' + +const findExistingComment = async ( + prNodeId: string +): Promise => { + try { + const result = await request({ + query: QueryPullRequestComments, + variables: { + prNodeId, + first: 50 + } + }) + + const node = result.data.node + if (!node || !('comments' in node)) { + return undefined + } + + const comments = node.comments.nodes ?? [] + const botLogin = 'github-actions[bot]' + + // Find the most recent comment from this action that contains our marker + const existingComment = comments.find( + comment => + comment?.author?.login === botLogin && + comment?.body?.includes(COMMENT_MARKER) + ) + + return existingComment?.id + } catch { + // If we can't find existing comments, we'll just create a new one + return undefined + } +} + const getNodeIdFromEvent = async () => { const {repo} = useContext() const {prNumber} = useCommonInputs() @@ -163,9 +227,36 @@ export const addComment = async ( if (prNodeId) { const {sha} = useContext() const {eventName} = useContextEvent() + const {commentMode, hideWranglerOutput} = useCommonInputs() + + // Build the comment body + let rawBody = `${COMMENT_MARKER}\n## Cloudflare Pages Deployment\n**Event Name:** ${eventName}\n**Environment:** ${deployment.environment}\n**Project:** ${deployment.project_name}\n**Built with commit:** ${sha}\n**Preview URL:** ${deployment.url}\n**Branch Preview URL:** ${getCloudflareDeploymentAlias(deployment)}` + + // Optionally include Wrangler output + if (!hideWranglerOutput) { + rawBody += `\n\n### Wrangler Output\n${output}` + } - const rawBody = `## Cloudflare Pages Deployment\n**Event Name:** ${eventName}\n**Environment:** ${deployment.environment}\n**Project:** ${deployment.project_name}\n**Built with commit:** ${sha}\n**Preview URL:** ${deployment.url}\n**Branch Preview URL:** ${getCloudflareDeploymentAlias(deployment)}\n\n### Wrangler Output\n${output}` + // Check if we should update an existing comment or create a new one + if (commentMode === 'update') { + const existingCommentId = await findExistingComment(prNodeId) + + if (existingCommentId) { + // Update existing comment + info('Updating existing PR comment') + await request({ + query: MutationUpdateComment, + variables: { + id: existingCommentId, + body: rawBody + } + }) + return existingCommentId + } + } + // Create new comment (either commentMode is 'new' or no existing comment found) + info('Creating new PR comment') const comment = await request({ query: MutationAddComment, variables: { diff --git a/src/common/inputs.ts b/src/common/inputs.ts index 3c6cd60e..7dbdac01 100644 --- a/src/common/inputs.ts +++ b/src/common/inputs.ts @@ -5,7 +5,9 @@ import { INPUT_KEY_GITHUB_ENVIRONMENT, INPUT_KEY_PR_NUMBER, INPUT_KEY_GITHUB_TOKEN, - INPUT_KEY_WRANGLER_VERSION + INPUT_KEY_WRANGLER_VERSION, + INPUT_KEY_COMMENT_MODE, + INPUT_KEY_HIDE_WRANGLER_OUTPUT } from '@/input-keys' type Inputs = { @@ -19,9 +21,18 @@ type Inputs = { prNumber?: string /** Wrangler version to use. */ wranglerVersion: string + /** Comment mode: 'new' or 'update' */ + commentMode: 'new' | 'update' + /** Whether to hide Wrangler output in PR comments */ + hideWranglerOutput: boolean } const getInputs = (): Inputs => { + const commentMode = + getInput(INPUT_KEY_COMMENT_MODE, {required: false}) || 'new' + const hideWranglerOutputInput = + getInput(INPUT_KEY_HIDE_WRANGLER_OUTPUT, {required: false}) || 'false' + return { cloudflareApiToken: getInput(INPUT_KEY_CLOUDFLARE_API_TOKEN, { required: true @@ -30,7 +41,9 @@ const getInputs = (): Inputs => { gitHubEnvironment: getInput(INPUT_KEY_GITHUB_ENVIRONMENT, {required: false}) || undefined, prNumber: getInput(INPUT_KEY_PR_NUMBER, {required: false}) || undefined, - wranglerVersion: getInput(INPUT_KEY_WRANGLER_VERSION) || '4.86.0' + wranglerVersion: getInput(INPUT_KEY_WRANGLER_VERSION) || '4.86.0', + commentMode: commentMode === 'update' ? 'update' : 'new', + hideWranglerOutput: hideWranglerOutputInput.toLowerCase() === 'true' } }