Skip to content

Commit 4afd3c1

Browse files
committed
feat: replace resolve_host_repo.cjs with job.workflow_* context
Replace the 208-line JavaScript host repo resolver with a simplified version that reads job.workflow_repository and job.workflow_sha from environment variables instead of parsing GITHUB_WORKFLOW_REF and falling back to the referenced_workflows API. Key improvements: - job.workflow_repository directly provides the platform repo, eliminating GITHUB_WORKFLOW_REF parsing and the referenced_workflows API fallback for cross-org scenarios - job.workflow_sha provides an immutable commit SHA for checkout pinning, instead of a potentially moving branch/tag ref - Context values passed via env: block to avoid shell injection - Extensive logging of all job.workflow_* fields for observability - Deletes the old resolve_host_repo.cjs and its 459-line test file
1 parent 5a06d31 commit 4afd3c1

8 files changed

Lines changed: 79 additions & 679 deletions

File tree

.github/workflows/smoke-workflow-call-with-inputs.lock.yml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/smoke-workflow-call.lock.yml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

actions/setup/js/resolve_host_repo.cjs

Lines changed: 34 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -4,202 +4,55 @@
44
/**
55
* Resolves the target repository and ref for the activation job checkout.
66
*
7-
* Uses GITHUB_WORKFLOW_REF to determine the platform (host) repository and branch/ref
8-
* regardless of the triggering event. This fixes cross-repo activation for event-driven
9-
* relays (e.g. on: issue_comment, on: push) where github.event_name is NOT 'workflow_call',
10-
* so the expression introduced in #20301 incorrectly fell back to github.repository
11-
* (the caller's repo) instead of the platform repo.
7+
* Uses the job.workflow_* context fields to determine the platform (host)
8+
* repository and pin the checkout to the exact executing commit SHA.
129
*
13-
* GITHUB_WORKFLOW_REF reflects the currently executing workflow file for most triggers, but
14-
* in cross-org workflow_call scenarios it resolves to the TOP-LEVEL CALLER's workflow ref,
15-
* not the reusable (callee) workflow being executed. Its format is:
16-
* owner/repo/.github/workflows/file.yml@refs/heads/main
10+
* These fields are passed via environment variables (JOB_WORKFLOW_REPOSITORY,
11+
* JOB_WORKFLOW_SHA, etc.) to avoid shell injection — the ${{ }} expressions
12+
* are evaluated in the env: block, not interpolated into script source.
1713
*
18-
* When the platform workflow runs cross-repo (called via uses: from the same org),
19-
* GITHUB_WORKFLOW_REF starts with the platform repo slug, while GITHUB_REPOSITORY is the
20-
* caller repo. Comparing the two lets us detect cross-repo invocations without relying on
21-
* event_name.
14+
* job.workflow_repository provides the owner/repo of the currently executing
15+
* workflow file, correctly identifying the platform repo in all relay patterns:
16+
* cross-repo workflow_call, event-driven relays (on: issue_comment, on: push),
17+
* and cross-org scenarios.
2218
*
23-
* For cross-org workflow_call, GITHUB_WORKFLOW_REF and GITHUB_REPOSITORY both resolve to
24-
* the caller's repo. In that case we fall back to the referenced_workflows API lookup to
25-
* find the actual callee (platform) repo and ref.
19+
* job.workflow_sha provides the immutable commit SHA of the workflow being
20+
* executed, ensuring the activation checkout pins to the exact revision rather
21+
* than a moving branch/tag ref.
2622
*
27-
* In a caller-hosted relay pinned to a feature branch (e.g. uses: platform/.github/workflows/
28-
* gateway.lock.yml@feature-branch), the @feature-branch portion is encoded in
29-
* GITHUB_WORKFLOW_REF. Emitting it as target_ref allows the activation checkout to use
30-
* the correct branch rather than the platform repo's default branch.
31-
*
32-
* SEC-005: The targetRepo and targetRef values are resolved solely from trusted system
33-
* environment variables (GITHUB_WORKFLOW_REF, GITHUB_REPOSITORY, GITHUB_REF) and the
34-
* GitHub Actions API (referenced_workflows), set/provided by the GitHub Actions runtime.
35-
* They are not derived from user-supplied input, so no allowlist check is required here.
36-
*
37-
* @safe-outputs-exempt SEC-005: values sourced from trusted GitHub Actions runtime env vars and referenced_workflows API only
23+
* @safe-outputs-exempt SEC-005: values sourced from trusted GitHub Actions runner context via env vars only
3824
*/
3925

40-
// Matches the "owner/repo" prefix from a GitHub workflow path of the form "owner/repo/...".
41-
const REPO_PREFIX_RE = /^([^/]+\/[^/]+)\//;
42-
43-
/**
44-
* Attempts to resolve the callee repository and ref from the referenced_workflows API.
45-
*
46-
* This is used as a fallback when GITHUB_WORKFLOW_REF points to the same repo as
47-
* GITHUB_REPOSITORY (cross-org workflow_call scenario), because in that case
48-
* GITHUB_WORKFLOW_REF reflects the caller's workflow ref, not the callee's.
49-
*
50-
* @param {string} currentRepo - The value of GITHUB_REPOSITORY (owner/repo)
51-
* @returns {Promise<{repo: string, ref: string} | null>} Resolved callee repo and ref, or null
52-
*/
53-
async function resolveFromReferencedWorkflows(currentRepo) {
54-
const rawRunId = process.env.GITHUB_RUN_ID;
55-
const runId = rawRunId ? parseInt(rawRunId, 10) : typeof context.runId === "number" ? context.runId : NaN;
56-
if (!Number.isFinite(runId)) {
57-
core.info("Run ID is unavailable or invalid, cannot perform referenced_workflows lookup");
58-
return null;
59-
}
60-
61-
const [runOwner, runRepo] = currentRepo.split("/");
62-
try {
63-
core.info(`Checking for cross-org callee via referenced_workflows API (run ${runId}, repo ${currentRepo})`);
64-
const runResponse = await github.rest.actions.getWorkflowRun({
65-
owner: runOwner,
66-
repo: runRepo,
67-
run_id: runId,
68-
});
69-
70-
const referencedWorkflows = runResponse.data.referenced_workflows || [];
71-
core.info(`Found ${referencedWorkflows.length} referenced workflow(s) in run`);
72-
for (const wf of referencedWorkflows) {
73-
core.info(` referenced workflow: path=${wf.path} sha=${wf.sha || "(none)"} ref=${wf.ref || "(none)"}`);
74-
}
75-
76-
// Collect all referenced workflows from a different repo than the caller.
77-
// In cross-org workflow_call, the callee (platform) repo is different from currentRepo.
78-
// If multiple cross-repo candidates are found we cannot safely pick one, so we bail out.
79-
const crossRepoCandidates = [];
80-
for (const wf of referencedWorkflows) {
81-
const pathRepoMatch = wf.path.match(REPO_PREFIX_RE);
82-
const entryRepo = pathRepoMatch ? pathRepoMatch[1] : "";
83-
if (entryRepo && entryRepo !== currentRepo) {
84-
crossRepoCandidates.push({ wf, repo: entryRepo });
85-
}
86-
}
87-
core.info(`Found ${crossRepoCandidates.length} cross-repo candidate(s) (excluding current repo ${currentRepo})`);
88-
89-
if (crossRepoCandidates.length === 0) {
90-
core.info("No cross-org callee found in referenced_workflows, using current repo");
91-
return null;
92-
}
93-
94-
if (crossRepoCandidates.length > 1) {
95-
core.info(`Referenced workflows lookup is ambiguous; found ${crossRepoCandidates.length} cross-repo candidates, not selecting one`);
96-
for (const candidate of crossRepoCandidates) {
97-
core.info(` Candidate referenced workflow path: ${candidate.wf.path}`);
98-
}
99-
return null;
100-
}
101-
102-
const matchingEntry = crossRepoCandidates[0].wf;
103-
const calleeRepo = crossRepoCandidates[0].repo;
104-
105-
// Prefer sha (immutable) over ref (branch/tag can drift) over path-parsed ref.
106-
const pathRefMatch = matchingEntry.path.match(/@(.+)$/);
107-
let calleeRefSource;
108-
if (matchingEntry.sha) {
109-
calleeRefSource = "sha";
110-
} else if (matchingEntry.ref) {
111-
calleeRefSource = "ref";
112-
} else if (pathRefMatch) {
113-
calleeRefSource = "path";
114-
} else {
115-
calleeRefSource = "none";
116-
}
117-
const calleeRef = matchingEntry.sha || matchingEntry.ref || (pathRefMatch ? pathRefMatch[1] : "");
118-
core.info(`Resolved callee repo from referenced_workflows: ${calleeRepo} @ ${calleeRef || "(default branch)"} (source: ${calleeRefSource})`);
119-
core.info(` Referenced workflow path: ${matchingEntry.path}`);
120-
return { repo: calleeRepo, ref: calleeRef };
121-
} catch (error) {
122-
const msg = error instanceof Error ? error.message : String(error);
123-
core.info(`Could not fetch referenced_workflows from API: ${msg}, using current repo`);
124-
return null;
125-
}
126-
}
127-
12826
/**
12927
* @returns {Promise<void>}
13028
*/
13129
async function main() {
132-
const workflowRef = process.env.GITHUB_WORKFLOW_REF || "";
30+
const targetRepo = process.env.JOB_WORKFLOW_REPOSITORY || "";
31+
const targetRef = process.env.JOB_WORKFLOW_SHA || "";
32+
const targetRepoName = targetRepo.split("/").pop() || "";
13333
const currentRepo = process.env.GITHUB_REPOSITORY || "";
13434

135-
core.info(`GITHUB_WORKFLOW_REF: ${workflowRef || "(not set)"}`);
136-
core.info(`GITHUB_REPOSITORY: ${currentRepo || "(not set)"}`);
137-
core.info(`GITHUB_RUN_ID: ${process.env.GITHUB_RUN_ID || "(not set)"}`);
138-
139-
// GITHUB_WORKFLOW_REF format: owner/repo/.github/workflows/file.yml@ref
140-
// The regex captures everything before the third slash segment (i.e., the owner/repo prefix).
141-
const repoMatch = workflowRef.match(REPO_PREFIX_RE);
142-
const workflowRepo = repoMatch ? repoMatch[1] : "";
143-
core.info(`Parsed workflow repo from GITHUB_WORKFLOW_REF: ${workflowRepo || "(could not parse)"}`);
144-
145-
// Fall back to currentRepo when GITHUB_WORKFLOW_REF cannot be parsed
146-
let targetRepo = workflowRepo || currentRepo;
147-
148-
// Extract the ref portion after '@' from GITHUB_WORKFLOW_REF.
149-
// GITHUB_WORKFLOW_REF format: owner/repo/.github/workflows/file.yml@ref
150-
// The ref may be a full ref like "refs/heads/feature-branch", a short name like "main",
151-
// a tag like "refs/tags/v1.0.0", or a commit SHA like "abc123def".
152-
//
153-
// When GITHUB_WORKFLOW_REF has no '@' segment (e.g., env var not set or malformed),
154-
// fall back to an empty string so that actions/checkout uses the repository's default
155-
// branch. We intentionally do NOT fall back to GITHUB_REF here because in cross-repo
156-
// scenarios GITHUB_REF is the *caller* repo's ref, not the callee's, and using it
157-
// would check out the wrong branch.
158-
const refMatch = workflowRef.match(/@(.+)$/);
159-
let targetRef = refMatch ? refMatch[1] : "";
160-
core.info(`Parsed workflow ref from GITHUB_WORKFLOW_REF: ${targetRef || "(none — will use default branch)"}`);
161-
162-
// Cross-org workflow_call detection: when GITHUB_WORKFLOW_REF points to the same repo as
163-
// GITHUB_REPOSITORY, it means GITHUB_WORKFLOW_REF is resolving to the caller's workflow
164-
// (not the callee's). This happens in cross-org workflow_call invocations where GitHub
165-
// Actions sets GITHUB_WORKFLOW_REF to the top-level caller's workflow ref rather than the
166-
// reusable workflow being executed. In that case, fall back to the referenced_workflows API
167-
// to find the actual callee (platform) repo and ref.
168-
//
169-
// Note: GITHUB_EVENT_NAME inside a reusable workflow reflects the ORIGINAL trigger event
170-
// (e.g., "push", "issues"), NOT "workflow_call", so we cannot use event_name to detect
171-
// this scenario.
172-
if (workflowRepo && workflowRepo === currentRepo) {
173-
core.info(`Cross-org workflow_call detected (workflowRepo === currentRepo = ${currentRepo}): falling back to referenced_workflows API`);
174-
const resolved = await resolveFromReferencedWorkflows(currentRepo);
175-
if (resolved) {
176-
targetRepo = resolved.repo;
177-
targetRef = resolved.ref || targetRef;
178-
} else {
179-
core.info("referenced_workflows lookup returned no result; keeping current repo as target");
180-
}
181-
} else if (!workflowRepo) {
182-
core.info("Could not parse workflowRepo from GITHUB_WORKFLOW_REF; falling back to GITHUB_REPOSITORY");
183-
} else {
184-
core.info(`Same-org cross-repo invocation: workflowRepo=${workflowRepo}, currentRepo=${currentRepo}`);
185-
}
186-
187-
core.info(`Resolved host repo for activation checkout: ${targetRepo}`);
188-
core.info(`Resolved host ref for activation checkout: ${targetRef || "(default branch)"}`);
189-
190-
if (targetRepo !== currentRepo && targetRepo !== "") {
191-
core.info(`Cross-repo invocation detected: platform repo is "${targetRepo}", caller is "${currentRepo}"`);
192-
await core.summary.addRaw(`**Activation Checkout**: Checking out platform repo \`${targetRepo}\` @ \`${targetRef}\` (caller: \`${currentRepo}\`)`).write();
35+
core.info("Resolving host repo via job.workflow_* context");
36+
core.info(`job.workflow_repository = ${targetRepo}`);
37+
core.info(`job.workflow_sha = ${targetRef}`);
38+
core.info(`job.workflow_ref = ${process.env.JOB_WORKFLOW_REF || ""}`);
39+
core.info(`job.workflow_file_path = ${process.env.JOB_WORKFLOW_FILE_PATH || ""}`);
40+
core.info(`github.repository = ${currentRepo}`);
41+
core.info("");
42+
core.info(`Resolved target_repo = ${targetRepo}`);
43+
core.info(`Resolved target_repo_name = ${targetRepoName}`);
44+
core.info(`Resolved target_ref = ${targetRef}`);
45+
46+
if (targetRepo && targetRepo !== currentRepo) {
47+
core.info(
48+
`Cross-repo invocation detected: platform repo "${targetRepo}" differs from caller "${currentRepo}"`
49+
);
19350
} else {
194-
core.info(`Same-repo invocation: checking out ${targetRepo} @ ${targetRef || "(default branch)"}`);
51+
core.info(
52+
`Same-repo invocation: platform and caller are both "${targetRepo}"`
53+
);
19554
}
19655

197-
// Compute the repository name (without owner prefix) for use cases that require
198-
// only the repo name, such as actions/create-github-app-token which expects
199-
// `repositories` to contain repo names only when `owner` is also provided.
200-
const targetRepoName = targetRepo.split("/").at(-1);
201-
core.info(`target_repo=${targetRepo} target_repo_name=${targetRepoName} target_ref=${targetRef || "(default branch)"}`);
202-
20356
core.setOutput("target_repo", targetRepo);
20457
core.setOutput("target_repo_name", targetRepoName);
20558
core.setOutput("target_ref", targetRef);

0 commit comments

Comments
 (0)