diff --git a/.github/workflows/build-genai.yml b/.github/workflows/build-genai.yml index 62f31fcbc9..535b44c75a 100644 --- a/.github/workflows/build-genai.yml +++ b/.github/workflows/build-genai.yml @@ -10,7 +10,9 @@ on: - "packages/core/**" - "packages/sample/**" - "packages/cli/**" - +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 16cc533c6a..a67de355c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,12 +5,15 @@ on: branches: [main] pull_request: workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build: runs-on: ubuntu-latest strategy: - matrix: - node-version: [20, 22] + matrix: + node-version: [20, 22] steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/genai-alt-text.yml b/.github/workflows/genai-alt-text.yml index 31d305d584..ff4d4c98ba 100644 --- a/.github/workflows/genai-alt-text.yml +++ b/.github/workflows/genai-alt-text.yml @@ -6,7 +6,9 @@ on: pull_request: paths: - "docs/src/**/*.png" - +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/genai-frontmatter.yml b/.github/workflows/genai-frontmatter.yml index 6c81dcf3ea..b2b24ccfec 100644 --- a/.github/workflows/genai-frontmatter.yml +++ b/.github/workflows/genai-frontmatter.yml @@ -7,6 +7,9 @@ on: paths: - "docs/src/**/*.md" - "docs/src/**/*.mdx" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/genai-pr-review.yml b/.github/workflows/genai-pr-review.yml index 5a762eba38..a524346f8d 100644 --- a/.github/workflows/genai-pr-review.yml +++ b/.github/workflows/genai-pr-review.yml @@ -1,13 +1,16 @@ name: genai pull request review on: pull_request: - types: [ready_for_review] + types: [opened, ready_for_review, reopened] paths: - yarn.lock - ".github/workflows/ollama.yml" - "packages/core/**/*" - "packages/cli/**/*" - "packages/samples/**/*" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/ollama.yml b/.github/workflows/ollama.yml index 10d3663b31..55b361e9a8 100644 --- a/.github/workflows/ollama.yml +++ b/.github/workflows/ollama.yml @@ -17,6 +17,9 @@ on: - "packages/core/**/*" - "packages/cli/**/*" - "packages/samples/**/*" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: tests: runs-on: ubuntu-latest diff --git a/docs/src/content/docs/reference/cli/commands.md b/docs/src/content/docs/reference/cli/commands.md index 01b7852e68..5d18546300 100644 --- a/docs/src/content/docs/reference/cli/commands.md +++ b/docs/src/content/docs/reference/cli/commands.md @@ -22,10 +22,9 @@ Options: -od, --out-data output file for data (.jsonl/ndjson will be aggregated). JSON schema information and validation will be included if available. -oa, --out-annotations output file for annotations (.csv will be rendered as csv, .jsonl/ndjson will be aggregated) -ocl, --out-changelog output file for changelogs - -prc, --pull-request-comment [string] create comment on a pull request. - -prd, --pull-request-description [string] upsert comment on a pull request description. + -prc, --pull-request-comment [string] create comment on a pull request with a unique id (defaults to script id) + -prd, --pull-request-description [string] create comment on a pull request description with a unique id (defaults to script id) -prr, --pull-request-reviews create pull request reviews from annotations - --no-pull-request-reviews-cache disable pull request reviews cache -j, --json emit full JSON response to output -y, --yaml emit full YAML response to output -p, --prompt dry run, don't execute LLM and return expanded prompt diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index ab76fa5fd0..f81c0f9392 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -102,20 +102,16 @@ export async function cli() { .option("-ocl, --out-changelog ", "output file for changelogs") .option( "-prc, --pull-request-comment [string]", - "create comment on a pull request." + "create comment on a pull request with a unique id (defaults to script id)" ) .option( "-prd, --pull-request-description [string]", - "upsert comment on a pull request description." + "create comment on a pull request description with a unique id (defaults to script id)" ) .option( "-prr, --pull-request-reviews", "create pull request reviews from annotations" ) - .option( - "--no-pull-request-reviews-cache", - "disable pull request reviews cache" - ) .option("-j, --json", "emit full JSON response to output") .option("-y, --yaml", "emit full YAML response to output") .option( diff --git a/packages/cli/src/run.ts b/packages/cli/src/run.ts index e621fa1d49..87a86697c0 100644 --- a/packages/cli/src/run.ts +++ b/packages/cli/src/run.ts @@ -32,13 +32,9 @@ import { CSV_REGEX, CLI_RUN_FILES_FOLDER, parseGHTokenFromEnv, - githubUpsetPullRequest, + githubUpdatePullRequestDescription, githubCreatePullRequestReviews, githubCreateIssueComment, - GITHUB_PULL_REQUEST_REVIEWS_CACHE, - JSONLineCache, - PullRequestReviewsCacheKey, - PullRequestReviewsCacheValue, } from "genaiscript-core" import { capitalize } from "inflection" import { basename, resolve, join, relative } from "node:path" @@ -66,7 +62,6 @@ export async function runScript( pullRequestComment: string | boolean pullRequestDescription: string | boolean pullRequestReviews: boolean - pullRequestReviewsCache: boolean outData: string label: string temperature: string @@ -106,7 +101,6 @@ export async function runScript( const maxTokens = normalizeInt(options.maxTokens) const maxToolCalls = normalizeInt(options.maxToolCalls) const cache = !!options.cache - const pullRequestReviewsCache = !!options.pullRequestReviewsCache const applyEdits = !!options.applyEdits const csvSeparator = options.csvSeparator || "\t" const removeOut = options.removeOut @@ -375,18 +369,7 @@ ${Array.from(files) if (pullRequestReviews && res.annotations?.length) { const info = parseGHTokenFromEnv(process.env) if (info.repository && info.issue) { - const cache = pullRequestReviewsCache - ? JSONLineCache.byName< - PullRequestReviewsCacheKey, - PullRequestReviewsCacheValue - >(GITHUB_PULL_REQUEST_REVIEWS_CACHE) - : undefined - await githubCreatePullRequestReviews( - script, - info, - res.annotations, - { cache } - ) + await githubCreatePullRequestReviews(script, info, res.annotations) } } @@ -407,7 +390,7 @@ ${Array.from(files) if (pullRequestDescription && res.text) { const info = parseGHTokenFromEnv(process.env) if (info.repository && info.issue) { - await githubUpsetPullRequest( + await githubUpdatePullRequestDescription( script, info, res.text, diff --git a/packages/core/src/github.ts b/packages/core/src/github.ts index 7532b70c88..abb5b01354 100644 --- a/packages/core/src/github.ts +++ b/packages/core/src/github.ts @@ -1,15 +1,9 @@ import { assert } from "node:console" -import { - GITHUB_API_VERSION, - GITHUB_PULL_REQUEST_REVIEWS_CACHE, - GITHUB_TOKEN, -} from "./constants" +import { GITHUB_API_VERSION, GITHUB_TOKEN } from "./constants" import { createFetch } from "./fetch" import { host } from "./host" import { link, prettifyMarkdown } from "./markdown" import { logError, logVerbose, normalizeInt } from "./util" -import { string } from "mathjs" -import { JSONLineCache } from "./cache" export interface GithubConnectionInfo { token: string @@ -61,13 +55,14 @@ export function parseGHTokenFromEnv( } // https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#update-a-pull-request -export async function githubUpsetPullRequest( +export async function githubUpdatePullRequestDescription( script: PromptScript, info: GithubConnectionInfo, text: string, commentTag: string ) { const { apiUrl, repository, issue } = info + assert(commentTag) if (!issue) return { updated: false, statusText: "missing issue number" } const token = await host.readSecret(GITHUB_TOKEN) @@ -89,15 +84,21 @@ export async function githubUpsetPullRequest( const resGetJson = (await resGet.json()) as { body: string } let { body } = resGetJson if (!body) body = "" - const tag = `\n\n\n\n` - const endTag = `\n\n\n\n` + const tag = `` + const endTag = `` + const sep = "\n\n" const start = body.indexOf(tag) const end = body.indexOf(endTag) if (start > -1 && end > -1 && start < end) { - body = body.slice(0, start + tag.length) + text + body.slice(end) + body = + body.slice(0, start + tag.length) + + sep + + text + + sep + + body.slice(end) } else { - body = body + tag + text + endTag + body = body + sep + tag + sep + text + sep + endTag + sep } const res = await fetch(url, { @@ -156,20 +157,22 @@ export async function githubCreateIssueComment( const tag = `` body = `${body}\n\n${tag}\n\n` // try to find the existing comment - const resListComments = await fetch(`${url}?per_page=100`, { - headers: { - Accept: "application/vnd.github+json", - Authorization: `Bearer ${token}`, - "X-GitHub-Api-Version": GITHUB_API_VERSION, - }, - }) + const resListComments = await fetch( + `${url}?per_page=100&sort=updated`, + { + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": GITHUB_API_VERSION, + }, + } + ) if (resListComments.status !== 200) return { created: false, statusText: resListComments.statusText } const comments = (await resListComments.json()) as { id: string body: string }[] - const comment = comments.find((c) => c.body.includes(tag)) if (comment) { const delurl = `${apiUrl}/repos/${repository}/issues/comments/${comment.id}` @@ -213,13 +216,12 @@ async function githubCreatePullRequestReview( script: PromptScript, info: GithubConnectionInfo, token: string, - annotation: Diagnostic + annotation: Diagnostic, + existingComments: { id: string; path: string; line: number; body: string }[] ) { assert(token) const { apiUrl, repository, issue, commitSha } = info - const fetch = await createFetch({ retryOn: [] }) - const url = `${apiUrl}/repos/${repository}/pulls/${issue}/comments` const body = { body: appendGeneratedComment(script, info, annotation.message), commit_id: commitSha, @@ -227,6 +229,21 @@ async function githubCreatePullRequestReview( line: annotation.range?.[0]?.[0], side: "RIGHT", } + if ( + existingComments.find( + (c) => + c.path === body.path && + c.line === body.line && + c.body === body.body + ) + ) { + logVerbose( + `pull request ${commitSha} comment creation already exists, skipping` + ) + return { created: false, statusText: "comment already exists" } + } + const fetch = await createFetch({ retryOn: [] }) + const url = `${apiUrl}/repos/${repository}/pulls/${issue}/comments` const res = await fetch(url, { method: "POST", headers: { @@ -251,36 +268,12 @@ async function githubCreatePullRequestReview( return r } -export interface PullRequestReviewsCacheKey { - repository: string - issue: number - scriptId: string - message: string - filename: string - line: number -} - -export interface PullRequestReviewsCacheValue { - created: boolean - statusText: string - html_url: string -} - -export type PullRequestReviewsCache = JSONLineCache< - PullRequestReviewsCacheKey, - PullRequestReviewsCacheValue -> - export async function githubCreatePullRequestReviews( script: PromptScript, info: GithubConnectionInfo, - annotations: Diagnostic[], - options?: { - cache?: PullRequestReviewsCache - } + annotations: Diagnostic[] ): Promise { - const { repository, issue, sha } = info - const { cache } = options || {} + const { repository, issue, sha, apiUrl } = info if (!annotations?.length) return true if (!issue) { @@ -297,28 +290,32 @@ export async function githubCreatePullRequestReviews( return false } + // query existing reviews + const fetch = await createFetch({ retryOn: [] }) + const url = `${apiUrl}/repos/${repository}/pulls/${issue}/comments` + const resListComments = await fetch(`${url}?per_page=100&sort=updated`, { + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": GITHUB_API_VERSION, + }, + }) + if (resListComments.status !== 200) return false + const comments = (await resListComments.json()) as { + id: string + path: string + line: number + body: string + }[] // code annotations for (const annotation of annotations) { - const cacheKey = { - repository, - issue, - scriptId: script.id, - message: annotation.message, - filename: annotation.filename, - line: annotation.range?.[0]?.[0], - } - const cached = await cache?.get(cacheKey) - if (cached) - logVerbose("ignore cached pull request review, " + cached.html_url) - else { - const res = await githubCreatePullRequestReview( - script, - info, - token, - annotation - ) - if (res.created) await cache?.set(cacheKey, res) - } + await githubCreatePullRequestReview( + script, + info, + token, + annotation, + comments + ) } return true } diff --git a/packages/sample/genaisrc/pr-describe.genai.js b/packages/sample/genaisrc/pr-describe.genai.js index c81c74dfc9..3384676b71 100644 --- a/packages/sample/genaisrc/pr-describe.genai.js +++ b/packages/sample/genaisrc/pr-describe.genai.js @@ -29,6 +29,7 @@ $`You are an expert software developer and architect. ## Instructions +- separate changes that affect the CLI from changes that affect the library - use bullet points to list the changes - use emojis to make the description more engaging - focus on the most important changes