From 4ce0303c8b38baac204e019b2fa6c96fb0fc0df5 Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Mon, 24 Jan 2022 14:03:45 +0100 Subject: [PATCH 1/3] refactor(preview-comment): auto-fill the github token --- .github/workflows/test.yml | 11 ++++++- README.md | 2 -- build/preview-comment/index.js | 44 +++++++++++++++------------ preview-comment/README.md | 10 ++++-- preview-comment/action.yml | 4 +++ src/actions/preview-comment.ts | 5 ++- src/github.ts | 44 +++++++++++++++------------ tests/actions/preview-comment.test.ts | 7 +++++ tests/github.test.ts | 22 ++++++++++++-- 9 files changed, 101 insertions(+), 48 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d65bf9d7..14d58335 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,8 +76,16 @@ jobs: script: | const message = `${{ steps.preview.outputs.message }}` if (!message) throw new Error('Message output is empty') + + - name: 🧪 Comment on PR (input auth) + uses: ./preview-comment + env: + EXPO_TEST_GITHUB_PULL: 149 + with: + project: ./temp + channel: test - - name: 🧪 Comment on PR + - name: 🧪 Comment on PR (envvar auth) uses: ./preview-comment env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -85,5 +93,6 @@ jobs: with: project: ./temp channel: test + github-token: '' diff --git a/README.md b/README.md index 73f8ee85..a97b2d82 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,6 @@ jobs: - name: 💬 Comment preview uses: expo/expo-github-action/preview-comment@v7 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: channel: pr-${{ github.event.number }} ``` diff --git a/build/preview-comment/index.js b/build/preview-comment/index.js index d732ef4e..155a7cdc 100644 --- a/build/preview-comment/index.js +++ b/build/preview-comment/index.js @@ -13563,6 +13563,7 @@ function commentInput() { message: (0, core_1.getInput)('message') || exports.DEFAULT_MESSAGE, messageId: (0, core_1.getInput)('message-id') || exports.DEFAULT_ID, project: (0, core_1.getInput)('project'), + githubToken: (0, core_1.getInput)('github-token'), }; } exports.commentInput = commentInput; @@ -13586,7 +13587,9 @@ async function commentAction(input = commentInput()) { (0, core_1.info)(`Skipped comment: 'comment' is disabled`); } else { - await (0, github_1.createIssueComment)((0, github_1.pullContext)(), { + await (0, github_1.createIssueComment)({ + ...(0, github_1.pullContext)(), + token: input.githubToken, id: messageId, body: messageBody, }); @@ -13719,16 +13722,16 @@ const assert_1 = __nccwpck_require__(9491); * Determine if a comment exists on an issue or pull with the provided identifier. * This will iterate all comments received from GitHub, and try to exit early if it exists. */ -async function fetchIssueComment(issue, commentId) { - const github = githubApi(); +async function fetchIssueComment(options) { + const github = githubApi(options); const iterator = github.paginate.iterator(github.rest.issues.listComments, { - owner: issue.owner, - repo: issue.repo, - issue_number: issue.number, + owner: options.owner, + repo: options.repo, + issue_number: options.number, }); for await (const { data: batch } of iterator) { for (const item of batch) { - if ((item.body || '').includes(commentId)) { + if ((item.body || '').includes(options.id)) { return item; } } @@ -13740,33 +13743,34 @@ exports.fetchIssueComment = fetchIssueComment; * This includes a hidden identifier (markdown comment) to identify the comment later. * It will also update the comment when a previous comment id was found. */ -async function createIssueComment(issue, comment) { - const github = githubApi(); - const body = `\n${comment.body}`; - const existing = await fetchIssueComment(issue, comment.id); +async function createIssueComment(options) { + const github = githubApi(options); + const body = `\n${options.body}`; + const existing = await fetchIssueComment(options); if (existing) { return github.rest.issues.updateComment({ - owner: issue.owner, - repo: issue.repo, + owner: options.owner, + repo: options.repo, comment_id: existing.id, body, }); } return github.rest.issues.createComment({ - owner: issue.owner, - repo: issue.repo, - issue_number: issue.number, + owner: options.owner, + repo: options.repo, + issue_number: options.number, body, }); } exports.createIssueComment = createIssueComment; /** * Get an authenticated octokit instance. - * This uses the 'GITHUB_TOKEN' environment variable. + * This uses the 'GITHUB_TOKEN' environment variable, or 'github-token' input. */ -function githubApi() { - (0, assert_1.ok)(process.env['GITHUB_TOKEN'], 'This step requires a GITHUB_TOKEN environment variable to create comments'); - return (0, github_1.getOctokit)(process.env['GITHUB_TOKEN']); +function githubApi(options = {}) { + const token = process.env['GITHUB_TOKEN'] || options.token; + (0, assert_1.ok)(token, `This step requires 'github-token' or a GITHUB_TOKEN environment variable to create comments`); + return (0, github_1.getOctokit)(token); } exports.githubApi = githubApi; /** diff --git a/preview-comment/README.md b/preview-comment/README.md index 1cbf3597..45f2605c 100644 --- a/preview-comment/README.md +++ b/preview-comment/README.md @@ -44,6 +44,7 @@ Here is a summary of all the input options you can use. | **comment** | `true` | If this action should comment on a PR | | **message** | _[see code][code-defaults]_ | The message template | | **message-id** | _[see code][code-defaults]_ | A unique id template to prevent duplicate comments ([read more](#preventing-duplicate-comments)) | +| **github-token** | `GITHUB_TOKEN` | A GitHub token to use when commenting on PR ([read more](#github-tokens)) | ## Available outputs @@ -107,8 +108,6 @@ jobs: - name: 💬 Comment in preview uses: expo/expo-github-action/preview-comment@v7 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: channel: pr-${{ github.event.number }} ``` @@ -173,6 +172,12 @@ jobs: When automating these preview comments, you have to be careful not to spam a pull request on every successful run. Every comment contains a generated **message-id** to identify previously made comments and update instead of creating a new comment. +### GitHub tokens + +When using the GitHub API, you always need to be authenticated. +This action tries to auto-authenticate using the [Automatic token authentication][link-gha-token] from GitHub. +You can overwrite the token by adding the `GITHUB_TOKEN` environment variable, or add the **github-token** input. +

with :heart: byCedric @@ -181,3 +186,4 @@ Every comment contains a generated **message-id** to identify previously made co [code-defaults]: ../src/actions/preview-comment.ts [link-actions]: https://help.github.com/en/categories/automating-your-workflow-with-github-actions +[link-gha-token]: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token diff --git a/preview-comment/action.yml b/preview-comment/action.yml index 666a1f0f..381b0712 100644 --- a/preview-comment/action.yml +++ b/preview-comment/action.yml @@ -25,6 +25,10 @@ inputs: message-id: description: A unique identifier to prevent multiple comments on the same pull request required: false + github-token: + description: GitHub access token to comment on PRs + required: false + default: ${{ github.token }} outputs: projectOwner: description: The resolved project owner diff --git a/src/actions/preview-comment.ts b/src/actions/preview-comment.ts index 0b26ae19..c78f8a75 100644 --- a/src/actions/preview-comment.ts +++ b/src/actions/preview-comment.ts @@ -20,6 +20,7 @@ export function commentInput() { message: getInput('message') || DEFAULT_MESSAGE, messageId: getInput('message-id') || DEFAULT_ID, project: getInput('project'), + githubToken: getInput('github-token'), }; } @@ -46,7 +47,9 @@ export async function commentAction(input: CommentInput = commentInput()) { if (!input.comment) { info(`Skipped comment: 'comment' is disabled`); } else { - await createIssueComment(pullContext(), { + await createIssueComment({ + ...pullContext(), + token: input.githubToken, id: messageId, body: messageBody, }); diff --git a/src/github.ts b/src/github.ts index d8ae006f..eb121418 100644 --- a/src/github.ts +++ b/src/github.ts @@ -3,6 +3,11 @@ import { ok as assert } from 'assert'; type IssueContext = typeof context['issue']; +type AuthContext = { + /** GitHub token from the 'github-input' to authenticate with */ + token?: string; +}; + type Comment = { /** A hidden identifier to embed in the comment */ id: string; @@ -14,17 +19,17 @@ type Comment = { * Determine if a comment exists on an issue or pull with the provided identifier. * This will iterate all comments received from GitHub, and try to exit early if it exists. */ -export async function fetchIssueComment(issue: IssueContext, commentId: Comment['id']) { - const github = githubApi(); +export async function fetchIssueComment(options: AuthContext & IssueContext & Pick) { + const github = githubApi(options); const iterator = github.paginate.iterator(github.rest.issues.listComments, { - owner: issue.owner, - repo: issue.repo, - issue_number: issue.number, + owner: options.owner, + repo: options.repo, + issue_number: options.number, }); for await (const { data: batch } of iterator) { for (const item of batch) { - if ((item.body || '').includes(commentId)) { + if ((item.body || '').includes(options.id)) { return item; } } @@ -36,35 +41,36 @@ export async function fetchIssueComment(issue: IssueContext, commentId: Comment[ * This includes a hidden identifier (markdown comment) to identify the comment later. * It will also update the comment when a previous comment id was found. */ -export async function createIssueComment(issue: IssueContext, comment: Comment) { - const github = githubApi(); - const body = `\n${comment.body}`; - const existing = await fetchIssueComment(issue, comment.id); +export async function createIssueComment(options: AuthContext & IssueContext & Comment) { + const github = githubApi(options); + const body = `\n${options.body}`; + const existing = await fetchIssueComment(options); if (existing) { return github.rest.issues.updateComment({ - owner: issue.owner, - repo: issue.repo, + owner: options.owner, + repo: options.repo, comment_id: existing.id, body, }); } return github.rest.issues.createComment({ - owner: issue.owner, - repo: issue.repo, - issue_number: issue.number, + owner: options.owner, + repo: options.repo, + issue_number: options.number, body, }); } /** * Get an authenticated octokit instance. - * This uses the 'GITHUB_TOKEN' environment variable. + * This uses the 'GITHUB_TOKEN' environment variable, or 'github-token' input. */ -export function githubApi(): ReturnType { - assert(process.env['GITHUB_TOKEN'], 'This step requires a GITHUB_TOKEN environment variable to create comments'); - return getOctokit(process.env['GITHUB_TOKEN']); +export function githubApi(options: AuthContext = {}): ReturnType { + const token = process.env['GITHUB_TOKEN'] || options.token; + assert(token, `This step requires 'github-token' or a GITHUB_TOKEN environment variable to create comments`); + return getOctokit(token); } /** diff --git a/tests/actions/preview-comment.test.ts b/tests/actions/preview-comment.test.ts index 8694d2e5..16e6aa4d 100644 --- a/tests/actions/preview-comment.test.ts +++ b/tests/actions/preview-comment.test.ts @@ -24,6 +24,7 @@ describe(commentInput, () => { message: DEFAULT_MESSAGE, messageId: DEFAULT_ID, project: undefined, + githubToken: undefined, }); }); @@ -46,6 +47,11 @@ describe(commentInput, () => { mockInput({ channel: 'pr-420' }); expect(commentInput()).toMatchObject({ channel: 'pr-420' }); }); + + it('returns github-token', () => { + mockInput({ 'github-token': 'fakegithubtoken' }); + expect(commentInput()).toMatchObject({ githubToken: 'fakegithubtoken' }); + }); }); describe(commentAction, () => { @@ -55,6 +61,7 @@ describe(commentAction, () => { message: DEFAULT_MESSAGE, messageId: DEFAULT_ID, project: '', + githubToken: '', }; beforeEach(() => { diff --git a/tests/github.test.ts b/tests/github.test.ts index 843ea3f6..01f4cd3e 100644 --- a/tests/github.test.ts +++ b/tests/github.test.ts @@ -8,18 +8,34 @@ jest.mock('@actions/github'); describe(githubApi, () => { afterEach(resetEnv); - it('throws when GITHUB_TOKEN is undefined', () => { + it('throws when GITHUB_TOKEN and input are undefined', () => { setEnv('GITHUB_TOKEN', ''); - expect(() => githubApi()).toThrow(`requires a GITHUB_TOKEN`); + expect(() => githubApi()).toThrow(`requires 'github-token' or a GITHUB_TOKEN`); }); - it('returns an octokit instance', () => { + it('returns octokit instance with GITHUB_TOKEN', () => { setEnv('GITHUB_TOKEN', 'fakegithubtoken'); const fakeGithub = {}; jest.mocked(github.getOctokit).mockReturnValue(fakeGithub as any); expect(githubApi()).toBe(fakeGithub); expect(github.getOctokit).toBeCalledWith('fakegithubtoken'); }); + + it('returns octokit instance with input', () => { + setEnv('GITHUB_TOKEN', ''); + const fakeGithub = {}; + jest.mocked(github.getOctokit).mockReturnValue(fakeGithub as any); + expect(githubApi({ token: 'fakegithubtoken' })).toBe(fakeGithub); + expect(github.getOctokit).toBeCalledWith('fakegithubtoken'); + }); + + it('uses GITHUB_TOKEN before input', () => { + setEnv('GITHUB_TOKEN', 'fakegithubtoken'); + const fakeGithub = {}; + jest.mocked(github.getOctokit).mockReturnValue(fakeGithub as any); + expect(githubApi({ token: 'badfakegithubtoken' })).toBe(fakeGithub); + expect(github.getOctokit).toBeCalledWith('fakegithubtoken'); + }); }); describe(pullContext, () => { From 367575d072a1b2fd3d948a3eec694d77ebfbe666 Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Mon, 24 Jan 2022 14:07:15 +0100 Subject: [PATCH 2/3] refactor; run tests on every pull request --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14d58335..d36eb1e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,8 @@ on: - cron: '0 15 * * *' push: branches: [main] + pull_request: + types: [opened, synchronize] workflow_dispatch: concurrency: From e25237b39e1ad2ddac6412848962c91fee5c2bba Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Mon, 24 Jan 2022 14:11:14 +0100 Subject: [PATCH 3/3] refactor: reword comment on pr tests --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d36eb1e7..9aee4eaf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: const message = `${{ steps.preview.outputs.message }}` if (!message) throw new Error('Message output is empty') - - name: 🧪 Comment on PR (input auth) + - name: 🧪 Comment on PR (github-token) uses: ./preview-comment env: EXPO_TEST_GITHUB_PULL: 149 @@ -87,7 +87,7 @@ jobs: project: ./temp channel: test - - name: 🧪 Comment on PR (envvar auth) + - name: 🧪 Comment on PR (GITHUB_TOKEN) uses: ./preview-comment env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -95,6 +95,6 @@ jobs: with: project: ./temp channel: test - github-token: '' + github-token: badtoken