-
Notifications
You must be signed in to change notification settings - Fork 30
chore(deps): update actions/checkout action to v6 #1273
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
base: main
Are you sure you want to change the base?
Conversation
|
👋 Thanks for Submitting! This PR is available for preview at the link below. ✅ PR tip preview: https://1273.pr.nala.bravesoftware.com/ - ./tokens/css/variables-android.old.css: 7390 bytes
+ ./tokens/css/variables-android.css: 7390 bytes
---
- ./tokens/css/variables-browser.old.css: 6644 bytes
+ ./tokens/css/variables-browser.css: 6644 bytes
---
- ./tokens/css/variables-ios.old.css: 8180 bytes
+ ./tokens/css/variables-ios.css: 8180 bytes
---
- ./tokens/css/variables-marketing.old.css: 13501 bytes
+ ./tokens/css/variables-marketing.css: 13501 bytes
---
- ./tokens/css/variables-news.old.css: 526 bytes
+ ./tokens/css/variables-news.css: 526 bytes
---
- ./tokens/css/variables-newtab.old.css: 1933 bytes
+ ./tokens/css/variables-newtab.css: 1933 bytes
---
- ./tokens/css/variables-search.old.css: 17733 bytes
+ ./tokens/css/variables-search.css: 17733 bytes
---
- ./tokens/css/variables-web3.old.css: 893 bytes
+ ./tokens/css/variables-web3.css: 893 bytes
---
- ./tokens/css/variables.old.css: 125593 bytes
+ ./tokens/css/variables.css: 125593 bytes
Variables Diff: variables-android.diff--- ./tokens/css/variables-android.old.css 2025-11-27 18:53:10.128193337 +0000
+++ ./tokens/css/variables-android.css 2025-11-27 18:52:39.040486124 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Nov 27 2025 02:51:26 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Nov 27 2025 18:52:39 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-browser.diff--- ./tokens/css/variables-browser.old.css 2025-11-27 18:53:10.367191028 +0000
+++ ./tokens/css/variables-browser.css 2025-11-27 18:52:39.026486269 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Nov 27 2025 02:51:26 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Nov 27 2025 18:52:39 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-ios.diff--- ./tokens/css/variables-ios.old.css 2025-11-27 18:53:10.614188646 +0000
+++ ./tokens/css/variables-ios.css 2025-11-27 18:52:39.055485969 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Nov 27 2025 02:51:26 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Nov 27 2025 18:52:39 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-marketing.diff--- ./tokens/css/variables-marketing.old.css 2025-11-27 18:53:10.840186464 +0000
+++ ./tokens/css/variables-marketing.css 2025-11-27 18:52:39.076485753 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Nov 27 2025 02:51:26 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Nov 27 2025 18:52:39 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-news.diff--- ./tokens/css/variables-news.old.css 2025-11-27 18:53:11.063184312 +0000
+++ ./tokens/css/variables-news.css 2025-11-27 18:52:39.113485370 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Nov 27 2025 02:51:26 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Nov 27 2025 18:52:39 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-newtab.diff--- ./tokens/css/variables-newtab.old.css 2025-11-27 18:53:11.301182013 +0000
+++ ./tokens/css/variables-newtab.css 2025-11-27 18:52:39.121485288 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Nov 27 2025 02:51:26 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Nov 27 2025 18:52:39 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-search.diff--- ./tokens/css/variables-search.old.css 2025-11-27 18:53:11.522179880 +0000
+++ ./tokens/css/variables-search.css 2025-11-27 18:52:39.099485515 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Nov 27 2025 02:51:26 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Nov 27 2025 18:52:39 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-web3.diff--- ./tokens/css/variables-web3.old.css 2025-11-27 18:53:11.733177844 +0000
+++ ./tokens/css/variables-web3.css 2025-11-27 18:52:39.125485247 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Nov 27 2025 02:51:26 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Nov 27 2025 18:52:39 GMT+0000 (Coordinated Universal Time)
*/
@media (prefers-color-scheme: light) {
Variables Diff: variables.diff--- ./tokens/css/variables.old.css 2025-11-27 18:53:11.953175722 +0000
+++ ./tokens/css/variables.css 2025-11-27 18:52:38.909487477 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Nov 27 2025 02:51:26 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Nov 27 2025 18:52:38 GMT+0000 (Coordinated Universal Time)
*/
:root {
|
fddcfb3 to
585710c
Compare
|
👋 Thanks for Submitting! This PR is available for preview at the link below. ✅ PR tip preview: https://1273.pr.nala.bravesoftware.com/ - ./tokens/css/variables-android.old.css: 7390 bytes
+ ./tokens/css/variables-android.css: 7390 bytes
---
- ./tokens/css/variables-browser.old.css: 6644 bytes
+ ./tokens/css/variables-browser.css: 6644 bytes
---
- ./tokens/css/variables-ios.old.css: 8180 bytes
+ ./tokens/css/variables-ios.css: 8180 bytes
---
- ./tokens/css/variables-marketing.old.css: 13501 bytes
+ ./tokens/css/variables-marketing.css: 13501 bytes
---
- ./tokens/css/variables-news.old.css: 526 bytes
+ ./tokens/css/variables-news.css: 526 bytes
---
- ./tokens/css/variables-newtab.old.css: 1933 bytes
+ ./tokens/css/variables-newtab.css: 1933 bytes
---
- ./tokens/css/variables-search.old.css: 17733 bytes
+ ./tokens/css/variables-search.css: 17733 bytes
---
- ./tokens/css/variables-web3.old.css: 893 bytes
+ ./tokens/css/variables-web3.css: 893 bytes
---
- ./tokens/css/variables.old.css: 125593 bytes
+ ./tokens/css/variables.css: 125593 bytes
Variables Diff: variables-android.diff--- ./tokens/css/variables-android.old.css 2025-12-03 21:34:51.501360533 +0000
+++ ./tokens/css/variables-android.css 2025-12-03 21:34:18.370149444 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Wed Dec 03 2025 21:33:19 GMT+0000 (Coordinated Universal Time)
+ * Generated on Wed Dec 03 2025 21:34:18 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-browser.diff--- ./tokens/css/variables-browser.old.css 2025-12-03 21:34:51.650361421 +0000
+++ ./tokens/css/variables-browser.css 2025-12-03 21:34:18.355149347 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Wed Dec 03 2025 21:33:19 GMT+0000 (Coordinated Universal Time)
+ * Generated on Wed Dec 03 2025 21:34:18 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-ios.diff--- ./tokens/css/variables-ios.old.css 2025-12-03 21:34:51.784362222 +0000
+++ ./tokens/css/variables-ios.css 2025-12-03 21:34:18.386149547 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Wed Dec 03 2025 21:33:19 GMT+0000 (Coordinated Universal Time)
+ * Generated on Wed Dec 03 2025 21:34:18 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-marketing.diff--- ./tokens/css/variables-marketing.old.css 2025-12-03 21:34:51.918363023 +0000
+++ ./tokens/css/variables-marketing.css 2025-12-03 21:34:18.409149695 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Wed Dec 03 2025 21:33:19 GMT+0000 (Coordinated Universal Time)
+ * Generated on Wed Dec 03 2025 21:34:18 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-news.diff--- ./tokens/css/variables-news.old.css 2025-12-03 21:34:52.007363553 +0000
+++ ./tokens/css/variables-news.css 2025-12-03 21:34:18.467150068 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Wed Dec 03 2025 21:33:19 GMT+0000 (Coordinated Universal Time)
+ * Generated on Wed Dec 03 2025 21:34:18 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-newtab.diff--- ./tokens/css/variables-newtab.old.css 2025-12-03 21:34:52.148364395 +0000
+++ ./tokens/css/variables-newtab.css 2025-12-03 21:34:18.476150126 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Wed Dec 03 2025 21:33:19 GMT+0000 (Coordinated Universal Time)
+ * Generated on Wed Dec 03 2025 21:34:18 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-search.diff--- ./tokens/css/variables-search.old.css 2025-12-03 21:34:52.229364878 +0000
+++ ./tokens/css/variables-search.css 2025-12-03 21:34:18.440149894 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Wed Dec 03 2025 21:33:19 GMT+0000 (Coordinated Universal Time)
+ * Generated on Wed Dec 03 2025 21:34:18 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-web3.diff--- ./tokens/css/variables-web3.old.css 2025-12-03 21:34:52.606367129 +0000
+++ ./tokens/css/variables-web3.css 2025-12-03 21:34:18.481150158 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Wed Dec 03 2025 21:33:19 GMT+0000 (Coordinated Universal Time)
+ * Generated on Wed Dec 03 2025 21:34:18 GMT+0000 (Coordinated Universal Time)
*/
@media (prefers-color-scheme: light) {
Variables Diff: variables.diff--- ./tokens/css/variables.old.css 2025-12-03 21:34:52.787368212 +0000
+++ ./tokens/css/variables.css 2025-12-03 21:34:18.224148504 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Wed Dec 03 2025 21:33:19 GMT+0000 (Coordinated Universal Time)
+ * Generated on Wed Dec 03 2025 21:34:18 GMT+0000 (Coordinated Universal Time)
*/
:root {
|
585710c to
32eef6f
Compare
|
[puLL-Merge] - actions/[email protected] Diffdiff --git .github/workflows/test.yml .github/workflows/test.yml
index e62ac3ba6..7c47d7b6a 100644
--- .github/workflows/test.yml
+++ .github/workflows/test.yml
@@ -302,12 +302,15 @@ jobs:
# Clone this repo
- name: Checkout
uses: actions/[email protected]
+ with:
+ path: actions-checkout
# Basic checkout using git
- name: Checkout basic
id: checkout
- uses: ./
+ uses: ./actions-checkout
with:
+ path: cloned-using-local-action
ref: test-data/v2/basic
# Verify output
@@ -325,7 +328,3 @@ jobs:
echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d"
exit 1
fi
-
- # needed to make checkout post cleanup succeed
- - name: Fix Checkout
- uses: actions/[email protected]
diff --git CHANGELOG.md CHANGELOG.md
index ff8b4e7b0..25befb782 100644
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,8 +1,17 @@
# Changelog
+## V6.0.0
+* Persist creds to a separate file by @ericsciple in https://github.com/actions/checkout/pull/2286
+* Update README to include Node.js 24 support details and requirements by @salmanmkc in https://github.com/actions/checkout/pull/2248
+
+## V5.0.1
+* Port v6 cleanup to v5 by @ericsciple in https://github.com/actions/checkout/pull/2301
+
## V5.0.0
* Update actions checkout to use node 24 by @salmanmkc in https://github.com/actions/checkout/pull/2226
+## V4.3.1
+* Port v6 cleanup to v4 by @ericsciple in https://github.com/actions/checkout/pull/2305
## V4.3.0
* docs: update README.md by @motss in https://github.com/actions/checkout/pull/1971
diff --git README.md README.md
index f9175e995..5ad476f49 100644
--- README.md
+++ README.md
@@ -1,10 +1,21 @@
[](https://github.com/actions/checkout/actions/workflows/test.yml)
-# Checkout V5
+# Checkout v6
-Checkout v5 now supports Node.js 24
+## What's new
-# Checkout V4
+- Updated `persist-credentials` to store the credentials under `$RUNNER_TEMP` instead of directly in the local git config.
+ - This requires a minimum Actions Runner version of [v2.329.0](https://github.com/actions/runner/releases/tag/v2.329.0) to access the persisted credentials for [Docker container action](https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action) scenarios.
+
+# Checkout v5
+
+## What's new
+
+- Updated to the node24 runtime
+ - This requires a minimum Actions Runner version of [v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) to run.
+
+
+# Checkout v4
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
@@ -154,9 +165,10 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
# Scenarios
- [Checkout V5](#checkout-v5)
+ - [What's new](#whats-new)
- [Checkout V4](#checkout-v4)
- [Note](#note)
-- [What's new](#whats-new)
+- [What's new](#whats-new-1)
- [Usage](#usage)
- [Scenarios](#scenarios)
- [Fetch only the root files](#fetch-only-the-root-files)
diff --git __test__/git-auth-helper.test.ts __test__/git-auth-helper.test.ts
index 7633704cc..ad3566ad6 100644
--- __test__/git-auth-helper.test.ts
+++ __test__/git-auth-helper.test.ts
@@ -86,16 +86,29 @@ describe('git-auth-helper tests', () => {
// Act
await authHelper.configureAuth()
- // Assert config
- const configContent = (
+ // Assert config - check that .git/config contains includeIf entries
+ const localConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
+ expect(
+ localConfigContent.indexOf('includeIf.gitdir:')
+ ).toBeGreaterThanOrEqual(0)
+
+ // Assert credentials config file contains the actual credentials
+ const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBe(1)
+ const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+ const credentialsContent = (
+ await fs.promises.readFile(credentialsConfigPath)
+ ).toString()
const basicCredential = Buffer.from(
`x-access-token:${settings.authToken}`,
'utf8'
).toString('base64')
expect(
- configContent.indexOf(
+ credentialsContent.indexOf(
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
@@ -120,7 +133,7 @@ describe('git-auth-helper tests', () => {
'inject https://github.com as github server url'
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
await testAuthHeader(
- configureAuth_AcceptsGitHubServerUrl,
+ configureAuth_AcceptsGitHubServerUrlSetToGHEC,
'https://github.com'
)
})
@@ -141,12 +154,17 @@ describe('git-auth-helper tests', () => {
// Act
await authHelper.configureAuth()
- // Assert config
- const configContent = (
- await fs.promises.readFile(localGitConfigPath)
+ // Assert config - check credentials config file (not local .git/config)
+ const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBe(1)
+ const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+ const credentialsContent = (
+ await fs.promises.readFile(credentialsConfigPath)
).toString()
expect(
- configContent.indexOf(
+ credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION`
)
).toBeGreaterThanOrEqual(0)
@@ -251,13 +269,16 @@ describe('git-auth-helper tests', () => {
expectedSshCommand
)
- // Asserty git config
+ // Assert git config
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
.toString()
.split('\n')
.filter(x => x)
- expect(gitConfigLines).toHaveLength(1)
- expect(gitConfigLines[0]).toMatch(/^http\./)
+ // Should have includeIf entries pointing to credentials file
+ expect(gitConfigLines.length).toBeGreaterThan(0)
+ expect(
+ gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0)
+ ).toBeTruthy()
})
const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
@@ -419,8 +440,20 @@ describe('git-auth-helper tests', () => {
expect(
configContent.indexOf('value-from-global-config')
).toBeGreaterThanOrEqual(0)
+ // Global config should have include.path pointing to credentials file
+ expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
+
+ // Check credentials in the separate config file
+ const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBeGreaterThan(0)
+ const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+ const credentialsContent = (
+ await fs.promises.readFile(credentialsConfigPath)
+ ).toString()
expect(
- configContent.indexOf(
+ credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
@@ -463,8 +496,20 @@ describe('git-auth-helper tests', () => {
const configContent = (
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
).toString()
+ // Global config should have include.path pointing to credentials file
+ expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
+
+ // Check credentials in the separate config file
+ const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBeGreaterThan(0)
+ const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+ const credentialsContent = (
+ await fs.promises.readFile(credentialsConfigPath)
+ ).toString()
expect(
- configContent.indexOf(
+ credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
@@ -550,15 +595,15 @@ describe('git-auth-helper tests', () => {
await authHelper.configureSubmoduleAuth()
// Assert
- expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
+ // Should configure insteadOf (2 calls for two values)
+ expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
- expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
- expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
+ expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(
/url.*insteadOf.*[email protected]:/
)
- expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
+ expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
/url.*insteadOf.*[email protected]:/
)
}
@@ -589,12 +634,12 @@ describe('git-auth-helper tests', () => {
await authHelper.configureSubmoduleAuth()
// Assert
- expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
+ // Should configure sshCommand (1 call)
+ expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
- expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
- expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
+ expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/)
}
)
@@ -660,19 +705,201 @@ describe('git-auth-helper tests', () => {
await setup(removeAuth_removesToken)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
- let gitConfigContent = (
+
+ // Verify includeIf entries exist in local config
+ let localConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
- expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
+ expect(
+ localConfigContent.indexOf('includeIf.gitdir:')
+ ).toBeGreaterThanOrEqual(0)
+
+ // Verify both host and container includeIf entries are present
+ const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
+ expect(
+ localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
+ ).toBeGreaterThanOrEqual(0)
+ expect(
+ localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
+ ).toBeGreaterThanOrEqual(0)
+
+ // Verify credentials file exists
+ let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBe(1)
+ const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
+
+ // Verify credentials file contains the auth token
+ let credentialsContent = (
+ await fs.promises.readFile(credentialsFilePath)
+ ).toString()
+ const basicCredential = Buffer.from(
+ `x-access-token:${settings.authToken}`,
+ 'utf8'
+ ).toString('base64')
+ expect(
+ credentialsContent.indexOf(
+ `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
+ )
+ ).toBeGreaterThanOrEqual(0)
+
+ // Verify the includeIf entries point to the credentials file
+ const containerCredentialsPath = path.posix.join(
+ '/github/runner_temp',
+ path.basename(credentialsFilePath)
+ )
+ expect(
+ localConfigContent.indexOf(credentialsFilePath)
+ ).toBeGreaterThanOrEqual(0)
+ expect(
+ localConfigContent.indexOf(containerCredentialsPath)
+ ).toBeGreaterThanOrEqual(0)
// Act
await authHelper.removeAuth()
- // Assert git config
- gitConfigContent = (
+ // Assert all includeIf entries removed from local git config
+ localConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
- expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
+ expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+ expect(
+ localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
+ ).toBeLessThan(0)
+ expect(
+ localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
+ ).toBeLessThan(0)
+ expect(localConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+ expect(localConfigContent.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+ // Assert credentials config file deleted
+ credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBe(0)
+
+ // Verify credentials file no longer exists on disk
+ try {
+ await fs.promises.stat(credentialsFilePath)
+ throw new Error('Credentials file should have been deleted')
+ } catch (err) {
+ if ((err as any)?.code !== 'ENOENT') {
+ throw err
+ }
+ }
+ })
+
+ const removeAuth_removesTokenFromSubmodules =
+ 'removeAuth removes token from submodules'
+ it(removeAuth_removesTokenFromSubmodules, async () => {
+ // Arrange
+ await setup(removeAuth_removesTokenFromSubmodules)
+
+ // Create fake submodule config paths
+ const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
+ const submodule2Dir = path.join(workspace, '.git', 'modules', 'submodule-2')
+ const submodule1ConfigPath = path.join(submodule1Dir, 'config')
+ const submodule2ConfigPath = path.join(submodule2Dir, 'config')
+
+ await fs.promises.mkdir(submodule1Dir, {recursive: true})
+ await fs.promises.mkdir(submodule2Dir, {recursive: true})
+ await fs.promises.writeFile(submodule1ConfigPath, '')
+ await fs.promises.writeFile(submodule2ConfigPath, '')
+
+ // Mock getSubmoduleConfigPaths to return our fake submodules (for both configure and remove)
+ const mockGetSubmoduleConfigPaths =
+ git.getSubmoduleConfigPaths as jest.Mock<any, any>
+ mockGetSubmoduleConfigPaths.mockResolvedValue([
+ submodule1ConfigPath,
+ submodule2ConfigPath
+ ])
+
+ const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+ await authHelper.configureAuth()
+ await authHelper.configureSubmoduleAuth()
+
+ // Verify credentials file exists
+ let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBe(1)
+ const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
+
+ // Verify submodule 1 config has includeIf entries
+ let submodule1Content = (
+ await fs.promises.readFile(submodule1ConfigPath)
+ ).toString()
+ const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
+ expect(
+ submodule1Content.indexOf(`includeIf.gitdir:${submodule1GitDir}.path`)
+ ).toBeGreaterThanOrEqual(0)
+ expect(
+ submodule1Content.indexOf(credentialsFilePath)
+ ).toBeGreaterThanOrEqual(0)
+
+ // Verify submodule 2 config has includeIf entries
+ let submodule2Content = (
+ await fs.promises.readFile(submodule2ConfigPath)
+ ).toString()
+ const submodule2GitDir = submodule2Dir.replace(/\\/g, '/')
+ expect(
+ submodule2Content.indexOf(`includeIf.gitdir:${submodule2GitDir}.path`)
+ ).toBeGreaterThanOrEqual(0)
+ expect(
+ submodule2Content.indexOf(credentialsFilePath)
+ ).toBeGreaterThanOrEqual(0)
+
+ // Verify both host and container paths are in each submodule config
+ const containerCredentialsPath = path.posix.join(
+ '/github/runner_temp',
+ path.basename(credentialsFilePath)
+ )
+ expect(
+ submodule1Content.indexOf(containerCredentialsPath)
+ ).toBeGreaterThanOrEqual(0)
+ expect(
+ submodule2Content.indexOf(containerCredentialsPath)
+ ).toBeGreaterThanOrEqual(0)
+
+ // Act - ensure mock persists for removeAuth
+ mockGetSubmoduleConfigPaths.mockResolvedValue([
+ submodule1ConfigPath,
+ submodule2ConfigPath
+ ])
+ await authHelper.removeAuth()
+
+ // Assert submodule 1 includeIf entries removed
+ submodule1Content = (
+ await fs.promises.readFile(submodule1ConfigPath)
+ ).toString()
+ expect(submodule1Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+ expect(submodule1Content.indexOf(credentialsFilePath)).toBeLessThan(0)
+ expect(submodule1Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+ // Assert submodule 2 includeIf entries removed
+ submodule2Content = (
+ await fs.promises.readFile(submodule2ConfigPath)
+ ).toString()
+ expect(submodule2Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+ expect(submodule2Content.indexOf(credentialsFilePath)).toBeLessThan(0)
+ expect(submodule2Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+ // Assert credentials config file deleted
+ credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+ f => f.startsWith('git-credentials-') && f.endsWith('.config')
+ )
+ expect(credentialsFiles.length).toBe(0)
+
+ // Verify credentials file no longer exists on disk
+ try {
+ await fs.promises.stat(credentialsFilePath)
+ throw new Error('Credentials file should have been deleted')
+ } catch (err) {
+ if ((err as any)?.code !== 'ENOENT') {
+ throw err
+ }
+ }
})
const removeGlobalConfig_removesOverride =
@@ -701,6 +928,52 @@ describe('git-auth-helper tests', () => {
}
}
})
+
+ const testCredentialsConfigPath_matchesCredentialsConfigPaths =
+ 'testCredentialsConfigPath matches credentials config paths'
+ it(testCredentialsConfigPath_matchesCredentialsConfigPaths, async () => {
+ // Arrange
+ await setup(testCredentialsConfigPath_matchesCredentialsConfigPaths)
+ const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+
+ // Get a real credentials config path
+ const credentialsConfigPath = await (
+ authHelper as any
+ ).getCredentialsConfigPath()
+
+ // Act & Assert
+ expect(
+ (authHelper as any).testCredentialsConfigPath(credentialsConfigPath)
+ ).toBe(true)
+ expect(
+ (authHelper as any).testCredentialsConfigPath(
+ '/some/path/git-credentials-12345678-abcd-1234-5678-123456789012.config'
+ )
+ ).toBe(true)
+ expect(
+ (authHelper as any).testCredentialsConfigPath(
+ '/some/path/git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
+ )
+ ).toBe(true)
+
+ // Test invalid paths
+ expect(
+ (authHelper as any).testCredentialsConfigPath(
+ '/some/path/other-config.config'
+ )
+ ).toBe(false)
+ expect(
+ (authHelper as any).testCredentialsConfigPath(
+ '/some/path/git-credentials-invalid.config'
+ )
+ ).toBe(false)
+ expect(
+ (authHelper as any).testCredentialsConfigPath(
+ '/some/path/git-credentials-.config'
+ )
+ ).toBe(false)
+ expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
+ })
})
async function setup(testName: string): Promise<void> {
@@ -715,6 +988,7 @@ async function setup(testName: string): Promise<void> {
await fs.promises.mkdir(tempHomedir, {recursive: true})
process.env['RUNNER_TEMP'] = runnerTemp
process.env['HOME'] = tempHomedir
+ process.env['GITHUB_WORKSPACE'] = workspace
// Create git config
globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
@@ -733,10 +1007,20 @@ async function setup(testName: string): Promise<void> {
checkout: jest.fn(),
checkoutDetach: jest.fn(),
config: jest.fn(
- async (key: string, value: string, globalConfig?: boolean) => {
- const configPath = globalConfig
- ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
- : localGitConfigPath
+ async (
+ key: string,
+ value: string,
+ globalConfig?: boolean,
+ add?: boolean,
+ configFile?: string
+ ) => {
+ const configPath =
+ configFile ||
+ (globalConfig
+ ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+ : localGitConfigPath)
+ // Ensure directory exists
+ await fs.promises.mkdir(path.dirname(configPath), {recursive: true})
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
}
),
@@ -756,6 +1040,7 @@ async function setup(testName: string): Promise<void> {
env: {},
fetch: jest.fn(),
getDefaultBranch: jest.fn(),
+ getSubmoduleConfigPaths: jest.fn(async () => []),
getWorkingDirectory: jest.fn(() => workspace),
init: jest.fn(),
isDetached: jest.fn(),
@@ -794,8 +1079,72 @@ async function setup(testName: string): Promise<void> {
return true
}
),
+ tryConfigUnsetValue: jest.fn(
+ async (
+ key: string,
+ value: string,
+ globalConfig?: boolean,
+ configPath?: string
+ ): Promise<boolean> => {
+ const targetConfigPath =
+ configPath ||
+ (globalConfig
+ ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+ : localGitConfigPath)
+ let content = await fs.promises.readFile(targetConfigPath)
+ let lines = content
+ .toString()
+ .split('\n')
+ .filter(x => x)
+ .filter(x => !(x.startsWith(key) && x.includes(value)))
+ await fs.promises.writeFile(targetConfigPath, lines.join('\n'))
+ return true
+ }
+ ),
tryDisableAutomaticGarbageCollection: jest.fn(),
tryGetFetchUrl: jest.fn(),
+ tryGetConfigValues: jest.fn(
+ async (
+ key: string,
+ globalConfig?: boolean,
+ configPath?: string
+ ): Promise<string[]> => {
+ const targetConfigPath =
+ configPath ||
+ (globalConfig
+ ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+ : localGitConfigPath)
+ const content = await fs.promises.readFile(targetConfigPath)
+ const lines = content
+ .toString()
+ .split('\n')
+ .filter(x => x && x.startsWith(key))
+ .map(x => x.substring(key.length).trim())
+ return lines
+ }
+ ),
+ tryGetConfigKeys: jest.fn(
+ async (
+ pattern: string,
+ globalConfig?: boolean,
+ configPath?: string
+ ): Promise<string[]> => {
+ const targetConfigPath =
+ configPath ||
+ (globalConfig
+ ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+ : localGitConfigPath)
+ const content = await fs.promises.readFile(targetConfigPath)
+ const lines = content
+ .toString()
+ .split('\n')
+ .filter(x => x)
+ const keys = lines
+ .filter(x => new RegExp(pattern).test(x.split(' ')[0]))
+ .map(x => x.split(' ')[0])
+ return [...new Set(keys)] // Remove duplicates
+ }
+ ),
tryReset: jest.fn(),
version: jest.fn()
}
@@ -830,6 +1179,7 @@ async function setup(testName: string): Promise<void> {
async function getActualSshKeyPath(): Promise<string> {
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
+ .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
.sort()
.map(x => path.join(runnerTemp, x))
if (actualTempFiles.length === 0) {
@@ -843,6 +1193,7 @@ async function getActualSshKeyPath(): Promise<string> {
async function getActualSshKnownHostsPath(): Promise<string> {
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
+ .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
.sort()
.map(x => path.join(runnerTemp, x))
if (actualTempFiles.length === 0) {
diff --git __test__/git-directory-helper.test.ts __test__/git-directory-helper.test.ts
index 22e9ae6d4..de79dc890 100644
--- __test__/git-directory-helper.test.ts
+++ __test__/git-directory-helper.test.ts
@@ -471,6 +471,7 @@ async function setup(testName: string): Promise<void> {
configExists: jest.fn(),
fetch: jest.fn(),
getDefaultBranch: jest.fn(),
+ getSubmoduleConfigPaths: jest.fn(async () => []),
getWorkingDirectory: jest.fn(() => repositoryPath),
init: jest.fn(),
isDetached: jest.fn(),
@@ -493,12 +494,15 @@ async function setup(testName: string): Promise<void> {
return true
}),
tryConfigUnset: jest.fn(),
+ tryConfigUnsetValue: jest.fn(),
tryDisableAutomaticGarbageCollection: jest.fn(),
tryGetFetchUrl: jest.fn(async () => {
// Sanity check - this function shouldn't be called when the .git directory doesn't exist
await fs.promises.stat(path.join(repositoryPath, '.git'))
return repositoryUrl
}),
+ tryGetConfigValues: jest.fn(),
+ tryGetConfigKeys: jest.fn(),
tryReset: jest.fn(async () => {
return true
}),
diff --git __test__/verify-submodules-recursive.sh __test__/verify-submodules-recursive.sh
index 1b68f9b97..5ecbb42d0 100755
--- __test__/verify-submodules-recursive.sh
+++ __test__/verify-submodules-recursive.sh
@@ -17,7 +17,7 @@ fi
echo "Testing persisted credential"
pushd ./submodules-recursive/submodule-level-1/submodule-level-2
-git config --local --name-only --get-regexp http.+extraheader && git fetch
+git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
if [ "$?" != "0" ]; then
echo "Failed to validate persisted credential"
popd
diff --git __test__/verify-submodules-true.sh __test__/verify-submodules-true.sh
index 43769fe06..4c311f846 100755
--- __test__/verify-submodules-true.sh
+++ __test__/verify-submodules-true.sh
@@ -17,7 +17,7 @@ fi
echo "Testing persisted credential"
pushd ./submodules-true/submodule-level-1
-git config --local --name-only --get-regexp http.+extraheader && git fetch
+git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
if [ "$?" != "0" ]; then
echo "Failed to validate persisted credential"
popd
diff --git dist/index.js dist/index.js
index f3ae6f3ea..a251a1966 100644
--- dist/index.js
+++ dist/index.js
@@ -162,6 +162,7 @@ class GitAuthHelper {
this.sshKeyPath = '';
this.sshKnownHostsPath = '';
this.temporaryHomePath = '';
+ this.credentialsConfigPath = ''; // Path to separate credentials config file in RUNNER_TEMP
this.git = gitCommandManager;
this.settings = gitSourceSettings || {};
// Token auth header
@@ -229,15 +230,17 @@ class GitAuthHelper {
configureGlobalAuth() {
return __awaiter(this, void 0, void 0, function* () {
// 'configureTempGlobalConfig' noops if already set, just returns the path
- const newGitConfigPath = yield this.configureTempGlobalConfig();
+ yield this.configureTempGlobalConfig();
try {
// Configure the token
- yield this.configureToken(newGitConfigPath, true);
+ yield this.configureToken(true);
// Configure HTTPS instead of SSH
yield this.git.tryConfigUnset(this.insteadOfKey, true);
if (!this.settings.sshKey) {
for (const insteadOfValue of this.insteadOfValues) {
- yield this.git.config(this.insteadOfKey, insteadOfValue, true, true);
+ yield this.git.config(this.insteadOfKey, insteadOfValue, true, // globalConfig?
+ true // add?
+ );
}
}
}
@@ -252,19 +255,34 @@ class GitAuthHelper {
configureSubmoduleAuth() {
return __awaiter(this, void 0, void 0, function* () {
// Remove possible previous HTTPS instead of SSH
- yield this.removeGitConfig(this.insteadOfKey, true);
+ yield this.removeSubmoduleGitConfig(this.insteadOfKey);
if (this.settings.persistCredentials) {
- // Configure a placeholder value. This approach avoids the credential being captured
- // by process creation audit events, which are commonly logged. For more information,
- // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
- const output = yield this.git.submoduleForeach(
- // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
- `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules);
- // Replace the placeholder
- const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
+ // Get the credentials config file path in RUNNER_TEMP
+ const credentialsConfigPath = this.getCredentialsConfigPath();
+ // Container credentials config path
+ const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
+ // Get submodule config file paths.
+ const configPaths = yield this.git.getSubmoduleConfigPaths(this.settings.nestedSubmodules);
+ // For each submodule, configure includeIf entries pointing to the shared credentials file.
+ // Configure both host and container paths to support Docker container actions.
for (const configPath of configPaths) {
- core.debug(`Replacing token placeholder in '${configPath}'`);
- yield this.replaceTokenPlaceholder(configPath);
+ // Submodule Git directory
+ let submoduleGitDir = path.dirname(configPath); // The config file is at .git/modules/submodule-name/config
+ submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+ // Configure host includeIf
+ yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig?
+ false, // add?
+ configPath);
+ // Container submodule git directory
+ const githubWorkspace = process.env['GITHUB_WORKSPACE'];
+ assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined');
+ let relativeSubmoduleGitDir = path.relative(githubWorkspace, submoduleGitDir);
+ relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+ const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir);
+ // Configure container includeIf
+ yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig?
+ false, // add?
+ configPath);
}
if (this.settings.sshKey) {
// Configure core.sshCommand
@@ -295,6 +313,10 @@ class GitAuthHelper {
}
});
}
+ /**
+ * Configures SSH authentication by writing the SSH key and known hosts,
+ * and setting up the GIT_SSH_COMMAND environment variable.
+ */
configureSsh() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.settings.sshKey) {
@@ -351,43 +373,88 @@ class GitAuthHelper {
}
});
}
- configureToken(configPath, globalConfig) {
- return __awaiter(this, void 0, void 0, function* () {
- // Validate args
- assert.ok((configPath && globalConfig) || (!configPath && !globalConfig), 'Unexpected configureToken parameter combinations');
- // Default config path
- if (!configPath && !globalConfig) {
- configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config');
- }
- // Configure a placeholder value. This approach avoids the credential being captured
- // by process creation audit events, which are commonly logged. For more information,
- // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
- yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig);
- // Replace the placeholder
- yield this.replaceTokenPlaceholder(configPath || '');
- });
- }
- replaceTokenPlaceholder(configPath) {
+ /**
+ * Configures token-based authentication by creating a credentials config file
+ * and setting up includeIf entries to reference it.
+ * @param globalConfig Whether to configure global config instead of local
+ */
+ configureToken(globalConfig) {
return __awaiter(this, void 0, void 0, function* () {
- assert.ok(configPath, 'configPath is not defined');
- let content = (yield fs.promises.readFile(configPath)).toString();
+ // Get the credentials config file path in RUNNER_TEMP
+ const credentialsConfigPath = this.getCredentialsConfigPath();
+ // Write placeholder to the separate credentials config file using git config.
+ // This approach avoids the credential being captured by process creation audit events,
+ // which are commonly logged. For more information, refer to
+ // https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
+ yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, false, // globalConfig?
+ false, // add?
+ credentialsConfigPath);
+ // Replace the placeholder in the credentials config file
+ let content = (yield fs.promises.readFile(credentialsConfigPath)).toString();
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue);
if (placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) {
- throw new Error(`Unable to replace auth placeholder in ${configPath}`);
+ throw new Error(`Unable to replace auth placeholder in ${credentialsConfigPath}`);
}
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined');
content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue);
- yield fs.promises.writeFile(configPath, content);
+ yield fs.promises.writeFile(credentialsConfigPath, content);
+ // Add include or includeIf to reference the credentials config
+ if (globalConfig) {
+ // Global config file is temporary
+ yield this.git.config('include.path', credentialsConfigPath, true // globalConfig?
+ );
+ }
+ else {
+ // Host git directory
+ let gitDir = path.join(this.git.getWorkingDirectory(), '.git');
+ gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+ // Configure host includeIf
+ const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`;
+ yield this.git.config(hostIncludeKey, credentialsConfigPath);
+ // Container git directory
+ const workingDirectory = this.git.getWorkingDirectory();
+ const githubWorkspace = process.env['GITHUB_WORKSPACE'];
+ assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined');
+ let relativePath = path.relative(githubWorkspace, workingDirectory);
+ relativePath = relativePath.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+ const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git');
+ // Container credentials config path
+ const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
+ // Configure container includeIf
+ const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`;
+ yield this.git.config(containerIncludeKey, containerCredentialsPath);
+ }
});
}
+ /**
+ * Gets or creates the path to the credentials config file in RUNNER_TEMP.
+ * @returns The absolute path to the credentials config file
+ */
+ getCredentialsConfigPath() {
+ if (this.credentialsConfigPath) {
+ return this.credentialsConfigPath;
+ }
+ const runnerTemp = process.env['RUNNER_TEMP'] || '';
+ assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
+ // Create a unique filename for this checkout instance
+ const configFileName = `git-credentials-${(0, uuid_1.v4)()}.config`;
+ this.credentialsConfigPath = path.join(runnerTemp, configFileName);
+ core.debug(`Credentials config path: ${this.credentialsConfigPath}`);
+ return this.credentialsConfigPath;
+ }
+ /**
+ * Removes SSH authentication configuration by cleaning up SSH keys,
+ * known hosts files, and SSH command configurations.
+ */
removeSsh() {
return __awaiter(this, void 0, void 0, function* () {
- var _a;
+ var _a, _b;
// SSH key
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath;
if (keyPath) {
try {
+ core.info(`Removing SSH key '${keyPath}'`);
yield io.rmRF(keyPath);
}
catch (err) {
@@ -399,37 +466,136 @@ class GitAuthHelper {
const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath;
if (knownHostsPath) {
try {
+ core.info(`Removing SSH known hosts '${knownHostsPath}'`);
yield io.rmRF(knownHostsPath);
}
- catch (_b) {
- // Intentionally empty
+ catch (err) {
+ core.debug(`${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
+ core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`);
}
}
// SSH command
+ core.info('Removing SSH command configuration');
yield this.removeGitConfig(SSH_COMMAND_KEY);
+ yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY);
});
}
+ /**
+ * Removes token-based authentication by cleaning up HTTP headers,
+ * includeIf entries, and credentials config files.
+ */
removeToken() {
return __awaiter(this, void 0, void 0, function* () {
- // HTTP extra header
+ var _a;
+ // Remove HTTP extra header
+ core.info('Removing HTTP extra header');
yield this.removeGitConfig(this.tokenConfigKey);
+ yield this.removeSubmoduleGitConfig(this.tokenConfigKey);
+ // Collect credentials config paths that need to be removed
+ const credentialsPaths = new Set();
+ // Remove includeIf entries that point to git-credentials-*.config files
+ core.info('Removing includeIf entries pointing to credentials config files');
+ const mainCredentialsPaths = yield this.removeIncludeIfCredentials();
+ mainCredentialsPaths.forEach(path => credentialsPaths.add(path));
+ // Remove submodule includeIf entries that point to git-credentials-*.config files
+ const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true);
+ for (const configPath of submoduleConfigPaths) {
+ const submoduleCredentialsPaths = yield this.removeIncludeIfCredentials(configPath);
+ submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path));
+ }
+ // Remove credentials config files
+ for (const credentialsPath of credentialsPaths) {
+ // Only remove credentials config files if they are under RUNNER_TEMP
+ const runnerTemp = process.env['RUNNER_TEMP'];
+ assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
+ if (credentialsPath.startsWith(runnerTemp)) {
+ try {
+ core.info(`Removing credentials config '${credentialsPath}'`);
+ yield io.rmRF(credentialsPath);
+ }
+ catch (err) {
+ core.debug(`${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
+ core.warning(`Failed to remove credentials config '${credentialsPath}'`);
+ }
+ }
+ else {
+ core.debug(`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`);
+ }
+ }
});
}
- removeGitConfig(configKey_1) {
- return __awaiter(this, arguments, void 0, function* (configKey, submoduleOnly = false) {
- if (!submoduleOnly) {
- if ((yield this.git.configExists(configKey)) &&
- !(yield this.git.tryConfigUnset(configKey))) {
- // Load the config contents
- core.warning(`Failed to remove '${configKey}' from the git config`);
- }
+ /**
+ * Removes a git config key from the local repository config.
+ * @param configKey The git config key to remove
+ */
+ removeGitConfig(configKey) {
+ return __awaiter(this, void 0, void 0, function* () {
+ if ((yield this.git.configExists(configKey)) &&
+ !(yield this.git.tryConfigUnset(configKey))) {
+ // Load the config contents
+ core.warning(`Failed to remove '${configKey}' from the git config`);
}
+ });
+ }
+ /**
+ * Removes a git config key from all submodule configs.
+ * @param configKey The git config key to remove
+ */
+ removeSubmoduleGitConfig(configKey) {
+ return __awaiter(this, void 0, void 0, function* () {
const pattern = regexpHelper.escape(configKey);
yield this.git.submoduleForeach(
- // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
+ // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true);
});
}
+ /**
+ * Removes includeIf entries that point to git-credentials-*.config files.
+ * @param configPath Optional path to a specific git config file to operate on
+ * @returns Array of unique credentials config file paths that were found and removed
+ */
+ removeIncludeIfCredentials(configPath) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const credentialsPaths = new Set();
+ try {
+ // Get all includeIf.gitdir keys
+ const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig?
+ configPath);
+ for (const key of keys) {
+ // Get all values for this key
+ const values = yield this.git.tryGetConfigValues(key, false, // globalConfig?
+ configPath);
+ if (values.length > 0) {
+ // Remove only values that match git-credentials-<uuid>.config pattern
+ for (const value of values) {
+ if (this.testCredentialsConfigPath(value)) {
+ credentialsPaths.add(value);
+ yield this.git.tryConfigUnsetValue(key, value, false, configPath);
+ }
+ }
+ }
+ }
+ }
+ catch (err) {
+ // Ignore errors - this is cleanup code
+ if (configPath) {
+ core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`);
+ }
+ else {
+ core.debug(`Error during includeIf cleanup: ${err}`);
+ }
+ }
+ return Array.from(credentialsPaths);
+ });
+ }
+ /**
+ * Tests if a path matches the git-credentials-*.config pattern.
+ * @param path The path to test
+ * @returns True if the path matches the credentials config pattern
+ */
+ testCredentialsConfigPath(path) {
+ return /git-credentials-[0-9a-f-]+\.config$/i.test(path);
+ }
}
@@ -627,9 +793,15 @@ class GitCommandManager {
yield this.execGit(args);
});
}
- config(configKey, configValue, globalConfig, add) {
+ config(configKey, configValue, globalConfig, add, configFile) {
return __awaiter(this, void 0, void 0, function* () {
- const args = ['config', globalConfig ? '--global' : '--local'];
+ const args = ['config'];
+ if (configFile) {
+ args.push('--file', configFile);
+ }
+ else {
+ args.push(globalConfig ? '--global' : '--local');
+ }
if (add) {
args.push('--add');
}
@@ -706,6 +878,16 @@ class GitCommandManager {
throw new Error('Unexpected output when retrieving default branch');
});
}
+ getSubmoduleConfigPaths(recursive) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Get submodule config file paths.
+ // Use `--show-origin` to get the config file path for each submodule.
+ const output = yield this.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, recursive);
+ // Extract config file paths from the output (lines starting with "file:").
+ const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
+ return configPaths;
+ });
+ }
getWorkingDirectory() {
return this.workingDirectory;
}
@@ -836,6 +1018,20 @@ class GitCommandManager {
return output.exitCode === 0;
});
}
+ tryConfigUnsetValue(configKey, configValue, globalConfig, configFile) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const args = ['config'];
+ if (configFile) {
+ args.push('--file', configFile);
+ }
+ else {
+ args.push(globalConfig ? '--global' : '--local');
+ }
+ args.push('--unset', configKey, configValue);
+ const output = yield this.execGit(args, true);
+ return output.exitCode === 0;
+ });
+ }
tryDisableAutomaticGarbageCollection() {
return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true);
@@ -855,6 +1051,46 @@ class GitCommandManager {
return stdout;
});
}
+ tryGetConfigValues(configKey, globalConfig, configFile) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const args = ['config'];
+ if (configFile) {
+ args.push('--file', configFile);
+ }
+ else {
+ args.push(globalConfig ? '--global' : '--local');
+ }
+ args.push('--get-all', configKey);
+ const output = yield this.execGit(args, true);
+ if (output.exitCode !== 0) {
+ return [];
+ }
+ return output.stdout
+ .trim()
+ .split('\n')
+ .filter(value => value.trim());
+ });
+ }
+ tryGetConfigKeys(pattern, globalConfig, configFile) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const args = ['config'];
+ if (configFile) {
+ args.push('--file', configFile);
+ }
+ else {
+ args.push(globalConfig ? '--global' : '--local');
+ }
+ args.push('--name-only', '--get-regexp', pattern);
+ const output = yield this.execGit(args, true);
+ if (output.exitCode !== 0) {
+ return [];
+ }
+ return output.stdout
+ .trim()
+ .split('\n')
+ .filter(key => key.trim());
+ });
+ }
tryReset() {
return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['reset', '--hard', 'HEAD'], true);
diff --git src/git-auth-helper.ts src/git-auth-helper.ts
index 126e8e5ee..a1950a60c 100644
--- src/git-auth-helper.ts
+++ src/git-auth-helper.ts
@@ -43,6 +43,7 @@ class GitAuthHelper {
private sshKeyPath = ''
private sshKnownHostsPath = ''
private temporaryHomePath = ''
+ private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP
constructor(
gitCommandManager: IGitCommandManager,
@@ -126,16 +127,21 @@ class GitAuthHelper {
async configureGlobalAuth(): Promise<void> {
// 'configureTempGlobalConfig' noops if already set, just returns the path
- const newGitConfigPath = await this.configureTempGlobalConfig()
+ await this.configureTempGlobalConfig()
try {
// Configure the token
- await this.configureToken(newGitConfigPath, true)
+ await this.configureToken(true)
// Configure HTTPS instead of SSH
await this.git.tryConfigUnset(this.insteadOfKey, true)
if (!this.settings.sshKey) {
for (const insteadOfValue of this.insteadOfValues) {
- await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
+ await this.git.config(
+ this.insteadOfKey,
+ insteadOfValue,
+ true, // globalConfig?
+ true // add?
+ )
}
}
} catch (err) {
@@ -150,24 +156,60 @@ class GitAuthHelper {
async configureSubmoduleAuth(): Promise<void> {
// Remove possible previous HTTPS instead of SSH
- await this.removeGitConfig(this.insteadOfKey, true)
+ await this.removeSubmoduleGitConfig(this.insteadOfKey)
if (this.settings.persistCredentials) {
- // Configure a placeholder value. This approach avoids the credential being captured
- // by process creation audit events, which are commonly logged. For more information,
- // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
- const output = await this.git.submoduleForeach(
- // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
- `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
+ // Get the credentials config file path in RUNNER_TEMP
+ const credentialsConfigPath = this.getCredentialsConfigPath()
+
+ // Container credentials config path
+ const containerCredentialsPath = path.posix.join(
+ '/github/runner_temp',
+ path.basename(credentialsConfigPath)
+ )
+
+ // Get submodule config file paths.
+ const configPaths = await this.git.getSubmoduleConfigPaths(
this.settings.nestedSubmodules
)
- // Replace the placeholder
- const configPaths: string[] =
- output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
+ // For each submodule, configure includeIf entries pointing to the shared credentials file.
+ // Configure both host and container paths to support Docker container actions.
for (const configPath of configPaths) {
- core.debug(`Replacing token placeholder in '${configPath}'`)
- await this.replaceTokenPlaceholder(configPath)
+ // Submodule Git directory
+ let submoduleGitDir = path.dirname(configPath) // The config file is at .git/modules/submodule-name/config
+ submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
+
+ // Configure host includeIf
+ await this.git.config(
+ `includeIf.gitdir:${submoduleGitDir}.path`,
+ credentialsConfigPath,
+ false, // globalConfig?
+ false, // add?
+ configPath
+ )
+
+ // Container submodule git directory
+ const githubWorkspace = process.env['GITHUB_WORKSPACE']
+ assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
+ let relativeSubmoduleGitDir = path.relative(
+ githubWorkspace,
+ submoduleGitDir
+ )
+ relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
+ const containerSubmoduleGitDir = path.posix.join(
+ '/github/workspace',
+ relativeSubmoduleGitDir
+ )
+
+ // Configure container includeIf
+ await this.git.config(
+ `includeIf.gitdir:${containerSubmoduleGitDir}.path`,
+ containerCredentialsPath,
+ false, // globalConfig?
+ false, // add?
+ configPath
+ )
}
if (this.settings.sshKey) {
@@ -201,6 +243,10 @@ class GitAuthHelper {
}
}
+ /**
+ * Configures SSH authentication by writing the SSH key and known hosts,
+ * and setting up the GIT_SSH_COMMAND environment variable.
+ */
private async configureSsh(): Promise<void> {
if (!this.settings.sshKey) {
return
@@ -272,57 +318,116 @@ class GitAuthHelper {
}
}
- private async configureToken(
- configPath?: string,
- globalConfig?: boolean
- ): Promise<void> {
- // Validate args
- assert.ok(
- (configPath && globalConfig) || (!configPath && !globalConfig),
- 'Unexpected configureToken parameter combinations'
- )
-
- // Default config path
- if (!configPath && !globalConfig) {
- configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
- }
-
- // Configure a placeholder value. This approach avoids the credential being captured
- // by process creation audit events, which are commonly logged. For more information,
- // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
+ /**
+ * Configures token-based authentication by creating a credentials config file
+ * and setting up includeIf entries to reference it.
+ * @param globalConfig Whether to configure global config instead of local
+ */
+ private async configureToken(globalConfig?: boolean): Promise<void> {
+ // Get the credentials config file path in RUNNER_TEMP
+ const credentialsConfigPath = this.getCredentialsConfigPath()
+
+ // Write placeholder to the separate credentials config file using git config.
+ // This approach avoids the credential being captured by process creation audit events,
+ // which are commonly logged. For more information, refer to
+ // https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
await this.git.config(
this.tokenConfigKey,
this.tokenPlaceholderConfigValue,
- globalConfig
+ false, // globalConfig?
+ false, // add?
+ credentialsConfigPath
)
- // Replace the placeholder
- await this.replaceTokenPlaceholder(configPath || '')
- }
-
- private async replaceTokenPlaceholder(configPath: string): Promise<void> {
- assert.ok(configPath, 'configPath is not defined')
- let content = (await fs.promises.readFile(configPath)).toString()
+ // Replace the placeholder in the credentials config file
+ let content = (await fs.promises.readFile(credentialsConfigPath)).toString()
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
if (
placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
) {
- throw new Error(`Unable to replace auth placeholder in ${configPath}`)
+ throw new Error(
+ `Unable to replace auth placeholder in ${credentialsConfigPath}`
+ )
}
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
content = content.replace(
this.tokenPlaceholderConfigValue,
this.tokenConfigValue
)
- await fs.promises.writeFile(configPath, content)
+ await fs.promises.writeFile(credentialsConfigPath, content)
+
+ // Add include or includeIf to reference the credentials config
+ if (globalConfig) {
+ // Global config file is temporary
+ await this.git.config(
+ 'include.path',
+ credentialsConfigPath,
+ true // globalConfig?
+ )
+ } else {
+ // Host git directory
+ let gitDir = path.join(this.git.getWorkingDirectory(), '.git')
+ gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
+
+ // Configure host includeIf
+ const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
+ await this.git.config(hostIncludeKey, credentialsConfigPath)
+
+ // Container git directory
+ const workingDirectory = this.git.getWorkingDirectory()
+ const githubWorkspace = process.env['GITHUB_WORKSPACE']
+ assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
+ let relativePath = path.relative(githubWorkspace, workingDirectory)
+ relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows
+ const containerGitDir = path.posix.join(
+ '/github/workspace',
+ relativePath,
+ '.git'
+ )
+
+ // Container credentials config path
+ const containerCredentialsPath = path.posix.join(
+ '/github/runner_temp',
+ path.basename(credentialsConfigPath)
+ )
+
+ // Configure container includeIf
+ const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
+ await this.git.config(containerIncludeKey, containerCredentialsPath)
+ }
+ }
+
+ /**
+ * Gets or creates the path to the credentials config file in RUNNER_TEMP.
+ * @returns The absolute path to the credentials config file
+ */
+ private getCredentialsConfigPath(): string {
+ if (this.credentialsConfigPath) {
+ return this.credentialsConfigPath
+ }
+
+ const runnerTemp = process.env['RUNNER_TEMP'] || ''
+ assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
+
+ // Create a unique filename for this checkout instance
+ const configFileName = `git-credentials-${uuid()}.config`
+ this.credentialsConfigPath = path.join(runnerTemp, configFileName)
+
+ core.debug(`Credentials config path: ${this.credentialsConfigPath}`)
+ return this.credentialsConfigPath
}
+ /**
+ * Removes SSH authentication configuration by cleaning up SSH keys,
+ * known hosts files, and SSH command configurations.
+ */
private async removeSsh(): Promise<void> {
// SSH key
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
if (keyPath) {
try {
+ core.info(`Removing SSH key '${keyPath}'`)
await io.rmRF(keyPath)
} catch (err) {
core.debug(`${(err as any)?.message ?? err}`)
@@ -335,40 +440,149 @@ class GitAuthHelper {
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
if (knownHostsPath) {
try {
+ core.info(`Removing SSH known hosts '${knownHostsPath}'`)
await io.rmRF(knownHostsPath)
- } catch {
- // Intentionally empty
+ } catch (err) {
+ core.debug(`${(err as any)?.message ?? err}`)
+ core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`)
}
}
// SSH command
+ core.info('Removing SSH command configuration')
await this.removeGitConfig(SSH_COMMAND_KEY)
+ await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY)
}
+ /**
+ * Removes token-based authentication by cleaning up HTTP headers,
+ * includeIf entries, and credentials config files.
+ */
private async removeToken(): Promise<void> {
- // HTTP extra header
+ // Remove HTTP extra header
+ core.info('Removing HTTP extra header')
await this.removeGitConfig(this.tokenConfigKey)
- }
+ await this.removeSubmoduleGitConfig(this.tokenConfigKey)
+
+ // Collect credentials config paths that need to be removed
+ const credentialsPaths = new Set<string>()
+
+ // Remove includeIf entries that point to git-credentials-*.config files
+ core.info('Removing includeIf entries pointing to credentials config files')
+ const mainCredentialsPaths = await this.removeIncludeIfCredentials()
+ mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
+
+ // Remove submodule includeIf entries that point to git-credentials-*.config files
+ const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true)
+ for (const configPath of submoduleConfigPaths) {
+ const submoduleCredentialsPaths =
+ await this.removeIncludeIfCredentials(configPath)
+ submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
+ }
- private async removeGitConfig(
- configKey: string,
- submoduleOnly: boolean = false
- ): Promise<void> {
- if (!submoduleOnly) {
- if (
- (await this.git.configExists(configKey)) &&
- !(await this.git.tryConfigUnset(configKey))
- ) {
- // Load the config contents
- core.warning(`Failed to remove '${configKey}' from the git config`)
+ // Remove credentials config files
+ for (const credentialsPath of credentialsPaths) {
+ // Only remove credentials config files if they are under RUNNER_TEMP
+ const runnerTemp = process.env['RUNNER_TEMP']
+ assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
+ if (credentialsPath.startsWith(runnerTemp)) {
+ try {
+ core.info(`Removing credentials config '${credentialsPath}'`)
+ await io.rmRF(credentialsPath)
+ } catch (err) {
+ core.debug(`${(err as any)?.message ?? err}`)
+ core.warning(
+ `Failed to remove credentials config '${credentialsPath}'`
+ )
+ }
+ } else {
+ core.debug(
+ `Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`
+ )
}
}
+ }
+
+ /**
+ * Removes a git config key from the local repository config.
+ * @param configKey The git config key to remove
+ */
+ private async removeGitConfig(configKey: string): Promise<void> {
+ if (
+ (await this.git.configExists(configKey)) &&
+ !(await this.git.tryConfigUnset(configKey))
+ ) {
+ // Load the config contents
+ core.warning(`Failed to remove '${configKey}' from the git config`)
+ }
+ }
+ /**
+ * Removes a git config key from all submodule configs.
+ * @param configKey The git config key to remove
+ */
+ private async removeSubmoduleGitConfig(configKey: string): Promise<void> {
const pattern = regexpHelper.escape(configKey)
await this.git.submoduleForeach(
- // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
+ // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
true
)
}
+
+ /**
+ * Removes includeIf entries t,hat point to git-credentials-*.config files.
+ * @param configPath Optional path to a specific git config file to operate on
+ * @returns Array of unique credentials config file paths that were found and removed
+ */
+ private async removeIncludeIfCredentials(
+ configPath?: string
+ ): Promise<string[]> {
+ const credentialsPaths = new Set<string>()
+
+ try {
+ // Get all includeIf.gitdir keys
+ const keys = await this.git.tryGetConfigKeys(
+ '^includeIf\\.gitdir:',
+ false, // globalConfig?
+ configPath
+ )
+
+ for (const key of keys) {
+ // Get all values for this key
+ const values = await this.git.tryGetConfigValues(
+ key,
+ false, // globalConfig?
+ configPath
+ )
+ if (values.length > 0) {
+ // Remove only values that match git-credentials-<uuid>.config pattern
+ for (const value of values) {
+ if (this.testCredentialsConfigPath(value)) {
+ credentialsPaths.add(value)
+ await this.git.tryConfigUnsetValue(key, value, false, configPath)
+ }
+ }
+ }
+ }
+ } catch (err) {
+ // Ignore errors - this is cleanup code
+ if (configPath) {
+ core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`)
+ } else {
+ core.debug(`Error during includeIf cleanup: ${err}`)
+ }
+ }
+
+ return Array.from(credentialsPaths)
+ }
+
+ /**
+ * Tests if a path matches the git-credentials-*.config pattern.
+ * @param path The path to test
+ * @returns True if the path matches the credentials config pattern
+ */
+ private testCredentialsConfigPath(path: string): boolean {
+ return /git-credentials-[0-9a-f-]+\.config$/i.test(path)
+ }
}
diff --git src/git-command-manager.ts src/git-command-manager.ts
index 8e42a387f..a45e15a86 100644
--- src/git-command-manager.ts
+++ src/git-command-manager.ts
@@ -28,7 +28,8 @@ export interface IGitCommandManager {
configKey: string,
configValue: string,
globalConfig?: boolean,
- add?: boolean
+ add?: boolean,
+ configFile?: string
): Promise<void>
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
fetch(
@@ -41,6 +42,7 @@ export interface IGitCommandManager {
}
): Promise<void>
getDefaultBranch(repositoryUrl: string): Promise<string>
+ getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
getWorkingDirectory(): string
init(): Promise<void>
isDetached(): Promise<boolean>
@@ -59,8 +61,24 @@ export interface IGitCommandManager {
tagExists(pattern: string): Promise<boolean>
tryClean(): Promise<boolean>
tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
+ tryConfigUnsetValue(
+ configKey: string,
+ configValue: string,
+ globalConfig?: boolean,
+ configFile?: string
+ ): Promise<boolean>
tryDisableAutomaticGarbageCollection(): Promise<boolean>
tryGetFetchUrl(): Promise<string>
+ tryGetConfigValues(
+ configKey: string,
+ globalConfig?: boolean,
+ configFile?: string
+ ): Promise<string[]>
+ tryGetConfigKeys(
+ pattern: string,
+ globalConfig?: boolean,
+ configFile?: string
+ ): Promise<string[]>
tryReset(): Promise<boolean>
version(): Promise<GitVersion>
}
@@ -223,9 +241,15 @@ class GitCommandManager {
configKey: string,
configValue: string,
globalConfig?: boolean,
- add?: boolean
+ add?: boolean,
+ configFile?: string
): Promise<void> {
- const args: string[] = ['config', globalConfig ? '--global' : '--local']
+ const args: string[] = ['config']
+ if (configFile) {
+ args.push('--file', configFile)
+ } else {
+ args.push(globalConfig ? '--global' : '--local')
+ }
if (add) {
args.push('--add')
}
@@ -323,6 +347,21 @@ class GitCommandManager {
throw new Error('Unexpected output when retrieving default branch')
}
+ async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> {
+ // Get submodule config file paths.
+ // Use `--show-origin` to get the config file path for each submodule.
+ const output = await this.submoduleForeach(
+ `git config --local --show-origin --name-only --get-regexp remote.origin.url`,
+ recursive
+ )
+
+ // Extract config file paths from the output (lines starting with "file:").
+ const configPaths =
+ output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
+
+ return configPaths
+ }
+
getWorkingDirectory(): string {
return this.workingDirectory
}
@@ -455,6 +494,24 @@ class GitCommandManager {
return output.exitCode === 0
}
+ async tryConfigUnsetValue(
+ configKey: string,
+ configValue: string,
+ globalConfig?: boolean,
+ configFile?: string
+ ): Promise<boolean> {
+ const args = ['config']
+ if (configFile) {
+ args.push('--file', configFile)
+ } else {
+ args.push(globalConfig ? '--global' : '--local')
+ }
+ args.push('--unset', configKey, configValue)
+
+ const output = await this.execGit(args, true)
+ return output.exitCode === 0
+ }
+
async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
const output = await this.execGit(
['config', '--local', 'gc.auto', '0'],
@@ -481,6 +538,56 @@ class GitCommandManager {
return stdout
}
+ async tryGetConfigValues(
+ configKey: string,
+ globalConfig?: boolean,
+ configFile?: string
+ ): Promise<string[]> {
+ const args = ['config']
+ if (configFile) {
+ args.push('--file', configFile)
+ } else {
+ args.push(globalConfig ? '--global' : '--local')
+ }
+ args.push('--get-all', configKey)
+
+ const output = await this.execGit(args, true)
+
+ if (output.exitCode !== 0) {
+ return []
+ }
+
+ return output.stdout
+ .trim()
+ .split('\n')
+ .filter(value => value.trim())
+ }
+
+ async tryGetConfigKeys(
+ pattern: string,
+ globalConfig?: boolean,
+ configFile?: string
+ ): Promise<string[]> {
+ const args = ['config']
+ if (configFile) {
+ args.push('--file', configFile)
+ } else {
+ args.push(globalConfig ? '--global' : '--local')
+ }
+ args.push('--name-only', '--get-regexp', pattern)
+
+ const output = await this.execGit(args, true)
+
+ if (output.exitCode !== 0) {
+ return []
+ }
+
+ return output.stdout
+ .trim()
+ .split('\n')
+ .filter(key => key.trim())
+ }
+
async tryReset(): Promise<boolean> {
const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
return output.exitCode === 0
DescriptionThis PR implements a major security enhancement for the Possible Issues
Security Hotspots
Privacy Hotspots
ChangesChanges
sequenceDiagram
participant Workflow
participant GitAuthHelper
participant GitCommandManager
participant FileSystem
participant GitConfig
Workflow->>GitAuthHelper: configureAuth()
GitAuthHelper->>GitAuthHelper: getCredentialsConfigPath()
GitAuthHelper->>FileSystem: Generate UUID filename
FileSystem-->>GitAuthHelper: git-credentials-<uuid>.config path
GitAuthHelper->>GitCommandManager: config(tokenKey, placeholder, configFile)
GitCommandManager->>FileSystem: Write placeholder to credentials file
GitAuthHelper->>FileSystem: Read credentials file
GitAuthHelper->>FileSystem: Replace placeholder with actual token
GitAuthHelper->>GitCommandManager: config(includeIf.gitdir:<host-path>.path, credentials-path)
GitCommandManager->>GitConfig: Add includeIf for host
GitAuthHelper->>GitCommandManager: config(includeIf.gitdir:<container-path>.path, credentials-path)
GitCommandManager->>GitConfig: Add includeIf for container
Note over GitAuthHelper,GitConfig: For submodules
GitAuthHelper->>GitCommandManager: getSubmoduleConfigPaths()
GitCommandManager-->>GitAuthHelper: List of submodule configs
loop For each submodule
GitAuthHelper->>GitCommandManager: config(includeIf..., credentials-path, submodule-config)
GitCommandManager->>FileSystem: Add includeIf to submodule config
end
Note over Workflow,GitConfig: Cleanup phase
Workflow->>GitAuthHelper: removeAuth()
GitAuthHelper->>GitAuthHelper: removeIncludeIfCredentials()
GitAuthHelper->>GitCommandManager: tryGetConfigKeys(includeIf.gitdir)
GitCommandManager-->>GitAuthHelper: List of includeIf keys
loop For each includeIf key
GitAuthHelper->>GitCommandManager: tryGetConfigValues(key)
GitCommandManager-->>GitAuthHelper: Credentials file paths
GitAuthHelper->>GitCommandManager: tryConfigUnsetValue(key, value)
end
loop For each credentials file
GitAuthHelper->>FileSystem: Delete credentials file
end
|
|
👋 Thanks for Submitting! This PR is available for preview at the link below. ✅ PR tip preview: https://1273.pr.nala.bravesoftware.com/ - ./tokens/css/variables-android.old.css: 7390 bytes
+ ./tokens/css/variables-android.css: 7390 bytes
---
- ./tokens/css/variables-browser.old.css: 6644 bytes
+ ./tokens/css/variables-browser.css: 6644 bytes
---
- ./tokens/css/variables-ios.old.css: 8180 bytes
+ ./tokens/css/variables-ios.css: 8180 bytes
---
- ./tokens/css/variables-marketing.old.css: 13501 bytes
+ ./tokens/css/variables-marketing.css: 13501 bytes
---
- ./tokens/css/variables-news.old.css: 526 bytes
+ ./tokens/css/variables-news.css: 526 bytes
---
- ./tokens/css/variables-newtab.old.css: 1933 bytes
+ ./tokens/css/variables-newtab.css: 1933 bytes
---
- ./tokens/css/variables-search.old.css: 17733 bytes
+ ./tokens/css/variables-search.css: 17733 bytes
---
- ./tokens/css/variables-web3.old.css: 893 bytes
+ ./tokens/css/variables-web3.css: 893 bytes
---
- ./tokens/css/variables.old.css: 125593 bytes
+ ./tokens/css/variables.css: 125593 bytes
Variables Diff: variables-android.diff--- ./tokens/css/variables-android.old.css 2025-12-04 14:54:02.545516911 +0000
+++ ./tokens/css/variables-android.css 2025-12-04 14:53:30.193514340 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 09:32:04 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 14:53:30 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-browser.diff--- ./tokens/css/variables-browser.old.css 2025-12-04 14:54:02.819516868 +0000
+++ ./tokens/css/variables-browser.css 2025-12-04 14:53:30.178514376 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 09:32:04 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 14:53:30 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-ios.diff--- ./tokens/css/variables-ios.old.css 2025-12-04 14:54:03.090516822 +0000
+++ ./tokens/css/variables-ios.css 2025-12-04 14:53:30.208514305 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 09:32:04 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 14:53:30 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-marketing.diff--- ./tokens/css/variables-marketing.old.css 2025-12-04 14:54:03.359516777 +0000
+++ ./tokens/css/variables-marketing.css 2025-12-04 14:53:30.234514243 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 09:32:04 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 14:53:30 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-news.diff--- ./tokens/css/variables-news.old.css 2025-12-04 14:54:03.491516754 +0000
+++ ./tokens/css/variables-news.css 2025-12-04 14:53:30.272514152 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 09:32:04 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 14:53:30 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-newtab.diff--- ./tokens/css/variables-newtab.old.css 2025-12-04 14:54:03.774516709 +0000
+++ ./tokens/css/variables-newtab.css 2025-12-04 14:53:30.286514119 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 09:32:04 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 14:53:30 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-search.diff--- ./tokens/css/variables-search.old.css 2025-12-04 14:54:04.049516663 +0000
+++ ./tokens/css/variables-search.css 2025-12-04 14:53:30.256514191 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 09:32:04 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 14:53:30 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-web3.diff--- ./tokens/css/variables-web3.old.css 2025-12-04 14:54:04.325516620 +0000
+++ ./tokens/css/variables-web3.css 2025-12-04 14:53:30.292514105 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 09:32:04 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 14:53:30 GMT+0000 (Coordinated Universal Time)
*/
@media (prefers-color-scheme: light) {
Variables Diff: variables.diff--- ./tokens/css/variables.old.css 2025-12-04 14:54:04.734516527 +0000
+++ ./tokens/css/variables.css 2025-12-04 14:53:30.065514644 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 09:32:04 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 14:53:30 GMT+0000 (Coordinated Universal Time)
*/
:root {
|
32eef6f to
557d367
Compare
|
👋 Thanks for Submitting! This PR is available for preview at the link below. ✅ PR tip preview: https://1273.pr.nala.bravesoftware.com/ - ./tokens/css/variables-android.old.css: 7390 bytes
+ ./tokens/css/variables-android.css: 7390 bytes
---
- ./tokens/css/variables-browser.old.css: 6644 bytes
+ ./tokens/css/variables-browser.css: 6644 bytes
---
- ./tokens/css/variables-ios.old.css: 8180 bytes
+ ./tokens/css/variables-ios.css: 8180 bytes
---
- ./tokens/css/variables-marketing.old.css: 13501 bytes
+ ./tokens/css/variables-marketing.css: 13501 bytes
---
- ./tokens/css/variables-news.old.css: 526 bytes
+ ./tokens/css/variables-news.css: 526 bytes
---
- ./tokens/css/variables-newtab.old.css: 1933 bytes
+ ./tokens/css/variables-newtab.css: 1933 bytes
---
- ./tokens/css/variables-search.old.css: 17733 bytes
+ ./tokens/css/variables-search.css: 17733 bytes
---
- ./tokens/css/variables-web3.old.css: 893 bytes
+ ./tokens/css/variables-web3.css: 893 bytes
---
- ./tokens/css/variables.old.css: 125593 bytes
+ ./tokens/css/variables.css: 125593 bytes
Variables Diff: variables-android.diff--- ./tokens/css/variables-android.old.css 2025-12-04 18:13:35.425343108 +0000
+++ ./tokens/css/variables-android.css 2025-12-04 18:13:01.999595750 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 14:54:58 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 18:13:02 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-browser.diff--- ./tokens/css/variables-browser.old.css 2025-12-04 18:13:35.603341756 +0000
+++ ./tokens/css/variables-browser.css 2025-12-04 18:13:01.984595860 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 14:54:58 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 18:13:01 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-ios.diff--- ./tokens/css/variables-ios.old.css 2025-12-04 18:13:35.767340511 +0000
+++ ./tokens/css/variables-ios.css 2025-12-04 18:13:02.015595631 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 14:54:58 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 18:13:02 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-marketing.diff--- ./tokens/css/variables-marketing.old.css 2025-12-04 18:13:35.938339211 +0000
+++ ./tokens/css/variables-marketing.css 2025-12-04 18:13:02.040595447 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 14:54:58 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 18:13:02 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-news.diff--- ./tokens/css/variables-news.old.css 2025-12-04 18:13:36.121337821 +0000
+++ ./tokens/css/variables-news.css 2025-12-04 18:13:02.083595129 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 14:54:58 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 18:13:02 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-newtab.diff--- ./tokens/css/variables-newtab.old.css 2025-12-04 18:13:36.286336570 +0000
+++ ./tokens/css/variables-newtab.css 2025-12-04 18:13:02.091595070 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 14:54:58 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 18:13:02 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-search.diff--- ./tokens/css/variables-search.old.css 2025-12-04 18:13:36.492335004 +0000
+++ ./tokens/css/variables-search.css 2025-12-04 18:13:02.064595270 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 14:54:58 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 18:13:02 GMT+0000 (Coordinated Universal Time)
*/
:root {
Variables Diff: variables-web3.diff--- ./tokens/css/variables-web3.old.css 2025-12-04 18:13:36.638333896 +0000
+++ ./tokens/css/variables-web3.css 2025-12-04 18:13:02.096595033 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 14:54:58 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 18:13:02 GMT+0000 (Coordinated Universal Time)
*/
@media (prefers-color-scheme: light) {
Variables Diff: variables.diff--- ./tokens/css/variables.old.css 2025-12-04 18:13:36.831332431 +0000
+++ ./tokens/css/variables.css 2025-12-04 18:13:01.859596783 +0000
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Thu Dec 04 2025 14:54:57 GMT+0000 (Coordinated Universal Time)
+ * Generated on Thu Dec 04 2025 18:13:01 GMT+0000 (Coordinated Universal Time)
*/
:root {
|
This PR contains the following updates:
v5.0.1->v6.0.0v6.0.1Release Notes
actions/checkout (actions/checkout)
v6.0.0Compare Source
Configuration
📅 Schedule: Branch creation - Tuesday through Thursday ( * * * * 2-4 ) (UTC), Automerge - At any time (no schedule defined).
🚦 Automerge: Enabled.
♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.