From 347a64333eedd76d778a2c00b490cbb9a31e4b94 Mon Sep 17 00:00:00 2001 From: Docker Agent Date: Fri, 8 May 2026 12:57:21 +0000 Subject: [PATCH 1/7] fix: use repo-scoped token for pulls.get in Check if org member step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ORG_MEMBERSHIP_TOKEN only has read:org scope — it lacks repo scope. When PR_SOURCE is 'trigger' (workflow_run path), the else branch calls github.rest.pulls.get() using that token, which returns 404 and crashes the step with HttpError: Not Found. Fix: add REPO_TOKEN env var (GITHUB_APP_TOKEN or github.token, both of which carry repo scope) and create a secondary octokit client with it exclusively for the pulls.get call. The primary github client (backed by ORG_MEMBERSHIP_TOKEN) is kept for checkMembershipForUser only. --- .github/workflows/review-pr.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/review-pr.yml b/.github/workflows/review-pr.yml index a2d97d8..aa21f69 100644 --- a/.github/workflows/review-pr.yml +++ b/.github/workflows/review-pr.yml @@ -269,6 +269,7 @@ jobs: PR_SOURCE: ${{ steps.pr.outputs.source }} ORG: docker COMMENT_AUTHOR: ${{ github.event.comment.user.login }} + REPO_TOKEN: ${{ env.GITHUB_APP_TOKEN || github.token }} with: github-token: ${{ env.ORG_MEMBERSHIP_TOKEN }} script: | @@ -279,7 +280,8 @@ jobs: if (source === 'event') { username = process.env.COMMENT_AUTHOR; } else { - const { data: pr } = await github.rest.pulls.get({ + const repoOctokit = github.getOctokit(process.env.REPO_TOKEN); + const { data: pr } = await repoOctokit.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: parseInt(process.env.PR_NUMBER, 10) From a40d3ccf93389a5f6c1fa717c073a6aede097eb0 Mon Sep 17 00:00:00 2001 From: Docker Agent Date: Fri, 8 May 2026 13:09:56 +0000 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20remove=20github.token=20fallback=20f?= =?UTF-8?q?rom=20REPO=5FTOKEN=20=E2=80=94=20use=20GITHUB=5FAPP=5FTOKEN=20o?= =?UTF-8?q?nly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per security design, GITHUB_APP_TOKEN is the PAT fetched from AWS Secrets Manager and must be the sole token for non-membership operations. A silent fallback to github.token is not acceptable — the setup-credentials step already guarantees GITHUB_APP_TOKEN is present before this step runs. --- .github/workflows/review-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/review-pr.yml b/.github/workflows/review-pr.yml index aa21f69..c94bd9a 100644 --- a/.github/workflows/review-pr.yml +++ b/.github/workflows/review-pr.yml @@ -269,7 +269,7 @@ jobs: PR_SOURCE: ${{ steps.pr.outputs.source }} ORG: docker COMMENT_AUTHOR: ${{ github.event.comment.user.login }} - REPO_TOKEN: ${{ env.GITHUB_APP_TOKEN || github.token }} + REPO_TOKEN: ${{ env.GITHUB_APP_TOKEN }} with: github-token: ${{ env.ORG_MEMBERSHIP_TOKEN }} script: | From e9a0c6af4c917f051184cfb96eb82586dcf87084 Mon Sep 17 00:00:00 2001 From: Docker Agent Date: Fri, 8 May 2026 13:23:38 +0000 Subject: [PATCH 3/7] refactor: move Check if org member logic to TypeScript bundle - Add resolvePrAuthor(repoToken, owner, repo, prNumber) to check-org-membership module: fetches PR author via pulls.get using a repo-scoped token, keeping the token split (ORG_MEMBERSHIP_TOKEN only for checkMembershipForUser, GITHUB_APP_TOKEN for pulls.get) as a first-class concern in the type-safe layer. - Add CLI entry point (main()) guarded by process.argv[1] filename check so the module continues to work as a library imported by mention-reply and main/auth without triggering the CLI code when bundled into those distributions. - Add check-org-membership to tsup entry map so the module gets its own dist/check-org-membership.js standalone bundle. - Add .github/actions/check-org-membership/action.yml (node24 action) that wraps the bundle with well-typed inputs (org-membership-token, github-app-token, org, pr-source, pr-number, comment-author) and output (is_member). - Extend unit tests: resolvePrAuthor happy/sad paths and a token-isolation test that asserts the two Octokit instances receive their respective tokens in order. The follow-up commit will update review-pr.yml to use this action via docker/cagent-action/.github/actions/check-org-membership@. --- .../actions/check-org-membership/action.yml | 32 +++++ .../__tests__/check-org-membership.test.ts | 100 +++++++++++++-- src/check-org-membership/index.ts | 120 +++++++++++++++++- tsup.config.ts | 8 +- 4 files changed, 241 insertions(+), 19 deletions(-) create mode 100644 .github/actions/check-org-membership/action.yml diff --git a/.github/actions/check-org-membership/action.yml b/.github/actions/check-org-membership/action.yml new file mode 100644 index 0000000..d14c074 --- /dev/null +++ b/.github/actions/check-org-membership/action.yml @@ -0,0 +1,32 @@ +name: 'Check Org Membership' +description: > + Verify whether a GitHub user belongs to a GitHub org. + Resolves the PR author via the GitHub API (using a repo-scoped token) when + pr-source is not "event"; uses comment-author directly for event-sourced triggers. +inputs: + org-membership-token: + description: 'PAT with read:org scope for org membership checks' + required: true + github-app-token: + description: 'PAT with repo scope; used to call pulls.get when pr-source is not "event"' + required: true + org: + description: 'GitHub org name to check membership against (e.g. "docker")' + required: true + pr-source: + description: 'Trigger source: "event" (uses comment-author) or any other value (uses pr-number + pulls.get)' + required: true + pr-number: + description: 'PR number as a string; required when pr-source is not "event"' + required: false + default: '' + comment-author: + description: 'GitHub login of the user who triggered the event; required when pr-source is "event"' + required: false + default: '' +outputs: + is_member: + description: '"true" if the resolved user is a member of the org, "false" otherwise' +runs: + using: 'node24' + main: '../../../dist/check-org-membership.js' diff --git a/src/check-org-membership/__tests__/check-org-membership.test.ts b/src/check-org-membership/__tests__/check-org-membership.test.ts index ffaa547..ef7c612 100644 --- a/src/check-org-membership/__tests__/check-org-membership.test.ts +++ b/src/check-org-membership/__tests__/check-org-membership.test.ts @@ -1,25 +1,47 @@ -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('@actions/core'); -const { mockCheckMembershipForUser, MockOctokit } = vi.hoisted(() => { - const mockCheckMembershipForUser = vi.fn().mockResolvedValue({}); // 204 = member - - class MockOctokit { - rest = { orgs: { checkMembershipForUser: mockCheckMembershipForUser } }; - } - - return { mockCheckMembershipForUser, MockOctokit }; -}); +const { mockCheckMembershipForUser, mockGetPull, MockOctokit, constructorTokens } = vi.hoisted( + () => { + const mockCheckMembershipForUser = vi.fn().mockResolvedValue({}); // 204 = member + const mockGetPull = vi.fn().mockResolvedValue({ data: { user: { login: 'bob' } } }); + + // Track which auth token was passed to each new Octokit() instance, in order. + // Index 0 = first instance created, 1 = second, etc. + const constructorTokens: string[] = []; + + class MockOctokit { + constructor({ auth }: { auth: string }) { + constructorTokens.push(auth); + } + rest = { + orgs: { checkMembershipForUser: mockCheckMembershipForUser }, + pulls: { get: mockGetPull }, + }; + } + + return { mockCheckMembershipForUser, mockGetPull, MockOctokit, constructorTokens }; + }, +); vi.mock('@octokit/rest', () => ({ Octokit: MockOctokit })); -import { checkOrgMembership } from '../index.js'; +import { checkOrgMembership, resolvePrAuthor } from '../index.js'; const ORG_TOKEN = 'fake-org-token'; +const REPO_TOKEN = 'fake-repo-token'; const ORG = 'docker'; const USERNAME = 'alice'; +beforeEach(() => { + constructorTokens.length = 0; +}); + +// --------------------------------------------------------------------------- +// checkOrgMembership +// --------------------------------------------------------------------------- + describe('checkOrgMembership', () => { it('returns true when the API returns 204 (member confirmed)', async () => { mockCheckMembershipForUser.mockResolvedValueOnce({}); @@ -71,3 +93,59 @@ describe('checkOrgMembership', () => { ); }); }); + +// --------------------------------------------------------------------------- +// resolvePrAuthor +// --------------------------------------------------------------------------- + +describe('resolvePrAuthor', () => { + it('returns the PR author login', async () => { + mockGetPull.mockResolvedValueOnce({ data: { user: { login: 'charlie' } } }); + + const login = await resolvePrAuthor(REPO_TOKEN, 'docker', 'myrepo', 42); + + expect(login).toBe('charlie'); + expect(mockGetPull).toHaveBeenCalledWith({ owner: 'docker', repo: 'myrepo', pull_number: 42 }); + }); + + it('returns empty string when user is null (e.g. deleted account)', async () => { + mockGetPull.mockResolvedValueOnce({ data: { user: null } }); + + const login = await resolvePrAuthor(REPO_TOKEN, 'docker', 'myrepo', 7); + + expect(login).toBe(''); + }); + + it('uses a separate Octokit instance with the repo token, not the org token', async () => { + mockGetPull.mockResolvedValueOnce({ data: { user: { login: 'dave' } } }); + + await resolvePrAuthor(REPO_TOKEN, 'docker', 'myrepo', 1); + + // The single Octokit instance created by resolvePrAuthor should use REPO_TOKEN + expect(constructorTokens).toEqual([REPO_TOKEN]); + }); + + it('propagates API errors', async () => { + mockGetPull.mockRejectedValueOnce(Object.assign(new Error('Not Found'), { status: 404 })); + + await expect(resolvePrAuthor(REPO_TOKEN, 'docker', 'myrepo', 999)).rejects.toThrow('Not Found'); + }); +}); + +// --------------------------------------------------------------------------- +// Token isolation: checkOrgMembership uses ORG_TOKEN, resolvePrAuthor uses REPO_TOKEN +// --------------------------------------------------------------------------- + +describe('token isolation', () => { + it('checkOrgMembership uses orgToken, resolvePrAuthor uses repoToken — never swapped', async () => { + mockCheckMembershipForUser.mockResolvedValueOnce({}); + mockGetPull.mockResolvedValueOnce({ data: { user: { login: 'eve' } } }); + + await checkOrgMembership(ORG_TOKEN, ORG, USERNAME); + await resolvePrAuthor(REPO_TOKEN, 'docker', 'myrepo', 5); + + // First Octokit instance (for checkOrgMembership) must use ORG_TOKEN + // Second Octokit instance (for resolvePrAuthor) must use REPO_TOKEN + expect(constructorTokens).toEqual([ORG_TOKEN, REPO_TOKEN]); + }); +}); diff --git a/src/check-org-membership/index.ts b/src/check-org-membership/index.ts index 968a57d..6efff33 100644 --- a/src/check-org-membership/index.ts +++ b/src/check-org-membership/index.ts @@ -1,16 +1,33 @@ /** * check-org-membership — verify whether a GitHub user belongs to an org. * - * Exported function: checkOrgMembership(orgToken, org, username) → boolean + * Exported functions: + * checkOrgMembership(orgToken, org, username) → boolean + * resolvePrAuthor(repoToken, owner, repo, prNumber) → string * - * HTTP 204 → true (member confirmed). - * HTTP 302/404 → false (not a member or token lacks visibility). - * HTTP 401 → throws a descriptive error (bad token). + * CLI (node24 action, invoked via .github/actions/check-org-membership/action.yml): + * Inputs (via @actions/core.getInput): + * org-membership-token PAT with read:org scope (for checkMembershipForUser) + * github-app-token PAT with repo scope (for pulls.get when pr-source != 'event') + * org GitHub org name (e.g. "docker") + * pr-source "event" | "trigger" | "input" + * pr-number PR number as string (required when pr-source != 'event') + * comment-author User login (required when pr-source == 'event') + * Standard env vars: + * GITHUB_REPOSITORY "owner/repo" (standard GitHub Actions env var) + * Outputs (via @actions/core.setOutput): + * is_member "true" | "false" + * + * Guard: the CLI entry point only executes when process.argv[1] ends with + * "check-org-membership.js" and VITEST is not set. This prevents the CLI from + * firing when this module is bundled into dist/mention-reply.js or dist/main.js + * as a library dependency. */ +import * as core from '@actions/core'; import { Octokit } from '@octokit/rest'; // --------------------------------------------------------------------------- -// Core function +// Core function: membership check // --------------------------------------------------------------------------- /** @@ -41,3 +58,96 @@ export async function checkOrgMembership( throw err; } } + +// --------------------------------------------------------------------------- +// Core function: PR author resolution +// --------------------------------------------------------------------------- + +/** + * Fetch the login of the PR author via the GitHub REST API. + * + * Uses `repoToken` (must have repo scope) — intentionally separate from + * `orgToken` so the read:org token is never used for repo-scoped API calls. + */ +export async function resolvePrAuthor( + repoToken: string, + owner: string, + repo: string, + prNumber: number, +): Promise { + const octokit = new Octokit({ auth: repoToken }); + const { data: pr } = await octokit.rest.pulls.get({ owner, repo, pull_number: prNumber }); + return pr.user?.login ?? ''; +} + +// --------------------------------------------------------------------------- +// CLI entry point +// --------------------------------------------------------------------------- + +async function main(): Promise { + const orgToken = core.getInput('org-membership-token', { required: true }); + const repoToken = core.getInput('github-app-token', { required: true }); + const org = core.getInput('org', { required: true }); + const prSource = core.getInput('pr-source', { required: true }); + const prNumberStr = core.getInput('pr-number'); + const commentAuthor = core.getInput('comment-author'); + const repository = process.env.GITHUB_REPOSITORY ?? ''; + + let username: string; + + if (prSource === 'event') { + username = commentAuthor; + } else { + const prNumber = parseInt(prNumberStr, 10); + if (!Number.isInteger(prNumber) || prNumber <= 0) { + core.setFailed(`Invalid pr-number: '${prNumberStr}' (expected positive integer)`); + return; + } + const slashIdx = repository.indexOf('/'); + if (slashIdx < 0) { + core.setFailed(`Invalid GITHUB_REPOSITORY: '${repository}' (expected 'owner/repo')`); + return; + } + const owner = repository.slice(0, slashIdx); + const repo = repository.slice(slashIdx + 1); + try { + username = await resolvePrAuthor(repoToken, owner, repo, prNumber); + } catch (err: unknown) { + core.setFailed( + `Failed to resolve PR author for #${prNumber}: ${err instanceof Error ? err.message : String(err)}`, + ); + return; + } + } + + try { + const isMember = await checkOrgMembership(orgToken, org, username); + core.setOutput('is_member', String(isMember)); + if (isMember) { + core.info(`✅ ${username} is a ${org} org member — proceeding with review`); + } else { + core.info(`⏭️ ${username} is not a ${org} org member — skipping review`); + } + } catch (err: unknown) { + const status = (err as { status?: number }).status; + if (status === 401) { + core.setFailed( + `❌ Org membership token is missing or invalid (HTTP 401).\n\n` + + `This token is fetched automatically from AWS Secrets Manager in docker/* repos.\n` + + `Ensure the workflow job has 'id-token: write' permission and OIDC is configured.`, + ); + } else { + core.setFailed( + `Failed to check org membership: ${err instanceof Error ? err.message : String(err)}`, + ); + } + } +} + +// Guard: only run as CLI when invoked directly as dist/check-org-membership.js, +// never when bundled into dist/mention-reply.js or dist/main.js as a library. +if (process.argv[1]?.endsWith('check-org-membership.js') && !process.env.VITEST) { + main().catch((err: unknown) => { + core.setFailed(`Unexpected error: ${err instanceof Error ? err.message : String(err)}`); + }); +} diff --git a/tsup.config.ts b/tsup.config.ts index 1cd7ec9..2d55bbb 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -3,15 +3,17 @@ import { resolve } from 'node:path'; import { defineConfig } from 'tsup'; // Explicit entry list: only modules that back an actual action.yml entrypoint. -// Library sub-modules (add-reaction, check-org-membership, get-pr-meta, -// post-comment) are imported by mention-reply but have no standalone action, -// so they don't get their own top-level dist bundle. +// Pure library sub-modules (add-reaction, get-pr-meta, post-comment) are imported +// by mention-reply but have no standalone action, so they don't get their own +// top-level dist bundle. check-org-membership is both a library and a standalone +// node24 action, so it IS in the entry map. const src = (name: string) => { const p = resolve(import.meta.dirname, 'src', name, 'index.ts'); if (!existsSync(p)) throw new Error(`tsup entry not found: ${p}`); return p; }; const entry = { + 'check-org-membership': src('check-org-membership'), credentials: src('credentials'), 'filter-diff': src('filter-diff'), main: src('main'), From 918adb5b60ad1e3ddff2639ca0452c2eb2132c25 Mon Sep 17 00:00:00 2001 From: Docker Agent Date: Fri, 8 May 2026 13:24:19 +0000 Subject: [PATCH 4/7] refactor: replace actions/github-script membership step with node24 action Wire up the new .github/actions/check-org-membership (node24) in review-pr.yml, replacing the inline actions/github-script block. The step now delegates to dist/check-org-membership.js via explicit inputs, matching the project convention of keeping business logic in tested TypeScript modules rather than inline YAML JS. Token split is enforced at the action boundary: org-membership-token -> checkMembershipForUser (read:org scope) github-app-token -> resolvePrAuthor / pulls.get (repo scope) SHA points to the commit that introduced the action file and bundle entry (e9a0c6af4c917f051184cfb96eb82586dcf87084); the release workflow will update all internal SHA pins together when the next version is cut. --- .github/workflows/review-pr.yml | 50 +++++---------------------------- 1 file changed, 7 insertions(+), 43 deletions(-) diff --git a/.github/workflows/review-pr.yml b/.github/workflows/review-pr.yml index c94bd9a..cf757bd 100644 --- a/.github/workflows/review-pr.yml +++ b/.github/workflows/review-pr.yml @@ -263,50 +263,14 @@ jobs: if: | steps.command.outputs.is_review != 'false' && steps.draft.outputs.skip != 'true' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - PR_NUMBER: ${{ steps.pr.outputs.number }} - PR_SOURCE: ${{ steps.pr.outputs.source }} - ORG: docker - COMMENT_AUTHOR: ${{ github.event.comment.user.login }} - REPO_TOKEN: ${{ env.GITHUB_APP_TOKEN }} + uses: docker/cagent-action/.github/actions/check-org-membership@e9a0c6af4c917f051184cfb96eb82586dcf87084 # in-branch with: - github-token: ${{ env.ORG_MEMBERSHIP_TOKEN }} - script: | - const org = process.env.ORG; - const source = process.env.PR_SOURCE; - - let username; - if (source === 'event') { - username = process.env.COMMENT_AUTHOR; - } else { - const repoOctokit = github.getOctokit(process.env.REPO_TOKEN); - const { data: pr } = await repoOctokit.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: parseInt(process.env.PR_NUMBER, 10) - }); - username = pr.user.login; - } - - try { - await github.rest.orgs.checkMembershipForUser({ org, username }); - core.setOutput('is_member', 'true'); - console.log(`✅ ${username} is a ${org} org member — proceeding with review`); - } catch (error) { - if (error.status === 404 || error.status === 302) { - core.setOutput('is_member', 'false'); - console.log(`⏭️ ${username} is not a ${org} org member — skipping review`); - } else if (error.status === 401) { - core.setFailed( - `❌ Org membership token is missing or invalid (HTTP 401).\n\n` + - `This token is fetched automatically from AWS Secrets Manager in docker/* repos.\n` + - `Ensure the workflow job has 'id-token: write' permission and OIDC is configured.` - ); - } else { - core.setFailed(`Failed to check org membership: ${error.message}`); - } - } + org-membership-token: ${{ env.ORG_MEMBERSHIP_TOKEN }} + github-app-token: ${{ env.GITHUB_APP_TOKEN }} + org: docker + pr-source: ${{ steps.pr.outputs.source }} + pr-number: ${{ steps.pr.outputs.number }} + comment-author: ${{ github.event.comment.user.login }} - name: Create check run if: | From 5e543cd8352630c6a9c4a5508a3deac9774fc1d2 Mon Sep 17 00:00:00 2001 From: Docker Agent Date: Fri, 8 May 2026 13:32:08 +0000 Subject: [PATCH 5/7] refactor: inline check-org-membership bundle call in review-pr.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the .github/actions/check-org-membership/ wrapper — no separate action file needed. The bundle is now invoked directly as a shell run step, consistent with how review-pr/action.yml calls other dist/ tools. Changes: - setup-credentials/action.yml: export CAGENT_ACTION_ROOT (the repo root of the downloaded cagent-action copy) to the job environment in the same 'Verify credentials' step. Subsequent steps in the calling workflow can then reach dist/ bundles via $CAGENT_ACTION_ROOT/dist/.js. - review-pr.yml 'Check if org member': replace the uses: action invocation with a plain 'run: node "$CAGENT_ACTION_ROOT/dist/check-org-membership.js"' step; inputs passed via env vars (ORG, PR_SOURCE, PR_NUMBER, COMMENT_AUTHOR); ORG_MEMBERSHIP_TOKEN and GITHUB_APP_TOKEN are already in the job env from setup-credentials. - src/check-org-membership/index.ts: CLI reads from process.env (not core.getInput) since it is invoked via a shell run step, not a node action. --- .../actions/check-org-membership/action.yml | 32 ------------- .github/actions/setup-credentials/action.yml | 2 + .github/workflows/review-pr.yml | 15 +++---- src/check-org-membership/index.ts | 45 +++++++++++-------- 4 files changed, 36 insertions(+), 58 deletions(-) delete mode 100644 .github/actions/check-org-membership/action.yml diff --git a/.github/actions/check-org-membership/action.yml b/.github/actions/check-org-membership/action.yml deleted file mode 100644 index d14c074..0000000 --- a/.github/actions/check-org-membership/action.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: 'Check Org Membership' -description: > - Verify whether a GitHub user belongs to a GitHub org. - Resolves the PR author via the GitHub API (using a repo-scoped token) when - pr-source is not "event"; uses comment-author directly for event-sourced triggers. -inputs: - org-membership-token: - description: 'PAT with read:org scope for org membership checks' - required: true - github-app-token: - description: 'PAT with repo scope; used to call pulls.get when pr-source is not "event"' - required: true - org: - description: 'GitHub org name to check membership against (e.g. "docker")' - required: true - pr-source: - description: 'Trigger source: "event" (uses comment-author) or any other value (uses pr-number + pulls.get)' - required: true - pr-number: - description: 'PR number as a string; required when pr-source is not "event"' - required: false - default: '' - comment-author: - description: 'GitHub login of the user who triggered the event; required when pr-source is "event"' - required: false - default: '' -outputs: - is_member: - description: '"true" if the resolved user is a member of the org, "false" otherwise' -runs: - using: 'node24' - main: '../../../dist/check-org-membership.js' diff --git a/.github/actions/setup-credentials/action.yml b/.github/actions/setup-credentials/action.yml index 315feab..2016c05 100644 --- a/.github/actions/setup-credentials/action.yml +++ b/.github/actions/setup-credentials/action.yml @@ -19,3 +19,5 @@ runs: echo "::error::GITHUB_APP_TOKEN was not set — setup-credentials failed silently." exit 1 fi + # Export the repo root so callers can reach dist/ bundles via $CAGENT_ACTION_ROOT + echo "CAGENT_ACTION_ROOT=$(cd "$GITHUB_ACTION_PATH/../../.." && pwd)" >> "$GITHUB_ENV" diff --git a/.github/workflows/review-pr.yml b/.github/workflows/review-pr.yml index cf757bd..ef2f1d1 100644 --- a/.github/workflows/review-pr.yml +++ b/.github/workflows/review-pr.yml @@ -263,14 +263,13 @@ jobs: if: | steps.command.outputs.is_review != 'false' && steps.draft.outputs.skip != 'true' - uses: docker/cagent-action/.github/actions/check-org-membership@e9a0c6af4c917f051184cfb96eb82586dcf87084 # in-branch - with: - org-membership-token: ${{ env.ORG_MEMBERSHIP_TOKEN }} - github-app-token: ${{ env.GITHUB_APP_TOKEN }} - org: docker - pr-source: ${{ steps.pr.outputs.source }} - pr-number: ${{ steps.pr.outputs.number }} - comment-author: ${{ github.event.comment.user.login }} + shell: bash + env: + PR_NUMBER: ${{ steps.pr.outputs.number }} + PR_SOURCE: ${{ steps.pr.outputs.source }} + ORG: docker + COMMENT_AUTHOR: ${{ github.event.comment.user.login }} + run: node "$CAGENT_ACTION_ROOT/dist/check-org-membership.js" - name: Create check run if: | diff --git a/src/check-org-membership/index.ts b/src/check-org-membership/index.ts index 6efff33..7ab86bd 100644 --- a/src/check-org-membership/index.ts +++ b/src/check-org-membership/index.ts @@ -5,18 +5,18 @@ * checkOrgMembership(orgToken, org, username) → boolean * resolvePrAuthor(repoToken, owner, repo, prNumber) → string * - * CLI (node24 action, invoked via .github/actions/check-org-membership/action.yml): - * Inputs (via @actions/core.getInput): - * org-membership-token PAT with read:org scope (for checkMembershipForUser) - * github-app-token PAT with repo scope (for pulls.get when pr-source != 'event') - * org GitHub org name (e.g. "docker") - * pr-source "event" | "trigger" | "input" - * pr-number PR number as string (required when pr-source != 'event') - * comment-author User login (required when pr-source == 'event') - * Standard env vars: - * GITHUB_REPOSITORY "owner/repo" (standard GitHub Actions env var) - * Outputs (via @actions/core.setOutput): - * is_member "true" | "false" + * CLI (invoked as a shell run step via dist/check-org-membership.js): + * All inputs are read from environment variables: + * ORG_MEMBERSHIP_TOKEN PAT with read:org scope (set by setup-credentials) + * GITHUB_APP_TOKEN PAT with repo scope (set by setup-credentials) + * GITHUB_REPOSITORY "owner/repo" (standard GitHub Actions env var) + * ORG GitHub org name to check (e.g. "docker") + * PR_SOURCE "event" | "trigger" | "input" + * PR_NUMBER PR number as string (required when PR_SOURCE != 'event') + * COMMENT_AUTHOR User login (required when PR_SOURCE == 'event') + * + * Outputs are written via @actions/core.setOutput (writes to $GITHUB_OUTPUT): + * is_member "true" | "false" * * Guard: the CLI entry point only executes when process.argv[1] ends with * "check-org-membership.js" and VITEST is not set. This prevents the CLI from @@ -85,14 +85,23 @@ export async function resolvePrAuthor( // --------------------------------------------------------------------------- async function main(): Promise { - const orgToken = core.getInput('org-membership-token', { required: true }); - const repoToken = core.getInput('github-app-token', { required: true }); - const org = core.getInput('org', { required: true }); - const prSource = core.getInput('pr-source', { required: true }); - const prNumberStr = core.getInput('pr-number'); - const commentAuthor = core.getInput('comment-author'); + const orgToken = process.env.ORG_MEMBERSHIP_TOKEN ?? ''; + const repoToken = process.env.GITHUB_APP_TOKEN ?? ''; + const org = process.env.ORG ?? ''; + const prSource = process.env.PR_SOURCE ?? ''; + const prNumberStr = process.env.PR_NUMBER ?? ''; + const commentAuthor = process.env.COMMENT_AUTHOR ?? ''; const repository = process.env.GITHUB_REPOSITORY ?? ''; + if (!orgToken) { + core.setFailed('ORG_MEMBERSHIP_TOKEN is not set — ensure setup-credentials ran successfully.'); + return; + } + if (!repoToken) { + core.setFailed('GITHUB_APP_TOKEN is not set — ensure setup-credentials ran successfully.'); + return; + } + let username: string; if (prSource === 'event') { From be83ae370a2c5a5d39c1d110acd2feae8615de75 Mon Sep 17 00:00:00 2001 From: Docker Agent Date: Fri, 8 May 2026 13:44:07 +0000 Subject: [PATCH 6/7] refactor: move setup-credentials to repo root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promotes setup-credentials from .github/actions/setup-credentials/ to setup-credentials/ so external consumers can reference it as: uses: docker/cagent-action/setup-credentials@VERSION matching the pattern already established by review-pr/ and consistent with how other repos already use it. The .github/actions/ location prevented clean external references. mention-reply stays under .github/actions/ — it is only called from within review-pr.yml and is not intended for external use. With setup-credentials at the root, $GITHUB_ACTION_PATH inside the action is one level deep, so the credentials bundle path simplifies from $GITHUB_ACTION_PATH/../../../dist/credentials.js to $GITHUB_ACTION_PATH/../dist/credentials.js and the CAGENT_ACTION_ROOT export from: cd "$GITHUB_ACTION_PATH/../../.." && pwd to: cd "$GITHUB_ACTION_PATH/.." && pwd All uses: references updated across all workflow files. --- .github/workflows/pr-describe.yml | 2 +- .github/workflows/release.yml | 6 +++--- .github/workflows/reply-to-feedback.yml | 2 +- .github/workflows/review-pr.yml | 10 +++++----- .github/workflows/security-scan.yml | 2 +- .github/workflows/self-review-pr.yml | 10 +++++----- .github/workflows/test.yml | 2 +- .github/workflows/update-consumers.yml | 2 +- .../workflows/update-docker-agent-version.yml | 2 +- AGENTS.md | 19 ++++++++++--------- .../action.yml | 4 ++-- 11 files changed, 31 insertions(+), 30 deletions(-) rename {.github/actions/setup-credentials => setup-credentials}/action.yml (80%) diff --git a/.github/workflows/pr-describe.yml b/.github/workflows/pr-describe.yml index 424a9eb..64cbfcc 100644 --- a/.github/workflows/pr-describe.yml +++ b/.github/workflows/pr-describe.yml @@ -21,7 +21,7 @@ jobs: id-token: write steps: - name: Setup credentials - uses: docker/cagent-action/.github/actions/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 + uses: docker/cagent-action/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 - name: Create check run id: create-check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1562d1a..1040372 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,7 @@ jobs: run: pnpm install --frozen-lockfile && pnpm build - name: Setup credentials - uses: ./.github/actions/setup-credentials + uses: ./setup-credentials - name: Calculate new version id: version @@ -325,7 +325,7 @@ jobs: persist-credentials: false - name: Setup credentials - uses: ./.github/actions/setup-credentials + uses: ./setup-credentials - name: Checkout cagent-action uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -447,7 +447,7 @@ jobs: persist-credentials: false - name: Setup credentials - uses: ./.github/actions/setup-credentials + uses: ./setup-credentials - name: Fetch release notes from GitHub id: release-notes diff --git a/.github/workflows/reply-to-feedback.yml b/.github/workflows/reply-to-feedback.yml index 024badf..dd2d7dc 100644 --- a/.github/workflows/reply-to-feedback.yml +++ b/.github/workflows/reply-to-feedback.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Setup credentials - uses: docker/cagent-action/.github/actions/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 + uses: docker/cagent-action/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 # ---------------------------------------------------------------- # Download artifact from the triggering workflow run diff --git a/.github/workflows/review-pr.yml b/.github/workflows/review-pr.yml index ef2f1d1..1bb6fc6 100644 --- a/.github/workflows/review-pr.yml +++ b/.github/workflows/review-pr.yml @@ -84,7 +84,7 @@ jobs: comment-author: ${{ steps.read.outputs.comment-author }} steps: - name: Setup credentials - uses: docker/cagent-action/.github/actions/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 + uses: docker/cagent-action/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 - name: Verify token for cross-run artifact download shell: bash @@ -256,7 +256,7 @@ jobs: if: | steps.command.outputs.is_review != 'false' && steps.draft.outputs.skip != 'true' - uses: docker/cagent-action/.github/actions/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 + uses: docker/cagent-action/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 - name: Check if org member id: membership @@ -378,7 +378,7 @@ jobs: - name: Setup credentials if: inputs.trigger-run-id != '' - uses: docker/cagent-action/.github/actions/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 + uses: docker/cagent-action/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 - name: Verify token for cross-run artifact download if: inputs.trigger-run-id != '' @@ -497,7 +497,7 @@ jobs: - name: Setup credentials if: steps.check.outputs.is_agent == 'true' - uses: docker/cagent-action/.github/actions/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 + uses: docker/cagent-action/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 - name: Check authorization if: steps.check.outputs.is_agent == 'true' @@ -723,7 +723,7 @@ jobs: steps: - name: Setup credentials - uses: docker/cagent-action/.github/actions/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 + uses: docker/cagent-action/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 - name: Run mention-reply handler id: mention-context diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index ccba81c..e08fac1 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -30,7 +30,7 @@ jobs: fetch-depth: 0 # Need full history to get commits from past week - name: Setup credentials - uses: docker/cagent-action/.github/actions/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 + uses: docker/cagent-action/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 - name: Get commits from past week diff --git a/.github/workflows/self-review-pr.yml b/.github/workflows/self-review-pr.yml index a5e4682..e64eeec 100644 --- a/.github/workflows/self-review-pr.yml +++ b/.github/workflows/self-review-pr.yml @@ -48,7 +48,7 @@ jobs: run: pnpm install --frozen-lockfile && pnpm build - name: Setup credentials - uses: ./.github/actions/setup-credentials + uses: ./setup-credentials - name: Verify token for cross-run artifact download shell: bash @@ -206,7 +206,7 @@ jobs: if: | steps.command.outputs.is_review != 'false' && steps.draft.outputs.skip != 'true' - uses: ./.github/actions/setup-credentials + uses: ./setup-credentials - name: Check if org member id: membership @@ -376,7 +376,7 @@ jobs: - name: Setup credentials if: github.event_name == 'workflow_run' - uses: ./.github/actions/setup-credentials + uses: ./setup-credentials - name: Verify token for cross-run artifact download if: github.event_name == 'workflow_run' @@ -495,7 +495,7 @@ jobs: - name: Setup credentials if: steps.check.outputs.is_agent == 'true' - uses: ./.github/actions/setup-credentials + uses: ./setup-credentials - name: Check authorization if: steps.check.outputs.is_agent == 'true' @@ -732,7 +732,7 @@ jobs: run: pnpm install --frozen-lockfile && pnpm build - name: Setup credentials - uses: ./.github/actions/setup-credentials + uses: ./setup-credentials - name: Run mention-reply handler id: mention-context diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 61bf105..b00967c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,7 @@ jobs: run: pnpm install --frozen-lockfile && pnpm build - name: Setup credentials - uses: ./.github/actions/setup-credentials + uses: ./setup-credentials continue-on-error: true - name: Run integration tests diff --git a/.github/workflows/update-consumers.yml b/.github/workflows/update-consumers.yml index 4632695..2d849fc 100644 --- a/.github/workflows/update-consumers.yml +++ b/.github/workflows/update-consumers.yml @@ -56,7 +56,7 @@ jobs: persist-credentials: false - name: Setup credentials - uses: ./.github/actions/setup-credentials + uses: ./setup-credentials - name: Checkout source for build uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/update-docker-agent-version.yml b/.github/workflows/update-docker-agent-version.yml index d9ff878..8155c6a 100644 --- a/.github/workflows/update-docker-agent-version.yml +++ b/.github/workflows/update-docker-agent-version.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup credentials - uses: docker/cagent-action/.github/actions/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 + uses: docker/cagent-action/setup-credentials@c22076b8856ee12d9b4c4685bb49cf26eb974079 # v1.5.0 - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/AGENTS.md b/AGENTS.md index 20b11f6..64df508 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,8 +30,8 @@ Anything else here (workflows under `.github/workflows/`, scripts, tests) exists │ ├── add-reaction/ # Adds emoji reactions to issue/PR comments. │ │ ├── index.ts # Entry → bundled to dist/add-reaction.js │ │ └── __tests__/ -│ ├── check-org-membership/ # Verifies a user belongs to a GitHub org (used by org-membership auth tier). -│ │ ├── index.ts # Entry → bundled to dist/check-org-membership.js +│ ├── check-org-membership/ # Verifies a user belongs to a GitHub org; also resolves PR author via pulls.get. +│ │ ├── index.ts # Entry → bundled to dist/check-org-membership.js (standalone CLI + library). │ │ └── __tests__/ │ ├── credentials/ # Fetches AWS secrets via OIDC, exports PAT and AI keys. │ │ ├── index.ts # Entry → bundled to dist/credentials.js @@ -83,15 +83,16 @@ Anything else here (workflows under `.github/workflows/`, scripts, tests) exists │ ├── refs/ # Reference docs passed to agents (posting format, code-review style). │ └── evals/ # cagent eval JSON files (success-*, security-*, marlin-*, etc.). │ +├── setup-credentials/ # Composite action: fetches AWS creds via OIDC, exports GITHUB_APP_TOKEN + +│ └── action.yml # ORG_MEMBERSHIP_TOKEN. At root so consumers can use +│ # docker/cagent-action/setup-credentials@VERSION directly. +│ # Also exports CAGENT_ACTION_ROOT (repo root of the downloaded action copy) +│ # for subsequent run: steps that need to invoke dist/ bundles. +│ ├── .github/ │ ├── actions/ -│ │ ├── mention-reply/ # Internal JS action (using: node20). main = dist/mention-reply.js. -│ │ │ └── action.yml # Note: unlike setup-credentials (composite + shell run:), this uses -│ │ │ # the native node20 runner form — a difference contributors will notice. -│ │ └── setup-credentials/ # Internal composite action: runs dist/credentials.js via a bash run: step. -│ │ │ # Uses OIDC → AWS to read docker-agent-action/github-app from Secrets Manager; -│ │ │ # exports GITHUB_APP_TOKEN (PAT) and ORG_MEMBERSHIP_TOKEN. No longer mints a GitHub App installation token. -│ │ └── action.yml +│ │ └── mention-reply/ # Internal-only JS action (node24). main = dist/mention-reply.js. +│ │ └── action.yml # Only used by review-pr.yml; not intended for external consumers. │ ├── workflows/ # CI + self-test + release workflows (see "Workflows" below). │ └── CODEOWNERS │ diff --git a/.github/actions/setup-credentials/action.yml b/setup-credentials/action.yml similarity index 80% rename from .github/actions/setup-credentials/action.yml rename to setup-credentials/action.yml index 2016c05..21c7f7a 100644 --- a/.github/actions/setup-credentials/action.yml +++ b/setup-credentials/action.yml @@ -10,7 +10,7 @@ runs: - name: Fetch credentials shell: bash - run: node "$GITHUB_ACTION_PATH/../../../dist/credentials.js" + run: node "$GITHUB_ACTION_PATH/../dist/credentials.js" - name: Verify credentials were obtained shell: bash @@ -20,4 +20,4 @@ runs: exit 1 fi # Export the repo root so callers can reach dist/ bundles via $CAGENT_ACTION_ROOT - echo "CAGENT_ACTION_ROOT=$(cd "$GITHUB_ACTION_PATH/../../.." && pwd)" >> "$GITHUB_ENV" + echo "CAGENT_ACTION_ROOT=$(cd "$GITHUB_ACTION_PATH/.." && pwd)" >> "$GITHUB_ENV" From d07ed34655bb224edc4772d708ffa494acff9ac0 Mon Sep 17 00:00:00 2001 From: Docker Agent Date: Fri, 8 May 2026 13:57:26 +0000 Subject: [PATCH 7/7] fix: correct dist/ path in review-pr/action.yml bundle invocations $ACTION_PATH is set from ${{ github.action_path }} which points to the review-pr/ subdirectory, not the repo root. So $ACTION_PATH/dist/ resolved to review-pr/dist/ (non-existent) rather than the root dist/. Fix both bundle calls to use $ACTION_PATH/../dist/, matching the same one-level-up pattern that setup-credentials/action.yml already uses ($GITHUB_ACTION_PATH/../dist/credentials.js). --- review-pr/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/review-pr/action.yml b/review-pr/action.yml index 3cdec16..fff0f35 100644 --- a/review-pr/action.yml +++ b/review-pr/action.yml @@ -262,7 +262,7 @@ runs: ACTION_PATH: ${{ github.action_path }} EXCLUDE_PATHS: ${{ inputs.exclude-paths }} run: | - node "$ACTION_PATH/dist/filter-diff.js" pr.diff "$EXCLUDE_PATHS" + node "$ACTION_PATH/../dist/filter-diff.js" pr.diff "$EXCLUDE_PATHS" - name: Split diff into chunks if: hashFiles('pr.diff') != '' @@ -343,7 +343,7 @@ runs: EXCLUDE_PATHS: ${{ inputs.exclude-paths }} run: | set -euo pipefail - node "$ACTION_PATH/dist/score-risk.js" pr.diff "$EXCLUDE_PATHS" + node "$ACTION_PATH/../dist/score-risk.js" pr.diff "$EXCLUDE_PATHS" echo "✅ File risk scores: $(jq -c . /tmp/file_risk_scores.json)" - name: Generate file history