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

ProposalPolice™ GH Actions Workflow #41038

Merged
merged 30 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d5dd18e
ProposalPolice™ GH Actions Workflow
ikevin127 Apr 25, 2024
dd16598
refactoring
ikevin127 Apr 27, 2024
709846a
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 Apr 30, 2024
d073a6c
new refactoring
ikevin127 May 8, 2024
741fc96
refactoring 2.0
ikevin127 May 25, 2024
35d0877
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 May 25, 2024
da27fb3
synced w/ main and npm ci, package-lock.json bump
ikevin127 May 25, 2024
0245beb
fixed lint and rebuilt
ikevin127 May 25, 2024
a85b61f
prettier
ikevin127 May 26, 2024
b69f414
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 May 28, 2024
00b8d89
github actions build
ikevin127 May 28, 2024
0d3c926
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 May 28, 2024
bf2f252
updated actions/ version
ikevin127 May 28, 2024
a949548
fixed action.yml errors
ikevin127 May 28, 2024
98dd06c
refactoring 3.0: OpenAIUtils, one action, constants
ikevin127 May 31, 2024
df9ef9c
refactoring 3.1: lint
ikevin127 May 31, 2024
623ca4d
refactoring 3.2: lint import
ikevin127 May 31, 2024
4ffbd42
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 Jun 22, 2024
090e17c
applied diff
ikevin127 Jun 22, 2024
5bf76b5
solved conflict
ikevin127 Jun 22, 2024
3f0a87f
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 Jun 22, 2024
edb3c56
added @octokit/webhooks-types and rebuilt gh-actions
ikevin127 Jun 22, 2024
036f890
fixed proposalPolice.yml
ikevin127 Jun 22, 2024
d252bff
lint, ready for review
ikevin127 Jun 22, 2024
02c2cf0
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 Jun 28, 2024
c04dc7c
added 'Proposal' keyword check
ikevin127 Jun 29, 2024
0b86bb7
clean install & rebuild
ikevin127 Jun 29, 2024
2cf10a1
Updated OpenAI keys
ikevin127 Jul 5, 2024
595eb1b
actions rebuild to fix validate fail
ikevin127 Jul 5, 2024
c0f2960
Merge branch 'main' of https://github.com/Expensify/App into feat/pro…
ikevin127 Jul 20, 2024
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
30 changes: 30 additions & 0 deletions .github/actions/javascript/proposalPoliceComment/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: ProposalPolice™ - Issue Comment Workflow
on:
issue_comment:
types: [created]

jobs:
process-issue-comment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "20"

- name: Install dependencies
run: npm install
working-directory: ../../../

# Checks if the comment is created and follows the template
- name: Run issue new comment script
if: github.event.action == 'created'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ASSISTANT_ID: ${{ secrets.OPENAI_ASSISTANT_ID }}
ISSUE: ${{ toJson(github.event.issue) }}
COMMENT: ${{ toJson(github.event.comment) }}
run: ./index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Import GitHub toolkit and Octokit REST client
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
import {context, getOctokit} from '@actions/github';
import InitOpenAI from 'openai';
import type {GitHubType} from '@github/libs/GithubUtils';

// @ts-ignore - process is not imported

Check failure on line 6 in .github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free
const OpenAI = new InitOpenAI({apiKey: process.env.OPENAI_API_KEY});

async function handleIssueCommentCreated(octokit: InstanceType<typeof GitHubType>, labelNames: string[]) {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
const payload = context.payload;
// @ts-ignore - process is not imported

Check failure on line 11 in .github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free
const OPENAI_ASSISTANT_ID = process.env.OPENAI_ASSISTANT_ID;

// check if the issue is opened and the has all passed labels
if (payload.issue?.state === 'open' && labelNames.every((labelName: string) => payload.issue?.labels.some((issueLabel: {name: string}) => issueLabel.name === labelName))) {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
if (!OPENAI_ASSISTANT_ID) {
console.log('OPENAI_ASSISTANT_ID missing from the environment variables');
return;
}

// 1, check if comment is proposal and if proposal template is followed
const content = `I NEED HELP WITH CASE (1.), CHECK IF COMMENT IS PROPOSAL AND IF TEMPLATE IS FOLLOWED AS PER INSTRUCTIONS. IT IS MANDATORY THAT YOU RESPOND ONLY WITH "NO_ACTION" IN CASE THE COMMENT IS NOT A PROPOSAL. Comment content: ${payload.comment?.body}`;

// create thread with first user message and run it
const createAndRunResponse = await OpenAI.beta.threads.createAndRun({
assistant_id: OPENAI_ASSISTANT_ID ?? '',

Check failure on line 26 in .github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

Object Literal Property name `assistant_id` must match one of the following formats: camelCase, UPPER_CASE, PascalCase
thread: {messages: [{role: 'user', content}]},
});

// count calls for debug purposes
let count = 0;
// poll for run completion
const intervalID = setInterval(() => {
OpenAI.beta.threads.runs
.retrieve(createAndRunResponse.thread_id, createAndRunResponse.id)
.then((run) => {

Check failure on line 36 in .github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

'run' is already declared in the upper scope on line 111 column 16
// return if run is not completed
if (run.status !== 'completed') {
return;
}

// get assistant response
OpenAI.beta.threads.messages
.list(createAndRunResponse.thread_id)
.then((threadMessages) => {
// list thread messages content
threadMessages.data.forEach((message, index) => {

Check failure on line 47 in .github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

Promise returned in function argument where a void return was expected
// @ts-ignore - we do have text value in content[0] but typescript doesn't know that

Check failure on line 48 in .github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free
// this is a 'openai' package type issue
let assistantResponse = message.content?.[index]?.text?.value;
console.log('issue_comment.created - assistantResponse', assistantResponse);

if (!assistantResponse) {
return console.log('issue_comment.created - assistantResponse is empty');
}

// check if assistant response is either NO_ACTION or "NO_ACTION" strings
// as sometimes the assistant response varies
const isNoAction = assistantResponse === 'NO_ACTION' || assistantResponse === '"NO_ACTION"';
// if assistant response is NO_ACTION or message role is 'user', do nothing
if (isNoAction || threadMessages.data?.[index]?.role === 'user') {
if (threadMessages.data?.[index]?.role === 'user') {
return;
}
return console.log('issue_comment.created - NO_ACTION');
}

// if the assistant responded with no action but there's some context in the response
if (assistantResponse.includes('[NO_ACTION]')) {
// extract the text after [NO_ACTION] from assistantResponse since this is a
// bot related action keyword
const noActionContext = assistantResponse.split('[NO_ACTION] ')?.[1]?.replace('"', '');
console.log('issue_comment.created - [NO_ACTION] w/ context: ', noActionContext);
return;
}
// replace {user} from response template with @username
assistantResponse = assistantResponse.replace('{user}', `@${payload.comment?.user.login}`);
// replace {proposalLink} from response template with the link to the comment
assistantResponse = assistantResponse.replace('{proposalLink}', payload.comment?.html_url);

// remove any double quotes from the final comment because sometimes the assistant's
// response contains double quotes / sometimes it doesn't
assistantResponse = assistantResponse.replace('"', '');
// create a comment with the assistant's response
console.log('issue_comment.created - proposal-police posts comment');
return octokit.issues.createComment({
...context.repo,
issue_number: payload.issue?.number as number,

Check failure on line 88 in .github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

Object Literal Property name `issue_number` must match one of the following formats: camelCase, UPPER_CASE, PascalCase

Check failure on line 88 in .github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

Use a ! assertion to more succinctly remove null and undefined from the type
body: assistantResponse,
});
});
})
.catch((err) => console.log('threads.messages.list - err', err));

// stop polling
clearInterval(intervalID);
})
.catch((err) => console.log('threads.runs.retrieve - err', err));

// increment count for every threads.runs.retrieve call
count++;
console.log('threads.runs.retrieve - called:', count);
}, 1500);
}

// return so that the script doesn't hang
return false;
}

// Main function to process the workflow event
async function run() {
// @ts-ignore - process is not imported

Check failure on line 112 in .github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free
const octokit: InstanceType<typeof GitHubType> = getOctokit(process.env.GITHUB_TOKEN);
await handleIssueCommentCreated(octokit, ['Help Wanted']);
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
}

run().catch((error) => {
console.error(error);
// @ts-ignore - process is not imported

Check failure on line 119 in .github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts

View workflow job for this annotation

GitHub Actions / Run ESLint

Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free
process.exit(1);
});
30 changes: 30 additions & 0 deletions .github/actions/javascript/proposalPoliceCommentEdit/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: ProposalPolice™ - Issue Comment Edit Workflow
on:
issue_comment:
types: [edited]

jobs:
process-issue-comment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "20"

- name: Install dependencies
run: npm install
working-directory: ../../../

# Checks if the comment is edited and if proposal template is followed
- name: Run issue comment edited script
if: github.event.action == 'edited'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ASSISTANT_ID: ${{ secrets.OPENAI_ASSISTANT_ID }}
ISSUE: ${{ toJson(github.event.issue) }}
COMMENT: ${{ toJson(github.event.comment) }}
run: ./index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Import GitHub toolkit and Octokit REST client
import {context, getOctokit} from '@actions/github';
import InitOpenAI from 'openai';
import type {GitHubType} from '@github/libs/GithubUtils';

// @ts-ignore - process is not imported
const OpenAI = new InitOpenAI({apiKey: process.env.OPENAI_API_KEY});

async function handleIssueCommentEdited(octokit: InstanceType<typeof GitHubType>, labelNames: string[]) {
const payload = context.payload;
// @ts-ignore - process is not imported
const OPENAI_ASSISTANT_ID = process.env.OPENAI_ASSISTANT_ID;

// check if the issue is opened and the has all passed labels
if (payload.issue?.state === 'open' && labelNames.every((labelName: string) => payload.issue?.labels.some((issueLabel: {name: string}) => issueLabel.name === labelName))) {
if (!OPENAI_ASSISTANT_ID) {
console.log('OPENAI_ASSISTANT_ID missing from the environment variables');
return;
}

// You need to adapt this part to fit the Edit Use Case as in the original function
const content = `I NEED HELP WITH CASE (2.) WHEN A USER THAT POSTED AN INITIAL PROPOSAL OR COMMENT (UNEDITED) THEN EDITS THE COMMENT - WE NEED TO CLASSIFY THE COMMENT BASED IN THE GIVEN INSTRUCTIONS AND IF TEMPLATE IS FOLLOWED AS PER INSTRUCTIONS. IT IS MANDATORY THAT YOU RESPOND ONLY WITH "NO_ACTION" IN CASE THE COMMENT IS NOT A PROPOSAL. \n\nPrevious comment content: ${payload.changes.body?.from}.\n\nEdited comment content: ${payload.comment?.body}`;

// create thread with first user message and run it
const createAndRunResponse = await OpenAI.beta.threads.createAndRun({
assistant_id: OPENAI_ASSISTANT_ID ?? '',
thread: {messages: [{role: 'user', content}]},
});

// count calls for debug purposes
let count = 0;
// poll for run completion
const intervalID = setInterval(() => {
OpenAI.beta.threads.runs
.retrieve(createAndRunResponse.thread_id, createAndRunResponse.id)
.then((run) => {
// return if run is not completed yet
if (run.status !== 'completed') {
console.log('issue_comment.edited - run pending completion');
return;
}

// get assistant response
OpenAI.beta.threads.messages
.list(createAndRunResponse.thread_id)
.then((threadMessages) => {
// list thread messages content
threadMessages.data.forEach((message, index) => {
// @ts-ignore - we do have text value in content[0] but typescript doesn't know that
// this is a 'openai' package type issue
let assistantResponse = message.content?.[index]?.text?.value;
console.log('issue_comment.edited - assistantResponse', assistantResponse);

if (!assistantResponse) {
return console.log('issue_comment.edited - assistantResponse is empty');
}

// check if assistant response is either NO_ACTION or "NO_ACTION" strings
// as sometimes the assistant response varies
const isNoAction = assistantResponse === 'NO_ACTION' || assistantResponse === '"NO_ACTION"';
// if assistant response is NO_ACTION or message role is 'user', do nothing
if (isNoAction || threadMessages.data?.[index]?.role === 'user') {
if (threadMessages.data?.[index]?.role === 'user') {
return;
}
return console.log('issue_comment.edited - NO_ACTION');
}

// edit comment if assistant detected substantial changes and if the comment was not edited already by the bot
if (assistantResponse.includes('[EDIT_COMMENT]') && !payload.comment?.body.includes('Edited by **proposal-police**')) {
// extract the text after [EDIT_COMMENT] from assistantResponse since this is a
// bot related action keyword
let extractedNotice = assistantResponse.split('[EDIT_COMMENT] ')?.[1]?.replace('"', '');
// format the github's updated_at like: 2024-01-24 13:15:24 UTC not 2024-01-28 18:18:28.000 UTC
const date = new Date(payload.comment?.updated_at);
const formattedDate = date.toISOString()?.split('.')?.[0]?.replace('T', ' ') + ' UTC';
extractedNotice = extractedNotice.replace('{updated_timestamp}', formattedDate);

console.log(`issue_comment.edited - proposal-police edits comment: ${payload.comment?.id}`);
return octokit.issues.updateComment({
...context.repo,
comment_id: payload.comment?.id as number,
body: `${extractedNotice}\n\n` + payload.comment?.body,
});
}

return false;
});
})
.catch((err) => console.log('threads.messages.list - err', err));

// stop polling
clearInterval(intervalID);
})
.catch((err) => console.log('threads.runs.retrieve - err', err));

// increment count for every threads.runs.retrieve call
count++;
console.log('threads.runs.retrieve - called:', count);
}, 1500);
}

// return so that the script doesn't hang
return false;
}

async function run() {
// @ts-ignore - process is not imported
const octokit: InstanceType<typeof GitHubType> = getOctokit(process.env.GITHUB_TOKEN);
await handleIssueCommentEdited(octokit, ['Help Wanted']);
}

run().catch((error) => {
console.error(error);
// @ts-ignore - process is not imported
process.exit(1);
});
14 changes: 13 additions & 1 deletion .github/libs/GithubUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/naming-convention, import/no-import-module-exports */
import * as core from '@actions/core';
import {getOctokitOptions, GitHub} from '@actions/github/lib/utils';
import type {Octokit as OctokitCore} from '@octokit/core';
import type {Octokit, Octokit as OctokitCore} from '@octokit/core';
Copy link
Contributor

Choose a reason for hiding this comment

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

You've imported the same thing twice here

import type {Constructor} from '@octokit/core/dist-types/types';
import type {graphql} from '@octokit/graphql/dist-types/types';
import type {components as OctokitComponents} from '@octokit/openapi-types/types';
import type {PaginateInterface} from '@octokit/plugin-paginate-rest';
Expand Down Expand Up @@ -534,4 +535,15 @@ export default GithubUtils;
// This is a temporary solution to allow the use of the GithubUtils class in both TypeScript and JavaScript.
// Once all the files that import GithubUtils are migrated to TypeScript, this can be removed.

declare const GitHubType: (new (...args: unknown[]) => Record<string, unknown>) & {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
new (...args: unknown[]): Record<string, unknown>;
plugins: unknown[];
} & typeof Octokit &
Constructor<
RestEndpointMethods & {
paginate: PaginateInterface;
}
>;

export {GitHubType};
export type {ListForRepoMethod, InternalOctokit, CreateCommentResponse, StagingDeployCashData};
2 changes: 2 additions & 0 deletions .github/scripts/buildActions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ declare -r GITHUB_ACTIONS=(
"$ACTIONS_DIR/validateReassureOutput/validateReassureOutput.ts"
"$ACTIONS_DIR/getGraphiteString/getGraphiteString.ts"
"$ACTIONS_DIR/getArtifactInfo/getArtifactInfo.ts"
"$ACTIONS_DIR/proposalPoliceComment/proposalPoliceComment.ts"
"$ACTIONS_DIR/proposalPoliceCommentEdit/proposalPoliceCommentEdit.ts"
)

# This will be inserted at the top of all compiled files as a warning to devs.
Expand Down
Loading
Loading