diff --git a/action.yml b/action.yml index 7b2b3cb1..eb102b18 100644 --- a/action.yml +++ b/action.yml @@ -100,21 +100,21 @@ runs: if [[ $flag && ${{ github.event.action }} != 'deleted' ]]; then if [ $bot_comment_id ]; then if [[ $type = pr_comment ]] || [[ $type = pr_description ]]; then - gh api repos/${{ github.repository }}/issues/comments/$bot_comment_id -X PATCH -f body="$message" + postCommentWithFallback "$message" gh api repos/${{ github.repository }}/issues/comments/$bot_comment_id -X PATCH -f body="$message" elif [[ $type = issue_comment ]] || [[ $type = issue_description ]]; then - gh api repos/${{ github.repository }}/issues/comments/$bot_comment_id -X PATCH -f body="$message" + postCommentWithFallback "$message" gh api repos/${{ github.repository }}/issues/comments/$bot_comment_id -X PATCH -f body="$message" elif [[ $type = discussion_description ]] || [[ $type = discussion_comment ]]; then - gh api graphql -f query='mutation($commentId: ID!, $body: String!) { updateDiscussionComment(input: {commentId: $commentId, body: $body}) { comment { id body }}}' -f commentId=$bot_comment_id -f body="$message" + postCommentWithFallback "$message" gh api graphql -f query='mutation($commentId: ID!, $body: String!) { updateDiscussionComment(input: {commentId: $commentId, body: $body}) { comment { id body }}}' -f commentId=$bot_comment_id -f body="$message" fi else if [[ $type = pr_comment ]] || [[ $type = pr_description ]]; then - gh pr comment $issue_url --body "$message" + postCommentWithFallback "$message" gh pr comment $issue_url --body "$message" elif [[ $type = issue_comment ]] || [[ $type = issue_description ]]; then - gh issue comment $issue_url --body "$message" + postCommentWithFallback "$message" gh issue comment $issue_url --body "$message" elif [[ $type = discussion_description ]]; then - addDiscussionComment $discussion_node_id "$message" + postCommentWithFallback "$message" addDiscussionComment $discussion_node_id "$message" elif [[ $type = discussion_comment ]]; then - addDiscussionComment $discussion_node_id "$message" $reply_to_id + postCommentWithFallback "$message" addDiscussionComment $discussion_node_id "$message" $reply_to_id fi fi else diff --git a/queries.sh b/queries.sh index bd6b7d02..f5d50c22 100644 --- a/queries.sh +++ b/queries.sh @@ -1,5 +1,42 @@ #!/bin/bash +# Runs a comment command and falls back to the log and job summary when a fork token cannot write comments. +function postCommentWithFallback() { + local MESSAGE=$1 + shift + + local OUTPUT + local EXIT_CODE + + if OUTPUT=$("$@" 2>&1); then + if [ -n "$OUTPUT" ]; then + printf '%s\n' "$OUTPUT" + fi + return 0 + else + EXIT_CODE=$? + fi + + if [[ "$OUTPUT" == *"Resource not accessible by integration"* ]]; then + printf '%s\n' "::warning::Unable to post the accessibility alt text comment because the workflow token cannot write comments. The message is included below and in the job summary." + printf '\n%s\n' "$MESSAGE" + + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + { + printf '# Accessibility alt text bot\n\n' + printf '%s\n' "$MESSAGE" + } >> "$GITHUB_STEP_SUMMARY" + fi + + return 0 + fi + + if [ -n "$OUTPUT" ]; then + printf '%s\n' "$OUTPUT" >&2 + fi + return "$EXIT_CODE" +} + # Given a node_id for a discussion comment that is a reply in thread, return the parent comment's node ID. function getDiscussionReplyToId() { local NODE_ID=$1 @@ -33,7 +70,7 @@ function addDiscussionComment() { } ' else - gh api graphql -F discussionId="$discussion_node_id" -F body="$message" -f query=' + gh api graphql -F discussionId="$DISCUSSION_NODE_ID" -F body="$MESSAGE" -f query=' mutation($discussionId: ID!, $body: String!) { addDiscussionComment(input: {discussionId: $discussionId, body: $body}) { comment { diff --git a/src/queries.test.js b/src/queries.test.js new file mode 100644 index 00000000..cefbfb96 --- /dev/null +++ b/src/queries.test.js @@ -0,0 +1,116 @@ +import { spawnSync } from "node:child_process"; +import { + existsSync, + mkdtempSync, + readFileSync, + rmSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { describe, expect, test } from "vitest"; + +const queriesPath = join( + dirname(fileURLToPath(import.meta.url)), + "..", + "queries.sh", +); + +const runShell = (body) => { + const temporaryDirectory = mkdtempSync( + join(tmpdir(), "accessibility-alt-text-bot-"), + ); + const summaryPath = join(temporaryDirectory, "summary.md"); + + try { + const result = spawnSync( + "bash", + [ + "-c", + `set -e -o pipefail\nsource "$1"\n${body}`, + "queries-test", + queriesPath, + ], + { + encoding: "utf8", + env: { + ...process.env, + GITHUB_STEP_SUMMARY: summaryPath, + TEST_MESSAGE: "Complete fallback message\nwith a second line.", + }, + }, + ); + + return { + result, + summary: existsSync(summaryPath) + ? readFileSync(summaryPath, "utf8") + : undefined, + }; + } finally { + rmSync(temporaryDirectory, { recursive: true, force: true }); + } +}; + +describe("postCommentWithFallback", () => { + test("preserves successful command output without a fallback", () => { + const { result, summary } = runShell(` + postCommentWithFallback "$TEST_MESSAGE" bash -c \ + 'printf "%s\\n" "$1"' comment-command "$TEST_MESSAGE" + `); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("Complete fallback message\nwith a second line.\n"); + expect(result.stderr).toBe(""); + expect(summary).toBeUndefined(); + }); + + test("reports the full message when fork permissions prevent a comment", () => { + const { result, summary } = runShell(` + postCommentWithFallback "$TEST_MESSAGE" bash -c \ + 'printf "GraphQL: Resource not accessible by integration (addComment)\\n" >&2; exit 1' + `); + + expect(result.status).toBe(0); + expect(result.stdout).toContain( + "::warning::Unable to post the accessibility alt text comment", + ); + expect(result.stdout).toContain("Complete fallback message\nwith a second line."); + expect(result.stderr).toBe(""); + expect(summary).toContain("# Accessibility alt text bot"); + expect(summary).toContain("Complete fallback message\nwith a second line."); + }); + + test("preserves unexpected failures", () => { + const { result, summary } = runShell(` + postCommentWithFallback "$TEST_MESSAGE" bash -c \ + 'printf "unexpected API failure\\n" >&2; exit 23' + `); + + expect(result.status).toBe(23); + expect(result.stdout).toBe(""); + expect(result.stderr).toBe("unexpected API failure\n"); + expect(summary).toBeUndefined(); + }); +}); + +describe("addDiscussionComment", () => { + test("uses its explicit arguments for a top-level comment", () => { + const { result } = runShell(` + discussion_node_id="global-id" + message="global message" + gh() { printf '<%s>\\n' "$@"; } + + addDiscussionComment "argument-id" "$TEST_MESSAGE" + `); + + expect(result.status).toBe(0); + expect(result.stdout).toContain(""); + expect(result.stdout).toContain( + "", + ); + expect(result.stdout).not.toContain("global-id"); + expect(result.stdout).not.toContain("global message"); + }); +});