Repository navigation
Support dynamic checkout sets from GitHub Actions expressions #63241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
4e22b94
22e30b8
faa0782
3c1e797
0250e45
a40339d
801b499
faa71e2
53fad24
55c159a
5e57e8d
e90d8f6
7062938
1a90bef
2e9f6b7
b948232
81577d7
2b17a5e
3a3800e
fab70be
e517a96
e5648f1
05ba9a8
553d2a1
3ce28a7
f38643f
b832d3d
88c6d55
fd68ce6
e82a8d7
5fbbffb
b13bdf4
de9f545
bfae6b1
2b83bd4
c580198
a3202ef
1247780
1ce527c
b5015e2
c7e114f
6c95d54
80e0b93
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ const fs = require("fs"); | |
| const path = require("path"); | ||
| const { getErrorMessage } = require("./error_helpers.cjs"); | ||
|
|
||
| const supportedFields = new Set(["repository", "ref", "path", "github-token", "token", "fetch-depth", "sparse-checkout", "submodules", "lfs", "current", "wiki"]); | ||
| const supportedFields = new Set(["repository", "ref", "path", "github-token", "token", "fetch-depth", "sparse-checkout", "submodules", "lfs", "wiki"]); | ||
|
|
||
| function parseDynamicCheckouts(value = process.env.GH_AW_DYNAMIC_CHECKOUTS || "") { | ||
| if (!value.trim()) { | ||
|
|
@@ -37,9 +37,6 @@ function normalizeCheckout(entry, workspace) { | |
| if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) { | ||
| throw new Error(`dynamic checkout repository must use owner/repo format, got '${repository}'`); | ||
| } | ||
| if (entry.current !== undefined && typeof entry.current !== "boolean") { | ||
| throw new Error("dynamic checkout current must be a boolean"); | ||
| } | ||
| if (entry.lfs !== undefined && typeof entry.lfs !== "boolean") { | ||
| throw new Error("dynamic checkout lfs must be a boolean"); | ||
| } | ||
|
|
@@ -68,13 +65,11 @@ function normalizeCheckout(entry, workspace) { | |
| repository, | ||
| ref: String(entry.ref || "").trim(), | ||
| path: checkoutPath, | ||
| target: path.resolve(workspace, checkoutPath), | ||
| token: String(entry["github-token"] || entry.token || ""), | ||
| fetchDepth, | ||
| sparseCheckout: String(entry["sparse-checkout"] || ""), | ||
| submodules: entry.submodules, | ||
| lfs: entry.lfs === true, | ||
| current: entry.current === true, | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -99,20 +94,37 @@ function credentialArgs(serverURL, token, maskSecret = value => core.setSecret(v | |
| async function checkoutRepository(checkout, options = {}) { | ||
| const workspace = options.workspace || process.env.GITHUB_WORKSPACE || ""; | ||
| const serverURL = (options.serverURL || process.env.GITHUB_SERVER_URL || "https://github.com").replace(/\/+$/, ""); | ||
| const token = options.overrideToken || checkout.token || process.env.GH_TOKEN || ""; | ||
| const token = checkout.token || options.overrideToken || process.env.GH_TOKEN || ""; | ||
| const persistCredentials = options.persistCredentials === true; | ||
| const runGit = options.runGit || defaultRunGit; | ||
|
|
||
| let workspaceReal; | ||
| let checkoutTarget; | ||
| try { | ||
| workspaceReal = fs.realpathSync(workspace); | ||
| if (fs.existsSync(checkout.target)) { | ||
| checkoutTarget = path.join(workspaceReal, checkout.path); | ||
| if (fs.existsSync(checkoutTarget)) { | ||
| throw new Error(`dynamic checkout path already exists: ${checkout.path}`); | ||
| } | ||
| fs.mkdirSync(path.dirname(checkout.target), { recursive: true }); | ||
| const parentReal = fs.realpathSync(path.dirname(checkout.target)); | ||
| if (parentReal !== workspaceReal && !parentReal.startsWith(workspaceReal + path.sep)) { | ||
| throw new Error(`dynamic checkout path escapes the workspace through a symbolic link: ${checkout.path}`); | ||
|
|
||
| let parent = workspaceReal; | ||
| const parentSegments = path | ||
| .dirname(checkout.path) | ||
| .split(path.sep) | ||
| .filter(segment => segment && segment !== "."); | ||
| for (const segment of parentSegments) { | ||
| const candidate = path.join(parent, segment); | ||
| if (fs.existsSync(candidate)) { | ||
| if (fs.lstatSync(candidate).isSymbolicLink()) { | ||
| throw new Error(`dynamic checkout path traverses a symbolic link: ${checkout.path}`); | ||
| } | ||
| } else { | ||
| fs.mkdirSync(candidate); | ||
| } | ||
| parent = fs.realpathSync(candidate); | ||
| if (parent !== workspaceReal && !parent.startsWith(workspaceReal + path.sep)) { | ||
| throw new Error(`dynamic checkout path escapes the workspace: ${checkout.path}`); | ||
| } | ||
| } | ||
|
Comment on lines
+186
to
+213
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } catch (error) { | ||
| throw new Error(`failed to prepare dynamic checkout path '${checkout.path}': ${getErrorMessage(error)}`, { cause: error }); | ||
|
|
@@ -123,61 +135,60 @@ async function checkoutRepository(checkout, options = {}) { | |
| if (checkout.fetchDepth > 0) { | ||
| cloneArgs.push("--depth", String(checkout.fetchDepth)); | ||
| } | ||
| cloneArgs.push(`${serverURL}/${checkout.repository}.git`, checkout.target); | ||
| cloneArgs.push(`${serverURL}/${checkout.repository}.git`, checkoutTarget); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnosing-bugs] 💡 Suggested fixPass const gitEnv = checkout.lfs ? undefined : { ...process.env, GIT_LFS_SKIP_SMUDGE: "1" };
await runGit(cloneArgs, { env: gitEnv });@copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already addressed in 801b499: clone, checkout, sparse-checkout, and submodule worktree updates now run with |
||
|
|
||
| core.info(`Checking out ${checkout.repository} into ${checkout.path}`); | ||
| await runGit(cloneArgs); | ||
| if (checkout.ref) { | ||
| const fetchArgs = [...authArgs, "-C", checkout.target, "fetch", "--no-tags"]; | ||
| const fetchArgs = [...authArgs, "-C", checkoutTarget, "fetch", "--no-tags"]; | ||
| if (checkout.fetchDepth > 0) { | ||
| fetchArgs.push("--depth", String(checkout.fetchDepth)); | ||
| } | ||
| fetchArgs.push("origin", checkout.ref); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnosing-bugs] 💡 Suggested fixfetchArgs.push("origin", "--", checkout.ref);Add a regression test with a ref like @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in the current branch: |
||
| await runGit(fetchArgs); | ||
| await runGit(["-C", checkout.target, "checkout", "--force", "FETCH_HEAD"]); | ||
| await runGit(["-C", checkoutTarget, "checkout", "--force", "FETCH_HEAD"]); | ||
| } | ||
|
|
||
| if (checkout.sparseCheckout.trim()) { | ||
| const patterns = checkout.sparseCheckout | ||
| .split(/\r?\n/) | ||
| .map(pattern => pattern.trim()) | ||
| .filter(Boolean); | ||
| await runGit(["-C", checkout.target, "sparse-checkout", "set", "--no-cone", ...patterns]); | ||
| await runGit(["-C", checkoutTarget, "sparse-checkout", "set", "--no-cone", ...patterns]); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sparse-checkout patterns are now validated and passed after Git's option terminator. Addressed in 801b499.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verified addressed in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnosing-bugs] Same argument-injection class as the 💡 Suggested fixawait runGit(["-C", checkoutTarget, "sparse-checkout", "set", "--no-cone", "--", ...patterns]);@copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in the current branch: sparse-checkout patterns are validated to reject leading
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in the current branch: sparse-checkout patterns beginning with |
||
| } | ||
| if (checkout.submodules === true || checkout.submodules === "true" || checkout.submodules === "recursive") { | ||
| const args = [...authArgs, "-C", checkout.target, "submodule", "update", "--init"]; | ||
| const args = [...authArgs, "-C", checkoutTarget, "submodule", "update", "--init"]; | ||
| if (checkout.submodules === "recursive") { | ||
| args.push("--recursive"); | ||
| } | ||
| await runGit(args); | ||
| } | ||
| if (checkout.lfs) { | ||
| await runGit([...authArgs, "-C", checkout.target, "lfs", "pull"]); | ||
| await runGit([...authArgs, "-C", checkoutTarget, "lfs", "pull"]); | ||
| } | ||
|
|
||
| const credentialKey = `http.${serverURL}/.extraheader`; | ||
| if (persistCredentials && token) { | ||
| const encoded = Buffer.from(`x-access-token:${token}`).toString("base64"); | ||
| await runGit(["-C", checkout.target, "config", credentialKey, `AUTHORIZATION: basic ${encoded}`]); | ||
| await runGit(["-C", checkoutTarget, "config", credentialKey, `AUTHORIZATION: basic ${encoded}`]); | ||
| } else { | ||
| try { | ||
| await runGit(["-C", checkout.target, "config", "--unset-all", credentialKey]); | ||
| await runGit(["-C", checkoutTarget, "config", "--unset-all", credentialKey]); | ||
| } catch (error) { | ||
| core.debug(`No persisted credential needed removal: ${getErrorMessage(error)}`); | ||
| } | ||
| } | ||
|
|
||
| let defaultBranch = ""; | ||
| try { | ||
| defaultBranch = (await runGit(["-C", checkout.target, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).replace(/^origin\//, ""); | ||
| defaultBranch = (await runGit(["-C", checkoutTarget, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).replace(/^origin\//, ""); | ||
| } catch (error) { | ||
| core.debug(`Could not resolve dynamic checkout default branch: ${getErrorMessage(error)}`); | ||
| } | ||
| return { | ||
| repository: checkout.repository, | ||
| path: checkout.path, | ||
| default_branch: defaultBranch, | ||
| current: checkout.current, | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -210,9 +221,6 @@ async function main(options = {}) { | |
| } | ||
| const checkouts = parseDynamicCheckouts(options.value); | ||
| const normalized = checkouts.map(entry => normalizeCheckout(entry, workspace)); | ||
| if (normalized.filter(entry => entry.current).length > 1) { | ||
| throw new Error("only one dynamic checkout may set current: true"); | ||
| } | ||
| if (new Set(normalized.map(entry => entry.path.toLowerCase())).size !== normalized.length) { | ||
| throw new Error("dynamic checkout paths must be unique"); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
L123: shrink: duplicated token alias handling (
github-tokenandtoken) every normalize call. Resolve once (const token = entry["github-token"] ?? entry.token ?? "") and stringify once.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No change for this style-only simplification: the current normalization keeps the token aliases close to the returned checkout shape without changing behavior.