Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4e22b94
Support expression-valued dynamic checkouts
Copilot Sep 24, 2026
22e30b8
Harden dynamic checkout runtime handling
Copilot Sep 24, 2026
faa0782
Reject secrets. references in dynamic checkout expressions
Copilot Sep 24, 2026
3c1e797
Merge branch 'main' into copilot/dynamic-checkouts-github-action
github-actions[bot] Sep 24, 2026
0250e45
Harden dynamic checkout configuration
Copilot Sep 24, 2026
a40339d
Clarify dynamic checkout allowlist errors
Copilot Sep 24, 2026
801b499
Validate deferred dynamic checkout errors
Copilot Sep 24, 2026
faa71e2
Merge branch 'main' into copilot/dynamic-checkouts-github-action
github-actions[bot] Sep 24, 2026
53fad24
Polish dynamic checkout validation
Copilot Sep 24, 2026
55c159a
Allow dynamic wiki checkout lists
Copilot Sep 24, 2026
5e57e8d
Harden dynamic checkout docs and paths
Copilot Sep 24, 2026
e90d8f6
Polish dynamic checkout path validation
Copilot Sep 24, 2026
7062938
Prepare dynamic checkout review follow-up
Copilot Sep 24, 2026
1a90bef
Revert unrelated generated workflow change
Copilot Sep 24, 2026
2e9f6b7
Regenerate workflow locks after checkout changes
Copilot Sep 24, 2026
b948232
Merge branch 'main' into copilot/dynamic-checkouts-github-action
github-actions[bot] Sep 24, 2026
81577d7
Document dynamic checkout runtime modules
Copilot Sep 25, 2026
2b17a5e
Merge branch 'main' into copilot/dynamic-checkouts-github-action
github-actions[bot] Sep 25, 2026
3a3800e
docs: add dynamic checkout sets to spec and review skill
Copilot Sep 25, 2026
fab70be
Rename dynamic checkout field to repos
Copilot Sep 25, 2026
e517a96
Polish dynamic checkout repos validation
Copilot Sep 25, 2026
e5648f1
Document dynamic checkout field migration
Copilot Sep 25, 2026
05ba9a8
Clarify dynamic checkout migration error
Copilot Sep 25, 2026
553d2a1
Name unsupported dynamic checkout fields
Copilot Sep 25, 2026
3ce28a7
Stabilize dynamic checkout field errors
Copilot Sep 25, 2026
f38643f
Clarify dynamic checkout field list errors
Copilot Sep 25, 2026
b832d3d
Refine dynamic checkout diagnostics
Copilot Sep 25, 2026
88c6d55
Merge branch 'main' into copilot/dynamic-checkouts-github-action
github-actions[bot] Sep 25, 2026
fd68ce6
Clarify static vs dynamic checkout syntax
Copilot Sep 25, 2026
e82a8d7
Merge branch 'main' into copilot/dynamic-checkouts-github-action
github-actions[bot] Sep 25, 2026
5fbbffb
Merge branch 'main' into copilot/dynamic-checkouts-github-action
github-actions[bot] Sep 26, 2026
b13bdf4
Merge branch 'main' into copilot/dynamic-checkouts-github-action
github-actions[bot] Sep 26, 2026
de9f545
Merge main into dynamic checkouts branch
Copilot Sep 26, 2026
bfae6b1
Resolve checkout documentation merge conflicts
Copilot Sep 26, 2026
2b83bd4
Harden dynamic checkout ref, repository, sparse-checkout, and server …
Copilot Sep 26, 2026
c580198
Merge branch 'main' into copilot/dynamic-checkouts-github-action
Copilot Sep 26, 2026
a3202ef
Merge branch 'main' into copilot/dynamic-checkouts-github-action
github-actions[bot] Sep 27, 2026
1247780
Merge branch 'main' into copilot/dynamic-checkouts-github-action
github-actions[bot] Sep 27, 2026
1ce527c
Merge branch 'main' into copilot/dynamic-checkouts-github-action
github-actions[bot] Sep 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/skills/checkout-credential-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Two important contexts deliberately run with **no git credentials**:
- The **safe-outputs MCP server** and its handlers (`generate_git_bundle.cjs`, `generate_git_patch.cjs`, `create_pull_request.cjs`). Errors in these paths explicitly say "the safe-outputs MCP server has no credentials for private repositories" — fetch/push will fail for private repos.
- The **agent runtime** after `actions/checkout`. The agent prompt in [actions/setup/md/safe_outputs_push_to_pr_branch.md](../../../actions/setup/md/safe_outputs_push_to_pr_branch.md) explicitly tells the model not to attempt `git fetch`, `git pull`, `git push`, or any other authenticated git operation, and to report unavailable branches rather than try to fetch them.

Expression-valued `checkout: { repos: ..., allowed-repos: ... }` entries are checked out at runtime by [actions/setup/js/dynamic_checkouts.cjs](../../../actions/setup/js/dynamic_checkouts.cjs) rather than `actions/checkout`. The compiler resolves the expression once per job and passes it, plus the resolved `allowed-repos` allowlist, through `GH_AW_DYNAMIC_CHECKOUTS` / `GH_AW_DYNAMIC_CHECKOUT_ALLOWED_REPOS`. Each dynamic checkout uses a command-level token (`GH_AW_DYNAMIC_CHECKOUT_TOKEN`, falling back to `GH_TOKEN`) injected only into that git invocation; the runtime does not persist git credentials afterward unless the caller explicitly asks it to keep them (`GH_AW_DYNAMIC_CHECKOUT_PERSIST_CREDENTIALS=true`), which only the `safe_outputs` job's PR/push checkout path does.

## Review checklist

When you see a new `git`, `gh`, `execFileSync('git'…)`, or compiled `run:` block:
Expand All @@ -27,10 +29,14 @@ When you see a new `git`, `gh`, `execFileSync('git'…)`, or compiled `run:` blo
3. **Which job/context emits it?** Agent job and safe-outputs MCP server both run without git credentials by design. Any remote git operation there must be wrapped in `try/catch`, fail soft, and surface a clear "no credentials" error rather than a raw git stderr.
4. **Sparse / shallow / monorepo concerns.** Avoid emitting steps that deepen (`git fetch --unshallow`, `--deepen=N`) or widen (`git fetch origin '+refs/heads/*'`) a sparse or shallow checkout of a large monorepo — these need credentials *and* can pull hundreds of MB. Prefer expanding `fetch:` / `fetch-depth:` / `sparse-checkout:` at compile time so it happens during `actions/checkout` with its internal token, never later.
5. **`gh` is REST, not git.** `gh api …` uses whatever `GH_TOKEN` is in the step's env — it does **not** automatically inherit per-checkout PATs. For cross-org private repos, either thread the right token in or accept the call will 404 and handle it.
6. **Dynamic checkout expressions.** A `checkout.repos` expression MUST NOT reference `secrets.*` directly — its resolved value lands in the single `GH_AW_DYNAMIC_CHECKOUTS` JSON payload rather than a statically declared env var, hiding the secret from static analysis; the fix is a top-level `env:` entry referenced via `env.NAME`. It also MUST NOT reference `steps.*`, since the same expression is re-evaluated independently in the agent job and the `safe_outputs` job. `allowed-repos` is mandatory and must come from trusted configuration rather than caller-controlled input. Any new field or code path added to `dynamic_checkouts.cjs` must keep passing `ref`/sparse-checkout patterns to `git` after a `--` terminator, keep `GIT_LFS_SKIP_SMUDGE=1` on non-LFS operations, and keep validating that checkout paths cannot escape the workspace via `..`, absolute paths, or symlinks.

## Related

- [docs/src/content/docs/reference/checkout.md](../../../docs/src/content/docs/reference/checkout.md) — "Git Credentials After Checkout"
- [docs/sparseness.md](../../../docs/sparseness.md) — sparse/blobless credential lifecycle
- [pkg/workflow/checkout_step_generator.go](../../../pkg/workflow/checkout_step_generator.go) — token wiring per checkout
- [actions/setup/md/safe_outputs_push_to_pr_branch.md](../../../actions/setup/md/safe_outputs_push_to_pr_branch.md) — agent-facing guidance
- [actions/setup/js/dynamic_checkouts.cjs](../../../actions/setup/js/dynamic_checkouts.cjs) — dynamic checkout runtime credential/path/git-arg handling
- [pkg/workflow/dynamic_checkout_context_validation.go](../../../pkg/workflow/dynamic_checkout_context_validation.go) and [pkg/workflow/dynamic_checkout_secrets_validation.go](../../../pkg/workflow/dynamic_checkout_secrets_validation.go) — compile-time `steps.*`/`secrets.*` rejection
- [docs/src/content/docs/specs/checkout-behavior-specification.md](../../../docs/src/content/docs/specs/checkout-behavior-specification.md) — §3.6 Dynamic Checkout Sets normative requirements
12 changes: 11 additions & 1 deletion actions/setup/js/build_checkout_manifest.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,17 @@ function buildCheckoutManifest(entries, options = {}) {
throw new Error(`Failed to create directory ${manifestDir}: ${getErrorMessage(err)}`, { cause: err });
}
const manifestPath = path.join(manifestDir, "checkout-manifest.json");
const manifest = {};
let manifest = {};
try {
if (fs.existsSync(manifestPath)) {
const existing = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
manifest = existing;
}
}
} catch (error) {
core.debug(`checkout-manifest: ignoring unreadable existing manifest: ${getErrorMessage(error)}`);
}
core.info(`checkout-manifest: building manifest for ${entries.length} checkout entries`);

for (const entry of entries) {
Expand Down
18 changes: 18 additions & 0 deletions actions/setup/js/build_checkout_manifest.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,22 @@ describe("build_checkout_manifest.cjs", () => {
const fileContents = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
expect(fileContents).toEqual(manifest);
});

it("preserves dynamic entries already written to the manifest", () => {
const workspace = createTempDir("checkout-manifest-workspace-");
const runnerTemp = createTempDir("checkout-manifest-runner-temp-");
tempDirs.push(workspace, runnerTemp);
const manifestDir = path.join(runnerTemp, "gh-aw", "safeoutputs");
fs.mkdirSync(manifestDir, { recursive: true });
fs.writeFileSync(path.join(manifestDir, "checkout-manifest.json"), JSON.stringify({ "owner/dynamic": { repository: "owner/dynamic", path: "dynamic", default_branch: "main" } }));

const { manifest } = buildCheckoutManifest([{ repository: "owner/static", path: "static" }], {
workspace,
runnerTemp,
runGH: () => "main\n",
});

expect(manifest).toHaveProperty("owner/dynamic");
expect(manifest).toHaveProperty("owner/static");
});
});
Loading
Loading