diff --git a/.github/workflows/smoke-workflow-call-with-inputs.lock.yml b/.github/workflows/smoke-workflow-call-with-inputs.lock.yml
index 458b0cbda62..bcfce829c69 100644
--- a/.github/workflows/smoke-workflow-call-with-inputs.lock.yml
+++ b/.github/workflows/smoke-workflow-call-with-inputs.lock.yml
@@ -109,13 +109,12 @@ jobs:
trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }}
- name: Resolve host repo for activation checkout
id: resolve-host-repo
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
- with:
- script: |
- const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io, getOctokit);
- const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs');
- await main();
+ run: |
+ repo="${{ job.workflow_repository }}"
+ ref="${{ job.workflow_ref }}"
+ echo "target_repo=${repo}" >> "$GITHUB_OUTPUT"
+ echo "target_repo_name=${repo##*/}" >> "$GITHUB_OUTPUT"
+ echo "target_ref=${ref##*@}" >> "$GITHUB_OUTPUT"
- name: Compute artifact prefix
id: artifact-prefix
env:
diff --git a/.github/workflows/smoke-workflow-call.lock.yml b/.github/workflows/smoke-workflow-call.lock.yml
index dd154dd81f2..d5346a87eee 100644
--- a/.github/workflows/smoke-workflow-call.lock.yml
+++ b/.github/workflows/smoke-workflow-call.lock.yml
@@ -112,13 +112,12 @@ jobs:
trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }}
- name: Resolve host repo for activation checkout
id: resolve-host-repo
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
- with:
- script: |
- const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io, getOctokit);
- const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs');
- await main();
+ run: |
+ repo="${{ job.workflow_repository }}"
+ ref="${{ job.workflow_ref }}"
+ echo "target_repo=${repo}" >> "$GITHUB_OUTPUT"
+ echo "target_repo_name=${repo##*/}" >> "$GITHUB_OUTPUT"
+ echo "target_ref=${ref##*@}" >> "$GITHUB_OUTPUT"
- name: Compute artifact prefix
id: artifact-prefix
env:
diff --git a/actions/setup/js/resolve_host_repo.cjs b/actions/setup/js/resolve_host_repo.cjs
deleted file mode 100644
index 1fca3b11dfe..00000000000
--- a/actions/setup/js/resolve_host_repo.cjs
+++ /dev/null
@@ -1,208 +0,0 @@
-// @ts-check
-///
-
-/**
- * Resolves the target repository and ref for the activation job checkout.
- *
- * Uses GITHUB_WORKFLOW_REF to determine the platform (host) repository and branch/ref
- * regardless of the triggering event. This fixes cross-repo activation for event-driven
- * relays (e.g. on: issue_comment, on: push) where github.event_name is NOT 'workflow_call',
- * so the expression introduced in #20301 incorrectly fell back to github.repository
- * (the caller's repo) instead of the platform repo.
- *
- * GITHUB_WORKFLOW_REF reflects the currently executing workflow file for most triggers, but
- * in cross-org workflow_call scenarios it resolves to the TOP-LEVEL CALLER's workflow ref,
- * not the reusable (callee) workflow being executed. Its format is:
- * owner/repo/.github/workflows/file.yml@refs/heads/main
- *
- * When the platform workflow runs cross-repo (called via uses: from the same org),
- * GITHUB_WORKFLOW_REF starts with the platform repo slug, while GITHUB_REPOSITORY is the
- * caller repo. Comparing the two lets us detect cross-repo invocations without relying on
- * event_name.
- *
- * For cross-org workflow_call, GITHUB_WORKFLOW_REF and GITHUB_REPOSITORY both resolve to
- * the caller's repo. In that case we fall back to the referenced_workflows API lookup to
- * find the actual callee (platform) repo and ref.
- *
- * In a caller-hosted relay pinned to a feature branch (e.g. uses: platform/.github/workflows/
- * gateway.lock.yml@feature-branch), the @feature-branch portion is encoded in
- * GITHUB_WORKFLOW_REF. Emitting it as target_ref allows the activation checkout to use
- * the correct branch rather than the platform repo's default branch.
- *
- * SEC-005: The targetRepo and targetRef values are resolved solely from trusted system
- * environment variables (GITHUB_WORKFLOW_REF, GITHUB_REPOSITORY, GITHUB_REF) and the
- * GitHub Actions API (referenced_workflows), set/provided by the GitHub Actions runtime.
- * They are not derived from user-supplied input, so no allowlist check is required here.
- *
- * @safe-outputs-exempt SEC-005: values sourced from trusted GitHub Actions runtime env vars and referenced_workflows API only
- */
-
-// Matches the "owner/repo" prefix from a GitHub workflow path of the form "owner/repo/...".
-const REPO_PREFIX_RE = /^([^/]+\/[^/]+)\//;
-
-/**
- * Attempts to resolve the callee repository and ref from the referenced_workflows API.
- *
- * This is used as a fallback when GITHUB_WORKFLOW_REF points to the same repo as
- * GITHUB_REPOSITORY (cross-org workflow_call scenario), because in that case
- * GITHUB_WORKFLOW_REF reflects the caller's workflow ref, not the callee's.
- *
- * @param {string} currentRepo - The value of GITHUB_REPOSITORY (owner/repo)
- * @returns {Promise<{repo: string, ref: string} | null>} Resolved callee repo and ref, or null
- */
-async function resolveFromReferencedWorkflows(currentRepo) {
- const rawRunId = process.env.GITHUB_RUN_ID;
- const runId = rawRunId ? parseInt(rawRunId, 10) : typeof context.runId === "number" ? context.runId : NaN;
- if (!Number.isFinite(runId)) {
- core.info("Run ID is unavailable or invalid, cannot perform referenced_workflows lookup");
- return null;
- }
-
- const [runOwner, runRepo] = currentRepo.split("/");
- try {
- core.info(`Checking for cross-org callee via referenced_workflows API (run ${runId}, repo ${currentRepo})`);
- const runResponse = await github.rest.actions.getWorkflowRun({
- owner: runOwner,
- repo: runRepo,
- run_id: runId,
- });
-
- const referencedWorkflows = runResponse.data.referenced_workflows || [];
- core.info(`Found ${referencedWorkflows.length} referenced workflow(s) in run`);
- for (const wf of referencedWorkflows) {
- core.info(` referenced workflow: path=${wf.path} sha=${wf.sha || "(none)"} ref=${wf.ref || "(none)"}`);
- }
-
- // Collect all referenced workflows from a different repo than the caller.
- // In cross-org workflow_call, the callee (platform) repo is different from currentRepo.
- // If multiple cross-repo candidates are found we cannot safely pick one, so we bail out.
- const crossRepoCandidates = [];
- for (const wf of referencedWorkflows) {
- const pathRepoMatch = wf.path.match(REPO_PREFIX_RE);
- const entryRepo = pathRepoMatch ? pathRepoMatch[1] : "";
- if (entryRepo && entryRepo !== currentRepo) {
- crossRepoCandidates.push({ wf, repo: entryRepo });
- }
- }
- core.info(`Found ${crossRepoCandidates.length} cross-repo candidate(s) (excluding current repo ${currentRepo})`);
-
- if (crossRepoCandidates.length === 0) {
- core.info("No cross-org callee found in referenced_workflows, using current repo");
- return null;
- }
-
- if (crossRepoCandidates.length > 1) {
- core.info(`Referenced workflows lookup is ambiguous; found ${crossRepoCandidates.length} cross-repo candidates, not selecting one`);
- for (const candidate of crossRepoCandidates) {
- core.info(` Candidate referenced workflow path: ${candidate.wf.path}`);
- }
- return null;
- }
-
- const matchingEntry = crossRepoCandidates[0].wf;
- const calleeRepo = crossRepoCandidates[0].repo;
-
- // Prefer sha (immutable) over ref (branch/tag can drift) over path-parsed ref.
- const pathRefMatch = matchingEntry.path.match(/@(.+)$/);
- let calleeRefSource;
- if (matchingEntry.sha) {
- calleeRefSource = "sha";
- } else if (matchingEntry.ref) {
- calleeRefSource = "ref";
- } else if (pathRefMatch) {
- calleeRefSource = "path";
- } else {
- calleeRefSource = "none";
- }
- const calleeRef = matchingEntry.sha || matchingEntry.ref || (pathRefMatch ? pathRefMatch[1] : "");
- core.info(`Resolved callee repo from referenced_workflows: ${calleeRepo} @ ${calleeRef || "(default branch)"} (source: ${calleeRefSource})`);
- core.info(` Referenced workflow path: ${matchingEntry.path}`);
- return { repo: calleeRepo, ref: calleeRef };
- } catch (error) {
- const msg = error instanceof Error ? error.message : String(error);
- core.info(`Could not fetch referenced_workflows from API: ${msg}, using current repo`);
- return null;
- }
-}
-
-/**
- * @returns {Promise}
- */
-async function main() {
- const workflowRef = process.env.GITHUB_WORKFLOW_REF || "";
- const currentRepo = process.env.GITHUB_REPOSITORY || "";
-
- core.info(`GITHUB_WORKFLOW_REF: ${workflowRef || "(not set)"}`);
- core.info(`GITHUB_REPOSITORY: ${currentRepo || "(not set)"}`);
- core.info(`GITHUB_RUN_ID: ${process.env.GITHUB_RUN_ID || "(not set)"}`);
-
- // GITHUB_WORKFLOW_REF format: owner/repo/.github/workflows/file.yml@ref
- // The regex captures everything before the third slash segment (i.e., the owner/repo prefix).
- const repoMatch = workflowRef.match(REPO_PREFIX_RE);
- const workflowRepo = repoMatch ? repoMatch[1] : "";
- core.info(`Parsed workflow repo from GITHUB_WORKFLOW_REF: ${workflowRepo || "(could not parse)"}`);
-
- // Fall back to currentRepo when GITHUB_WORKFLOW_REF cannot be parsed
- let targetRepo = workflowRepo || currentRepo;
-
- // Extract the ref portion after '@' from GITHUB_WORKFLOW_REF.
- // GITHUB_WORKFLOW_REF format: owner/repo/.github/workflows/file.yml@ref
- // The ref may be a full ref like "refs/heads/feature-branch", a short name like "main",
- // a tag like "refs/tags/v1.0.0", or a commit SHA like "abc123def".
- //
- // When GITHUB_WORKFLOW_REF has no '@' segment (e.g., env var not set or malformed),
- // fall back to an empty string so that actions/checkout uses the repository's default
- // branch. We intentionally do NOT fall back to GITHUB_REF here because in cross-repo
- // scenarios GITHUB_REF is the *caller* repo's ref, not the callee's, and using it
- // would check out the wrong branch.
- const refMatch = workflowRef.match(/@(.+)$/);
- let targetRef = refMatch ? refMatch[1] : "";
- core.info(`Parsed workflow ref from GITHUB_WORKFLOW_REF: ${targetRef || "(none — will use default branch)"}`);
-
- // Cross-org workflow_call detection: when GITHUB_WORKFLOW_REF points to the same repo as
- // GITHUB_REPOSITORY, it means GITHUB_WORKFLOW_REF is resolving to the caller's workflow
- // (not the callee's). This happens in cross-org workflow_call invocations where GitHub
- // Actions sets GITHUB_WORKFLOW_REF to the top-level caller's workflow ref rather than the
- // reusable workflow being executed. In that case, fall back to the referenced_workflows API
- // to find the actual callee (platform) repo and ref.
- //
- // Note: GITHUB_EVENT_NAME inside a reusable workflow reflects the ORIGINAL trigger event
- // (e.g., "push", "issues"), NOT "workflow_call", so we cannot use event_name to detect
- // this scenario.
- if (workflowRepo && workflowRepo === currentRepo) {
- core.info(`Cross-org workflow_call detected (workflowRepo === currentRepo = ${currentRepo}): falling back to referenced_workflows API`);
- const resolved = await resolveFromReferencedWorkflows(currentRepo);
- if (resolved) {
- targetRepo = resolved.repo;
- targetRef = resolved.ref || targetRef;
- } else {
- core.info("referenced_workflows lookup returned no result; keeping current repo as target");
- }
- } else if (!workflowRepo) {
- core.info("Could not parse workflowRepo from GITHUB_WORKFLOW_REF; falling back to GITHUB_REPOSITORY");
- } else {
- core.info(`Same-org cross-repo invocation: workflowRepo=${workflowRepo}, currentRepo=${currentRepo}`);
- }
-
- core.info(`Resolved host repo for activation checkout: ${targetRepo}`);
- core.info(`Resolved host ref for activation checkout: ${targetRef || "(default branch)"}`);
-
- if (targetRepo !== currentRepo && targetRepo !== "") {
- core.info(`Cross-repo invocation detected: platform repo is "${targetRepo}", caller is "${currentRepo}"`);
- await core.summary.addRaw(`**Activation Checkout**: Checking out platform repo \`${targetRepo}\` @ \`${targetRef}\` (caller: \`${currentRepo}\`)`).write();
- } else {
- core.info(`Same-repo invocation: checking out ${targetRepo} @ ${targetRef || "(default branch)"}`);
- }
-
- // Compute the repository name (without owner prefix) for use cases that require
- // only the repo name, such as actions/create-github-app-token which expects
- // `repositories` to contain repo names only when `owner` is also provided.
- const targetRepoName = targetRepo.split("/").at(-1);
- core.info(`target_repo=${targetRepo} target_repo_name=${targetRepoName} target_ref=${targetRef || "(default branch)"}`);
-
- core.setOutput("target_repo", targetRepo);
- core.setOutput("target_repo_name", targetRepoName);
- core.setOutput("target_ref", targetRef);
-}
-
-module.exports = { main };
diff --git a/actions/setup/js/resolve_host_repo.test.cjs b/actions/setup/js/resolve_host_repo.test.cjs
deleted file mode 100644
index cdf3937d31a..00000000000
--- a/actions/setup/js/resolve_host_repo.test.cjs
+++ /dev/null
@@ -1,459 +0,0 @@
-// @ts-check
-import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
-
-// Mock the global objects that GitHub Actions provides
-const mockCore = {
- info: vi.fn(),
- warning: vi.fn(),
- error: vi.fn(),
- setFailed: vi.fn(),
- setOutput: vi.fn(),
- summary: {
- addRaw: vi.fn().mockReturnThis(),
- write: vi.fn().mockResolvedValue(undefined),
- },
-};
-
-const mockGetWorkflowRun = vi.fn();
-const mockGithub = {
- rest: {
- actions: {
- getWorkflowRun: mockGetWorkflowRun,
- },
- },
-};
-
-const mockContext = {
- runId: 99999,
-};
-
-// Set up global mocks before importing the module
-global.core = mockCore;
-global.github = mockGithub;
-global.context = mockContext;
-
-/**
- * Sets up a one-time mock response for getWorkflowRun with no referenced workflows.
- * Used for same-repo and same-org cross-repo tests where the API should not change the result.
- */
-function mockNoReferencedWorkflowsOnce() {
- mockGetWorkflowRun.mockResolvedValueOnce({ data: { referenced_workflows: [] } });
-}
-
-describe("resolve_host_repo.cjs", () => {
- let main;
-
- beforeEach(async () => {
- vi.clearAllMocks();
- // Defensive reset of mock implementation as a safety measure.
- // All tests use *Once variants, but mockReset() ensures no state leaks
- // if a test adds a persistent mock or if a future test omits the Once variant.
- mockGetWorkflowRun.mockReset();
- mockCore.summary.addRaw.mockReturnThis();
- mockCore.summary.write.mockResolvedValue(undefined);
-
- const module = await import("./resolve_host_repo.cjs");
- main = module.main;
- });
-
- afterEach(() => {
- delete process.env.GITHUB_WORKFLOW_REF;
- delete process.env.GITHUB_REPOSITORY;
- delete process.env.GITHUB_REF;
- delete process.env.GITHUB_RUN_ID;
- // Reset context.runId to the default value to prevent test state leakage
- mockContext.runId = 99999;
- });
-
- it("should output the platform repo when invoked cross-repo", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "my-org/app-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "my-org/platform-repo");
- });
-
- it("should log a cross-repo detection message and write step summary", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "my-org/app-repo";
-
- await main();
-
- expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Cross-repo invocation detected"));
- expect(mockCore.summary.addRaw).toHaveBeenCalled();
- expect(mockCore.summary.write).toHaveBeenCalled();
- });
-
- it("should output the current repo when same-repo invocation", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "my-org/platform-repo";
- mockNoReferencedWorkflowsOnce();
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "my-org/platform-repo");
- expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Same-repo invocation"));
- });
-
- it("should not write step summary for same-repo invocations", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "my-org/platform-repo";
- mockNoReferencedWorkflowsOnce();
-
- await main();
-
- expect(mockCore.summary.write).not.toHaveBeenCalled();
- });
-
- it("should fall back to GITHUB_REPOSITORY when GITHUB_WORKFLOW_REF is empty", async () => {
- process.env.GITHUB_WORKFLOW_REF = "";
- process.env.GITHUB_REPOSITORY = "my-org/fallback-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "my-org/fallback-repo");
- });
-
- it("should fall back to GITHUB_REPOSITORY when GITHUB_WORKFLOW_REF has unexpected format", async () => {
- process.env.GITHUB_WORKFLOW_REF = "not-a-valid-ref";
- process.env.GITHUB_REPOSITORY = "my-org/fallback-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "my-org/fallback-repo");
- });
-
- it("should handle event-driven relay (issue_comment) that calls a cross-repo workflow", async () => {
- // This is the exact scenario from the bug report:
- // An issue_comment event in app-repo triggers a relay that calls the platform workflow.
- // GITHUB_WORKFLOW_REF reflects the platform workflow, GITHUB_REPOSITORY is the caller.
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/my-workflow.lock.yml@main";
- process.env.GITHUB_REPOSITORY = "my-org/app-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "my-org/platform-repo");
- expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Cross-repo invocation detected"));
- });
-
- it("should fall back to empty string when GITHUB_REPOSITORY is also undefined", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main";
- delete process.env.GITHUB_REPOSITORY;
-
- await main();
-
- // workflowRepo parsed from GITHUB_WORKFLOW_REF is "my-org/platform-repo"
- // currentRepo is "" since env var is deleted
- // targetRepo = workflowRepo || currentRepo = "my-org/platform-repo"
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "my-org/platform-repo");
- });
-
- it("should log GITHUB_WORKFLOW_REF and GITHUB_REPOSITORY", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "my-org/app-repo";
-
- await main();
-
- expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GITHUB_WORKFLOW_REF:"));
- expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GITHUB_REPOSITORY:"));
- });
-
- it("should output target_ref extracted from GITHUB_WORKFLOW_REF", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/feature-branch";
- process.env.GITHUB_REPOSITORY = "my-org/app-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "refs/heads/feature-branch");
- });
-
- it("should output target_ref for a short branch ref (not refs/heads/...)", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@main";
- process.env.GITHUB_REPOSITORY = "my-org/app-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "main");
- });
-
- it("should output target_ref for a feature branch in a caller-hosted relay", async () => {
- // This is the exact scenario from the bug report:
- // relay is pinned to @feature-branch, activation should check out feature-branch
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/platform-gateway.lock.yml@refs/heads/my-feature";
- process.env.GITHUB_REPOSITORY = "my-org/app-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "my-org/platform-repo");
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "refs/heads/my-feature");
- });
-
- it("should output empty target_ref when GITHUB_WORKFLOW_REF has no @ segment (no GITHUB_REF fallback)", async () => {
- // When GITHUB_WORKFLOW_REF has no '@', we cannot determine the callee ref.
- // We intentionally do NOT fall back to GITHUB_REF because in cross-repo scenarios
- // GITHUB_REF is the caller's ref, not the callee's. Empty string tells actions/checkout
- // to use the repository's default branch.
- process.env.GITHUB_WORKFLOW_REF = "not-a-valid-ref";
- process.env.GITHUB_REF = "refs/heads/fallback-branch";
- process.env.GITHUB_REPOSITORY = "my-org/fallback-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "");
- });
-
- it("should output empty target_ref when GITHUB_WORKFLOW_REF is empty", async () => {
- process.env.GITHUB_WORKFLOW_REF = "";
- delete process.env.GITHUB_REF;
- process.env.GITHUB_REPOSITORY = "my-org/fallback-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "");
- });
-
- it("should output target_ref for a tag ref", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/tags/v1.0.0";
- process.env.GITHUB_REPOSITORY = "my-org/app-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "refs/tags/v1.0.0");
- });
-
- it("should output target_ref for a commit SHA", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@abc123def456";
- process.env.GITHUB_REPOSITORY = "my-org/app-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "abc123def456");
- });
-
- it("should output target_repo_name when invoked cross-repo", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "my-org/app-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo_name", "platform-repo");
- });
-
- it("should output target_repo_name when same-repo invocation", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "my-org/platform-repo";
- mockNoReferencedWorkflowsOnce();
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo_name", "platform-repo");
- });
-
- it("should output target_repo_name without owner prefix when falling back to GITHUB_REPOSITORY", async () => {
- process.env.GITHUB_WORKFLOW_REF = "";
- process.env.GITHUB_REPOSITORY = "my-org/fallback-repo";
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo_name", "fallback-repo");
- });
-
- it("should include target_ref in step summary for cross-repo invocations", async () => {
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/feature-branch";
- process.env.GITHUB_REPOSITORY = "my-org/app-repo";
-
- await main();
-
- expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("refs/heads/feature-branch"));
- expect(mockCore.summary.write).toHaveBeenCalled();
- });
-
- describe("cross-org workflow_call scenarios", () => {
- it("should resolve callee repo via referenced_workflows API when GITHUB_WORKFLOW_REF matches GITHUB_REPOSITORY", async () => {
- // Cross-org workflow_call: GITHUB_WORKFLOW_REF points to the caller's repo (not the callee),
- // so workflowRepo === currentRepo. The referenced_workflows API returns the actual callee.
- process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "caller-org/caller-repo";
- process.env.GITHUB_RUN_ID = "12345";
-
- mockGetWorkflowRun.mockResolvedValueOnce({
- data: {
- referenced_workflows: [
- {
- path: "platform-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main",
- sha: "abc123def456",
- ref: "refs/heads/main",
- },
- ],
- },
- });
-
- await main();
-
- expect(mockGetWorkflowRun).toHaveBeenCalledWith({
- owner: "caller-org",
- repo: "caller-repo",
- run_id: 12345,
- });
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "platform-org/platform-repo");
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo_name", "platform-repo");
- // sha is preferred over ref
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "abc123def456");
- expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Resolved callee repo from referenced_workflows"));
- });
-
- it("should use ref from referenced_workflows entry when sha is absent", async () => {
- process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "caller-org/caller-repo";
- process.env.GITHUB_RUN_ID = "12345";
-
- mockGetWorkflowRun.mockResolvedValueOnce({
- data: {
- referenced_workflows: [
- {
- path: "platform-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/feature",
- sha: undefined,
- ref: "refs/heads/feature",
- },
- ],
- },
- });
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "platform-org/platform-repo");
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "refs/heads/feature");
- });
-
- it("should fall back to path-parsed ref when sha and ref are absent in referenced_workflows", async () => {
- process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "caller-org/caller-repo";
- process.env.GITHUB_RUN_ID = "12345";
-
- mockGetWorkflowRun.mockResolvedValueOnce({
- data: {
- referenced_workflows: [
- {
- path: "platform-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/stable",
- sha: undefined,
- ref: undefined,
- },
- ],
- },
- });
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "platform-org/platform-repo");
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "refs/heads/stable");
- });
-
- it("should log cross-repo detection and write step summary for cross-org callee", async () => {
- process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "caller-org/caller-repo";
- process.env.GITHUB_RUN_ID = "12345";
-
- mockGetWorkflowRun.mockResolvedValueOnce({
- data: {
- referenced_workflows: [
- {
- path: "platform-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main",
- sha: "abc123",
- ref: "refs/heads/main",
- },
- ],
- },
- });
-
- await main();
-
- expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Cross-repo invocation detected"));
- expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("platform-org/platform-repo"));
- expect(mockCore.summary.write).toHaveBeenCalled();
- });
-
- it("should fall back to GITHUB_REPOSITORY when referenced_workflows has no cross-org entry", async () => {
- // workflowRepo === currentRepo but no cross-org entry (same-org same-repo, no callee)
- process.env.GITHUB_WORKFLOW_REF = "my-org/my-repo/.github/workflows/my-workflow.lock.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "my-org/my-repo";
- process.env.GITHUB_RUN_ID = "12345";
-
- mockGetWorkflowRun.mockResolvedValueOnce({ data: { referenced_workflows: [] } });
-
- await main();
-
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "my-org/my-repo");
- expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No cross-org callee found in referenced_workflows"));
- });
-
- it("should fall back to GITHUB_REPOSITORY when referenced_workflows has multiple cross-org entries (ambiguous)", async () => {
- // Cannot safely select one callee when multiple cross-repo workflows are referenced.
- process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "caller-org/caller-repo";
- process.env.GITHUB_RUN_ID = "12345";
-
- mockGetWorkflowRun.mockResolvedValueOnce({
- data: {
- referenced_workflows: [
- {
- path: "platform-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main",
- sha: "abc123",
- ref: "refs/heads/main",
- },
- {
- path: "other-org/other-repo/.github/workflows/other.lock.yml@refs/heads/main",
- sha: "def456",
- ref: "refs/heads/main",
- },
- ],
- },
- });
-
- await main();
-
- // Falls back to currentRepo since the result is ambiguous
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "caller-org/caller-repo");
- expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Referenced workflows lookup is ambiguous"));
- });
-
- it("should fall back gracefully when referenced_workflows API call fails", async () => {
- process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "caller-org/caller-repo";
- process.env.GITHUB_RUN_ID = "12345";
-
- mockGetWorkflowRun.mockRejectedValueOnce(new Error("API unavailable"));
-
- await main();
-
- // Should fall back to the currentRepo (caller) — not ideal but safe degradation
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "caller-org/caller-repo");
- expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("API unavailable"));
- });
-
- it("should fall back gracefully when GITHUB_RUN_ID is missing", async () => {
- process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "caller-org/caller-repo";
- delete process.env.GITHUB_RUN_ID;
- mockContext.runId = NaN;
-
- await main();
-
- expect(mockGetWorkflowRun).not.toHaveBeenCalled();
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "caller-org/caller-repo");
- expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Run ID is unavailable or invalid"));
- });
-
- it("should not call referenced_workflows API for normal cross-repo (same-org) invocations", async () => {
- // workflowRepo !== currentRepo → no API call needed
- process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main";
- process.env.GITHUB_REPOSITORY = "my-org/app-repo";
- process.env.GITHUB_RUN_ID = "12345";
-
- await main();
-
- expect(mockGetWorkflowRun).not.toHaveBeenCalled();
- expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "my-org/platform-repo");
- });
- });
-});
diff --git a/pkg/workflow/checkout_manager.go b/pkg/workflow/checkout_manager.go
index 1a72e83ad47..ae639ac5b2b 100644
--- a/pkg/workflow/checkout_manager.go
+++ b/pkg/workflow/checkout_manager.go
@@ -132,7 +132,8 @@ type CheckoutManager struct {
// crossRepoTargetRepo holds the platform (host) repository to use when performing
// .github/.agents sparse checkout steps for cross-repo workflow_call invocations.
//
- // In the activation job this is set to "${{ steps.resolve-host-repo.outputs.target_repo }}".
+ // In the activation job this is set to "${{ steps.resolve-host-repo.outputs.target_repo }}"
+ // (derived from job.workflow_repository at runtime).
// In the agent and safe_outputs jobs it is set to "${{ needs.activation.outputs.target_repo }}".
// An empty string means the checkout targets the current repository (github.repository).
crossRepoTargetRepo string
@@ -140,7 +141,8 @@ type CheckoutManager struct {
// performing .github/.agents sparse checkout steps for cross-repo workflow_call
// invocations pinned to a non-default branch.
//
- // In the activation job this is set to "${{ steps.resolve-host-repo.outputs.target_ref }}".
+ // In the activation job this is set to "${{ steps.resolve-host-repo.outputs.target_ref }}"
+ // (derived from job.workflow_ref at runtime).
// In the agent and safe_outputs jobs it is set to "${{ needs.activation.outputs.target_ref }}".
// An empty string means the checkout uses the repository's default branch.
crossRepoTargetRef string
@@ -163,7 +165,8 @@ func NewCheckoutManager(userCheckouts []*CheckoutConfig) *CheckoutManager {
// .github/.agents sparse checkout steps. Call this when the workflow has a workflow_call
// trigger and the checkout should target the platform repo rather than github.repository.
//
-// In the activation job pass "${{ steps.resolve-host-repo.outputs.target_repo }}".
+// In the activation job pass "${{ steps.resolve-host-repo.outputs.target_repo }}"
+// (derived from job.workflow_repository at runtime).
// In downstream jobs (agent, safe_outputs) pass "${{ needs.activation.outputs.target_repo }}".
func (cm *CheckoutManager) SetCrossRepoTargetRepo(repo string) {
checkoutManagerLog.Printf("Setting cross-repo target repo: %q", repo)
@@ -181,7 +184,8 @@ func (cm *CheckoutManager) GetCrossRepoTargetRepo() string {
// .github/.agents sparse checkout steps. Call this when the workflow has a workflow_call
// trigger and the checkout should target a specific branch rather than the default branch.
//
-// In the activation job pass "${{ steps.resolve-host-repo.outputs.target_ref }}".
+// In the activation job pass "${{ steps.resolve-host-repo.outputs.target_ref }}"
+// (derived from job.workflow_ref at runtime).
// In downstream jobs (agent, safe_outputs) pass "${{ needs.activation.outputs.target_ref }}".
func (cm *CheckoutManager) SetCrossRepoTargetRef(ref string) {
checkoutManagerLog.Printf("Setting cross-repo target ref: %q", ref)
diff --git a/pkg/workflow/checkout_step_generator.go b/pkg/workflow/checkout_step_generator.go
index 6654d9710cd..fc7cca89173 100644
--- a/pkg/workflow/checkout_step_generator.go
+++ b/pkg/workflow/checkout_step_generator.go
@@ -83,10 +83,12 @@ func (cm *CheckoutManager) GenerateAdditionalCheckoutSteps(getActionPin func(str
//
// Parameters:
// - repository: the repository to checkout. May be a literal "owner/repo" value or a
-// GitHub Actions expression such as "${{ steps.resolve-host-repo.outputs.target_repo }}".
+// GitHub Actions expression such as "${{ steps.resolve-host-repo.outputs.target_repo }}"
+// (which is derived from job.workflow_repository at runtime).
// Pass an empty string to omit the repository field and check out the current repository.
// - ref: the branch, tag, or SHA to checkout. May be a literal value or a GitHub Actions
-// expression such as "${{ steps.resolve-host-repo.outputs.target_ref }}".
+// expression such as "${{ steps.resolve-host-repo.outputs.target_ref }}"
+// (which is derived from job.workflow_ref at runtime).
// Pass an empty string to omit the ref field and use the repository's default branch.
// - getActionPin: resolves an action reference to a pinned SHA form.
// - extraPaths: additional paths to include in the sparse-checkout beyond .github and .agents.
diff --git a/pkg/workflow/compiler_activation_job.go b/pkg/workflow/compiler_activation_job.go
index 4814e6b4fa3..7bd974b0682 100644
--- a/pkg/workflow/compiler_activation_job.go
+++ b/pkg/workflow/compiler_activation_job.go
@@ -52,11 +52,9 @@ func (c *Compiler) buildActivationJob(data *WorkflowData, preActivationJobCreate
// When a workflow_call trigger is present, resolve the platform (host) repository before
// generating aw_info so that target_repo can be included in aw_info.json and used by
- // the checkout step. This is necessary for event-driven relays (e.g. on: issue_comment)
- // where github.event_name is not 'workflow_call', making the previous expression
- // (github.event_name == 'workflow_call' && github.action_repository || github.repository)
- // unreliable. GITHUB_WORKFLOW_REF always reflects the executing workflow's repo regardless
- // of how it was triggered.
+ // the checkout step. The job context fields job.workflow_repository and job.workflow_ref
+ // are set by GitHub Actions to reflect the reusable (callee) workflow's own identity at
+ // runtime, regardless of the triggering event or whether the call is cross-org.
if hasWorkflowCallTrigger(data.On) && !data.InlinedImports {
compilerActivationJobLog.Print("Adding resolve-host-repo step for workflow_call trigger")
steps = append(steps, c.generateResolveHostRepoStep())
@@ -615,29 +613,25 @@ func (c *Compiler) generatePromptInActivationJob(steps *[]string, data *Workflow
}
// generateResolveHostRepoStep generates a step that resolves the platform (host) repository
-// for the activation job checkout by inspecting GITHUB_WORKFLOW_REF at runtime.
+// for the activation job checkout using the job context fields introduced in GitHub Actions.
//
-// This step replaces the previous compile-time expression
+// job.workflow_repository gives the owner/repo of the reusable workflow being executed (the
+// callee/platform repo), correctly handling cross-org and event-driven relay scenarios.
+// job.workflow_ref gives the full ref path (owner/repo/.github/workflows/file.yml@ref),
+// from which we extract the ref portion after '@'.
//
-// github.event_name == 'workflow_call' && github.action_repository || github.repository
-//
-// which only worked when the outermost trigger was workflow_call. For event-driven relays
-// (e.g. on: issue_comment, on: push) the event_name is the native event, so the old
-// expression always fell back to github.repository (the caller's repo), causing the
-// activation job to check out the wrong repository.
-//
-// GITHUB_WORKFLOW_REF always contains the path of the currently executing workflow file
-// (owner/repo/.github/workflows/file.yml@ref), regardless of the triggering event.
-// Comparing its owner/repo prefix with GITHUB_REPOSITORY reliably detects cross-repo
-// invocations for all relay patterns.
+// These fields replaced the previous approach of parsing GITHUB_WORKFLOW_REF at runtime and
+// falling back to the referenced_workflows API for cross-org workflow_call scenarios.
func (c *Compiler) generateResolveHostRepoStep() string {
var step strings.Builder
step.WriteString(" - name: Resolve host repo for activation checkout\n")
step.WriteString(" id: resolve-host-repo\n")
- step.WriteString(fmt.Sprintf(" uses: %s\n", GetActionPin("actions/github-script")))
- step.WriteString(" with:\n")
- step.WriteString(" script: |\n")
- step.WriteString(generateGitHubScriptWithRequire("resolve_host_repo.cjs"))
+ step.WriteString(" run: |\n")
+ step.WriteString(" repo=\"${{ job.workflow_repository }}\"\n")
+ step.WriteString(" ref=\"${{ job.workflow_ref }}\"\n")
+ step.WriteString(" echo \"target_repo=${repo}\" >> \"$GITHUB_OUTPUT\"\n")
+ step.WriteString(" echo \"target_repo_name=${repo##*/}\" >> \"$GITHUB_OUTPUT\"\n")
+ step.WriteString(" echo \"target_ref=${ref##*@}\" >> \"$GITHUB_OUTPUT\"\n")
return step.String()
}
diff --git a/pkg/workflow/compiler_activation_job_test.go b/pkg/workflow/compiler_activation_job_test.go
index 0c42822d1c5..8ea87e010ca 100644
--- a/pkg/workflow/compiler_activation_job_test.go
+++ b/pkg/workflow/compiler_activation_job_test.go
@@ -12,9 +12,8 @@ import (
// workflowCallRepo is the expression injected into the repository: field of the
// activation-job checkout step when a workflow_call trigger is detected.
-// The resolve-host-repo step (which runs before checkout) parses GITHUB_WORKFLOW_REF
-// at runtime to determine the platform repo, correctly handling both pure workflow_call
-// relays and event-driven relays (e.g. on: issue_comment) where event_name != 'workflow_call'.
+// The resolve-host-repo step reads job.workflow_repository at runtime to determine the
+// platform repo, correctly handling cross-org and event-driven relay scenarios.
const workflowCallRepo = "${{ steps.resolve-host-repo.outputs.target_repo }}"
// workflowCallRef is the expression injected into the ref: field of the activation-job
@@ -226,7 +225,7 @@ func TestGenerateGitHubFolderCheckoutStep(t *testing.T) {
}
// TestGenerateResolveHostRepoStep verifies that the resolve-host-repo step is correctly
-// generated and does not contain the broken event_name-based expression.
+// generated using job context fields instead of a JavaScript script.
func TestGenerateResolveHostRepoStep(t *testing.T) {
c := NewCompilerWithVersion("dev")
c.SetActionMode(ActionModeDev)
@@ -237,16 +236,24 @@ func TestGenerateResolveHostRepoStep(t *testing.T) {
"step should have the correct id")
assert.Contains(t, result, "Resolve host repo for activation checkout",
"step should have the correct name")
- assert.Contains(t, result, "actions/github-script",
- "step should use actions/github-script")
- assert.Contains(t, result, "resolve_host_repo.cjs",
- "step should require resolve_host_repo.cjs")
+ assert.Contains(t, result, "job.workflow_repository",
+ "step should use job.workflow_repository context field")
+ assert.Contains(t, result, "job.workflow_ref",
+ "step should use job.workflow_ref context field")
+ assert.Contains(t, result, "target_repo",
+ "step should set target_repo output")
+ assert.Contains(t, result, "target_repo_name",
+ "step should set target_repo_name output")
+ assert.Contains(t, result, "target_ref",
+ "step should set target_ref output")
// Verify the broken event_name expression is NOT present
assert.NotContains(t, result, "github.event_name == 'workflow_call'",
"step must not use the broken event_name-based expression")
assert.NotContains(t, result, "github.action_repository",
"step must not use github.action_repository (unreliable for event-driven relays)")
+ assert.NotContains(t, result, "resolve_host_repo.cjs",
+ "step must not use the old resolve_host_repo.cjs script")
}
// TestCheckoutDoesNotUseEventNameExpression verifies that the checkout step for