Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(preview-comment): auto-fill the github token #156

Merged
merged 3 commits into from
Jan 25, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ on:
- cron: '0 15 * * *'
push:
branches: [main]
pull_request:
types: [opened, synchronize]
workflow_dispatch:

concurrency:
@@ -76,14 +78,23 @@ jobs:
script: |
const message = `${{ steps.preview.outputs.message }}`
if (!message) throw new Error('Message output is empty')

- name: 🧪 Comment on PR (github-token)
uses: ./preview-comment
env:
EXPO_TEST_GITHUB_PULL: 149
with:
project: ./temp
channel: test

- name: 🧪 Comment on PR
- name: 🧪 Comment on PR (GITHUB_TOKEN)
uses: ./preview-comment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EXPO_TEST_GITHUB_PULL: 149
with:
project: ./temp
channel: test
github-token: badtoken


2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 }}
```
44 changes: 24 additions & 20 deletions build/preview-comment/index.js
Original file line number Diff line number Diff line change
@@ -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 = `<!-- ${comment.id} -->\n${comment.body}`;
const existing = await fetchIssueComment(issue, comment.id);
async function createIssueComment(options) {
const github = githubApi(options);
const body = `<!-- ${options.id} -->\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;
/**
10 changes: 8 additions & 2 deletions preview-comment/README.md
Original file line number Diff line number Diff line change
@@ -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.

<div align="center">
<br />
with :heart:&nbsp;<strong>byCedric</strong>
@@ -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
4 changes: 4 additions & 0 deletions preview-comment/action.yml
Original file line number Diff line number Diff line change
@@ -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 }}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the magic happens, it uses the github context to auto-fill the token on github-token.

outputs:
projectOwner:
description: The resolved project owner
5 changes: 4 additions & 1 deletion src/actions/preview-comment.ts
Original file line number Diff line number Diff line change
@@ -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,
});
44 changes: 25 additions & 19 deletions src/github.ts
Original file line number Diff line number Diff line change
@@ -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<Comment, 'id'>) {
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 = `<!-- ${comment.id} -->\n${comment.body}`;
const existing = await fetchIssueComment(issue, comment.id);
export async function createIssueComment(options: AuthContext & IssueContext & Comment) {
const github = githubApi(options);
const body = `<!-- ${options.id} -->\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<typeof getOctokit> {
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<typeof getOctokit> {
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);
}

/**
7 changes: 7 additions & 0 deletions tests/actions/preview-comment.test.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
22 changes: 19 additions & 3 deletions tests/github.test.ts
Original file line number Diff line number Diff line change
@@ -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, () => {