Compare commits

...

8 Commits

Author SHA1 Message Date
eric sciple
eddff1118c Persist creds to a separate file 2025-10-17 19:00:01 +00:00
Salman Chishti
ff7abcd0c3 Update README to include Node.js 24 support details and requirements (#2248)
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
Build and Test / build (push) Has been cancelled
Build and Test / test (macos-latest) (push) Has been cancelled
Build and Test / test (ubuntu-latest) (push) Has been cancelled
Build and Test / test (windows-latest) (push) Has been cancelled
Build and Test / test-proxy (push) Has been cancelled
Build and Test / test-bypass-proxy (push) Has been cancelled
Build and Test / test-git-container (push) Has been cancelled
Build and Test / test-output (push) Has been cancelled
* Update README to include Node.js 24 support details and requirements

* Update
2025-08-13 13:57:25 +01:00
Salman Chishti
08c6903cd8 Prepare v5.0.0 release (#2238)
Some checks failed
Check dist / check-dist (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Build and Test / build (push) Has been cancelled
Build and Test / test (macos-latest) (push) Has been cancelled
Build and Test / test (ubuntu-latest) (push) Has been cancelled
Build and Test / test (windows-latest) (push) Has been cancelled
Build and Test / test-proxy (push) Has been cancelled
Build and Test / test-bypass-proxy (push) Has been cancelled
Build and Test / test-git-container (push) Has been cancelled
Build and Test / test-output (push) Has been cancelled
2025-08-11 13:35:28 +01:00
Salman Chishti
9f265659d3 Update actions checkout to use node 24 (#2226)
* use node 24

* update other parts to node 24

* bump to major version, audit fix, changelog

* update licenses

* update dist

* update major version

* will do separate pr for v5 and will do a minor version for previous changes
2025-08-11 11:52:51 +01:00
Salman Chishti
08eba0b27e Prepare release v4.3.0 (#2237) 2025-08-11 11:30:37 +01:00
Salman Chishti
631c7dc4f8 Update package dependencies (#2236)
* package updates

* update dist

* Update license files
2025-08-11 11:22:41 +01:00
Tingluo Huang
8edcb1bdb4 Update CODEOWNERS for actions (#2224)
Some checks failed
Check dist / check-dist (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Build and Test / build (push) Has been cancelled
Build and Test / test (macos-latest) (push) Has been cancelled
Build and Test / test (ubuntu-latest) (push) Has been cancelled
Build and Test / test (windows-latest) (push) Has been cancelled
Build and Test / test-proxy (push) Has been cancelled
Build and Test / test-bypass-proxy (push) Has been cancelled
Build and Test / test-git-container (push) Has been cancelled
Build and Test / test-output (push) Has been cancelled
2025-07-23 09:20:20 -04:00
Ben De St Paer-Gotch
09d2acae67 Update README.md (#2194)
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
Build and Test / build (push) Has been cancelled
Build and Test / test (macos-latest) (push) Has been cancelled
Build and Test / test (ubuntu-latest) (push) Has been cancelled
Build and Test / test (windows-latest) (push) Has been cancelled
Build and Test / test-proxy (push) Has been cancelled
Build and Test / test-bypass-proxy (push) Has been cancelled
Build and Test / test-git-container (push) Has been cancelled
Build and Test / test-output (push) Has been cancelled
2025-06-06 09:19:16 +01:00
22 changed files with 1161 additions and 442 deletions

View File

@@ -24,10 +24,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
- name: Set Node.js 20.x - name: Set Node.js 24.x
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 24.x
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci

View File

@@ -18,7 +18,7 @@ jobs:
steps: steps:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 24.x
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.6
- run: npm ci - run: npm ci
- run: npm run build - run: npm run build
@@ -302,12 +302,15 @@ jobs:
# Clone this repo # Clone this repo
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.6
with:
path: actions-checkout
# Basic checkout using git # Basic checkout using git
- name: Checkout basic - name: Checkout basic
id: checkout id: checkout
uses: ./ uses: ./actions-checkout
with: with:
path: cloned-using-local-action
ref: test-data/v2/basic ref: test-data/v2/basic
# Verify output # Verify output
@@ -325,7 +328,3 @@ jobs:
echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d" echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d"
exit 1 exit 1
fi fi
# needed to make checkout post cleanup succeed
- name: Fix Checkout
uses: actions/checkout@v4.1.6

View File

@@ -11,6 +11,7 @@ on:
type: choice type: choice
description: The major version to update description: The major version to update
options: options:
- v5
- v4 - v4
- v3 - v3
- v2 - v2

View File

@@ -1,9 +1,9 @@
--- ---
name: "@octokit/endpoint" name: "@octokit/endpoint"
version: 9.0.5 version: 9.0.6
type: npm type: npm
summary: Turns REST API endpoints into generic request options summary: Turns REST API endpoints into generic request options
homepage: homepage:
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE

View File

@@ -1,9 +1,9 @@
--- ---
name: "@octokit/plugin-paginate-rest" name: "@octokit/plugin-paginate-rest"
version: 9.2.1 version: 9.2.2
type: npm type: npm
summary: Octokit plugin to paginate REST API endpoint responses summary: Octokit plugin to paginate REST API endpoint responses
homepage: homepage:
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE

View File

@@ -1,9 +1,9 @@
--- ---
name: "@octokit/request-error" name: "@octokit/request-error"
version: 5.1.0 version: 5.1.1
type: npm type: npm
summary: Error class for Octokit request errors summary: Error class for Octokit request errors
homepage: homepage:
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE

View File

@@ -1,10 +1,10 @@
--- ---
name: "@octokit/request" name: "@octokit/request"
version: 8.4.0 version: 8.4.1
type: npm type: npm
summary: Send parameterized requests to GitHub's APIs with sensible defaults in browsers summary: Send parameterized requests to GitHub's APIs with sensible defaults in browsers
and Node and Node
homepage: homepage:
license: mit license: mit
licenses: licenses:
- sources: LICENSE - sources: LICENSE

View File

@@ -1,6 +1,6 @@
--- ---
name: undici name: undici
version: 5.28.4 version: 5.29.0
type: npm type: npm
summary: An HTTP/1.1 client, written from scratch for Node.js summary: An HTTP/1.1 client, written from scratch for Node.js
homepage: https://undici.nodejs.org homepage: https://undici.nodejs.org

View File

@@ -1,5 +1,18 @@
# Changelog # Changelog
## V5.0.0
* Update actions checkout to use node 24 by @salmanmkc in https://github.com/actions/checkout/pull/2226
## V4.3.0
* docs: update README.md by @motss in https://github.com/actions/checkout/pull/1971
* Add internal repos for checking out multiple repositories by @mouismail in https://github.com/actions/checkout/pull/1977
* Documentation update - add recommended permissions to Readme by @benwells in https://github.com/actions/checkout/pull/2043
* Adjust positioning of user email note and permissions heading by @joshmgross in https://github.com/actions/checkout/pull/2044
* Update README.md by @nebuk89 in https://github.com/actions/checkout/pull/2194
* Update CODEOWNERS for actions by @TingluoHuang in https://github.com/actions/checkout/pull/2224
* Update package dependencies by @salmanmkc in https://github.com/actions/checkout/pull/2236
## v4.2.2 ## v4.2.2
* `url-helper.ts` now leverages well-known environment variables by @jww3 in https://github.com/actions/checkout/pull/1941 * `url-helper.ts` now leverages well-known environment variables by @jww3 in https://github.com/actions/checkout/pull/1941
* Expand unit test coverage for `isGhes` by @jww3 in https://github.com/actions/checkout/pull/1946 * Expand unit test coverage for `isGhes` by @jww3 in https://github.com/actions/checkout/pull/1946

View File

@@ -1 +1 @@
* @actions/actions-launch * @actions/actions-runtime

View File

@@ -1,5 +1,13 @@
[![Build and Test](https://github.com/actions/checkout/actions/workflows/test.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/test.yml) [![Build and Test](https://github.com/actions/checkout/actions/workflows/test.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/test.yml)
# 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 # Checkout V4
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it. This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
@@ -10,6 +18,24 @@ The auth token is persisted in the local git config. This enables your scripts t
When Git 2.18 or higher is not in your PATH, falls back to the REST API to download the files. When Git 2.18 or higher is not in your PATH, falls back to the REST API to download the files.
### Note
Thank you for your interest in this GitHub action, however, right now we are not taking contributions.
We continue to focus our resources on strategic areas that help our customers be successful while making developers' lives easier. While GitHub Actions remains a key part of this vision, we are allocating resources towards other areas of Actions and are not taking contributions to this repository at this time. The GitHub public roadmap is the best place to follow along for any updates on features were working on and what stage theyre in.
We are taking the following steps to better direct requests related to GitHub Actions, including:
1. We will be directing questions and support requests to our [Community Discussions area](https://github.com/orgs/community/discussions/categories/actions)
2. High Priority bugs can be reported through Community Discussions or you can report these to our support team https://support.github.com/contact/bug-report.
3. Security Issues should be handled as per our [security.md](security.md)
We will still provide security updates for this project and fix major breaking changes during this time.
You are welcome to still raise bugs in this repo.
# What's new # What's new
Please refer to the [release page](https://github.com/actions/checkout/releases/latest) for the latest release notes. Please refer to the [release page](https://github.com/actions/checkout/releases/latest) for the latest release notes.
@@ -18,7 +44,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
<!-- start usage --> <!-- start usage -->
```yaml ```yaml
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
# Repository name with owner. For example, actions/checkout # Repository name with owner. For example, actions/checkout
# Default: ${{ github.repository }} # Default: ${{ github.repository }}
@@ -131,24 +157,33 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
# Scenarios # Scenarios
- [Fetch only the root files](#Fetch-only-the-root-files) - [Checkout V5](#checkout-v5)
- [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder) - [What's new](#whats-new)
- [Fetch only a single file](#Fetch-only-a-single-file) - [Checkout V4](#checkout-v4)
- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches) - [Note](#note)
- [Checkout a different branch](#Checkout-a-different-branch) - [What's new](#whats-new-1)
- [Checkout HEAD^](#Checkout-HEAD) - [Usage](#usage)
- [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side) - [Scenarios](#scenarios)
- [Checkout multiple repos (nested)](#Checkout-multiple-repos-nested) - [Fetch only the root files](#fetch-only-the-root-files)
- [Checkout multiple repos (private)](#Checkout-multiple-repos-private) - [Fetch only the root files and `.github` and `src` folder](#fetch-only-the-root-files-and-github-and-src-folder)
- [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit) - [Fetch only a single file](#fetch-only-a-single-file)
- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event) - [Fetch all history for all tags and branches](#fetch-all-history-for-all-tags-and-branches)
- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token) - [Checkout a different branch](#checkout-a-different-branch)
- [Push a commit to a PR using the built-in token](#Push-a-commit-to-a-PR-using-the-built-in-token) - [Checkout HEAD^](#checkout-head)
- [Checkout multiple repos (side by side)](#checkout-multiple-repos-side-by-side)
- [Checkout multiple repos (nested)](#checkout-multiple-repos-nested)
- [Checkout multiple repos (private)](#checkout-multiple-repos-private)
- [Checkout pull request HEAD commit instead of merge commit](#checkout-pull-request-head-commit-instead-of-merge-commit)
- [Checkout pull request on closed event](#checkout-pull-request-on-closed-event)
- [Push a commit using the built-in token](#push-a-commit-using-the-built-in-token)
- [Push a commit to a PR using the built-in token](#push-a-commit-to-a-pr-using-the-built-in-token)
- [Recommended permissions](#recommended-permissions)
- [License](#license)
## Fetch only the root files ## Fetch only the root files
```yaml ```yaml
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
sparse-checkout: . sparse-checkout: .
``` ```
@@ -156,7 +191,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Fetch only the root files and `.github` and `src` folder ## Fetch only the root files and `.github` and `src` folder
```yaml ```yaml
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
sparse-checkout: | sparse-checkout: |
.github .github
@@ -166,7 +201,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Fetch only a single file ## Fetch only a single file
```yaml ```yaml
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
sparse-checkout: | sparse-checkout: |
README.md README.md
@@ -176,7 +211,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Fetch all history for all tags and branches ## Fetch all history for all tags and branches
```yaml ```yaml
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
``` ```
@@ -184,7 +219,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Checkout a different branch ## Checkout a different branch
```yaml ```yaml
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
ref: my-branch ref: my-branch
``` ```
@@ -192,7 +227,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Checkout HEAD^ ## Checkout HEAD^
```yaml ```yaml
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
fetch-depth: 2 fetch-depth: 2
- run: git checkout HEAD^ - run: git checkout HEAD^
@@ -202,12 +237,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
```yaml ```yaml
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
path: main path: main
- name: Checkout tools repo - name: Checkout tools repo
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
repository: my-org/my-tools repository: my-org/my-tools
path: my-tools path: my-tools
@@ -218,10 +253,10 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
```yaml ```yaml
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Checkout tools repo - name: Checkout tools repo
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
repository: my-org/my-tools repository: my-org/my-tools
path: my-tools path: my-tools
@@ -232,12 +267,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
```yaml ```yaml
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
path: main path: main
- name: Checkout private tools - name: Checkout private tools
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
repository: my-org/my-private-tools repository: my-org/my-private-tools
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
@@ -250,7 +285,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Checkout pull request HEAD commit instead of merge commit ## Checkout pull request HEAD commit instead of merge commit
```yaml ```yaml
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
``` ```
@@ -266,7 +301,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
``` ```
## Push a commit using the built-in token ## Push a commit using the built-in token
@@ -277,7 +312,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- run: | - run: |
date > generated.txt date > generated.txt
# Note: the following account information will not work on GHES # Note: the following account information will not work on GHES
@@ -299,7 +334,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.head_ref }}
- run: | - run: |

View File

@@ -86,16 +86,29 @@ describe('git-auth-helper tests', () => {
// Act // Act
await authHelper.configureAuth() await authHelper.configureAuth()
// Assert config // Assert config - check that .git/config contains includeIf entries
const configContent = ( const localConfigContent = (
await fs.promises.readFile(localGitConfigPath) await fs.promises.readFile(localGitConfigPath)
).toString() ).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( const basicCredential = Buffer.from(
`x-access-token:${settings.authToken}`, `x-access-token:${settings.authToken}`,
'utf8' 'utf8'
).toString('base64') ).toString('base64')
expect( expect(
configContent.indexOf( credentialsContent.indexOf(
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}` `http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
) )
).toBeGreaterThanOrEqual(0) ).toBeGreaterThanOrEqual(0)
@@ -120,7 +133,7 @@ describe('git-auth-helper tests', () => {
'inject https://github.com as github server url' 'inject https://github.com as github server url'
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => { it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
await testAuthHeader( await testAuthHeader(
configureAuth_AcceptsGitHubServerUrl, configureAuth_AcceptsGitHubServerUrlSetToGHEC,
'https://github.com' 'https://github.com'
) )
}) })
@@ -141,12 +154,17 @@ describe('git-auth-helper tests', () => {
// Act // Act
await authHelper.configureAuth() await authHelper.configureAuth()
// Assert config // Assert config - check credentials config file (not local .git/config)
const configContent = ( const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
await fs.promises.readFile(localGitConfigPath) 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() ).toString()
expect( expect(
configContent.indexOf( credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION` `http.https://github.com/.extraheader AUTHORIZATION`
) )
).toBeGreaterThanOrEqual(0) ).toBeGreaterThanOrEqual(0)
@@ -251,13 +269,16 @@ describe('git-auth-helper tests', () => {
expectedSshCommand expectedSshCommand
) )
// Asserty git config // Assert git config
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath)) const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
.toString() .toString()
.split('\n') .split('\n')
.filter(x => x) .filter(x => x)
expect(gitConfigLines).toHaveLength(1) // Should have includeIf entries pointing to credentials file
expect(gitConfigLines[0]).toMatch(/^http\./) expect(gitConfigLines.length).toBeGreaterThan(0)
expect(
gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0)
).toBeTruthy()
}) })
const configureAuth_setsSshCommandWhenPersistCredentialsTrue = const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
@@ -419,8 +440,20 @@ describe('git-auth-helper tests', () => {
expect( expect(
configContent.indexOf('value-from-global-config') configContent.indexOf('value-from-global-config')
).toBeGreaterThanOrEqual(0) ).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( expect(
configContent.indexOf( credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}` `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
) )
).toBeGreaterThanOrEqual(0) ).toBeGreaterThanOrEqual(0)
@@ -463,8 +496,20 @@ describe('git-auth-helper tests', () => {
const configContent = ( const configContent = (
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig')) await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
).toString() ).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( expect(
configContent.indexOf( credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}` `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
) )
).toBeGreaterThanOrEqual(0) ).toBeGreaterThanOrEqual(0)
@@ -550,15 +595,15 @@ describe('git-auth-helper tests', () => {
await authHelper.configureSubmoduleAuth() await authHelper.configureSubmoduleAuth()
// Assert // Assert
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4) // Should configure insteadOf (2 calls for two values)
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/ /unset-all.*insteadOf/
) )
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
/url.*insteadOf.*git@github.com:/ /url.*insteadOf.*git@github.com:/
) )
expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch( expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
/url.*insteadOf.*org-123456@github.com:/ /url.*insteadOf.*org-123456@github.com:/
) )
} }
@@ -589,12 +634,12 @@ describe('git-auth-helper tests', () => {
await authHelper.configureSubmoduleAuth() await authHelper.configureSubmoduleAuth()
// Assert // Assert
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3) // Should configure sshCommand (1 call)
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/ /unset-all.*insteadOf/
) )
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/)
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
} }
) )
@@ -660,19 +705,35 @@ describe('git-auth-helper tests', () => {
await setup(removeAuth_removesToken) await setup(removeAuth_removesToken)
const authHelper = gitAuthHelper.createAuthHelper(git, settings) const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth() await authHelper.configureAuth()
let gitConfigContent = (
// Sanity check - verify includeIf entries exist in local config
let localConfigContent = (
await fs.promises.readFile(localGitConfigPath) await fs.promises.readFile(localGitConfigPath)
).toString() ).toString()
expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check expect(
localConfigContent.indexOf('includeIf.gitdir:')
).toBeGreaterThanOrEqual(0)
// Sanity check - 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)
// Act // Act
await authHelper.removeAuth() await authHelper.removeAuth()
// Assert git config // Assert includeIf entries removed from local git config
gitConfigContent = ( localConfigContent = (
await fs.promises.readFile(localGitConfigPath) await fs.promises.readFile(localGitConfigPath)
).toString() ).toString()
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0) expect(localConfigContent.indexOf('includeIf.gitdir:')).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)
}) })
const removeGlobalConfig_removesOverride = const removeGlobalConfig_removesOverride =
@@ -701,6 +762,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> { async function setup(testName: string): Promise<void> {
@@ -715,6 +822,7 @@ async function setup(testName: string): Promise<void> {
await fs.promises.mkdir(tempHomedir, {recursive: true}) await fs.promises.mkdir(tempHomedir, {recursive: true})
process.env['RUNNER_TEMP'] = runnerTemp process.env['RUNNER_TEMP'] = runnerTemp
process.env['HOME'] = tempHomedir process.env['HOME'] = tempHomedir
process.env['GITHUB_WORKSPACE'] = workspace
// Create git config // Create git config
globalGitConfigPath = path.join(tempHomedir, '.gitconfig') globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
@@ -733,10 +841,20 @@ async function setup(testName: string): Promise<void> {
checkout: jest.fn(), checkout: jest.fn(),
checkoutDetach: jest.fn(), checkoutDetach: jest.fn(),
config: jest.fn( config: jest.fn(
async (key: string, value: string, globalConfig?: boolean) => { async (
const configPath = globalConfig key: string,
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig') value: string,
: localGitConfigPath 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}`) await fs.promises.appendFile(configPath, `\n${key} ${value}`)
} }
), ),
@@ -756,6 +874,7 @@ async function setup(testName: string): Promise<void> {
env: {}, env: {},
fetch: jest.fn(), fetch: jest.fn(),
getDefaultBranch: jest.fn(), getDefaultBranch: jest.fn(),
getSubmoduleConfigPaths: jest.fn(async () => []),
getWorkingDirectory: jest.fn(() => workspace), getWorkingDirectory: jest.fn(() => workspace),
init: jest.fn(), init: jest.fn(),
isDetached: jest.fn(), isDetached: jest.fn(),
@@ -794,8 +913,57 @@ async function setup(testName: string): Promise<void> {
return true return true
} }
), ),
tryConfigUnsetValue: jest.fn(
async (
key: string,
value: string,
globalConfig?: boolean
): Promise<boolean> => {
const configPath = globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath
let content = await fs.promises.readFile(configPath)
let lines = content
.toString()
.split('\n')
.filter(x => x)
.filter(x => !(x.startsWith(key) && x.includes(value)))
await fs.promises.writeFile(configPath, lines.join('\n'))
return true
}
),
tryDisableAutomaticGarbageCollection: jest.fn(), tryDisableAutomaticGarbageCollection: jest.fn(),
tryGetFetchUrl: jest.fn(), tryGetFetchUrl: jest.fn(),
tryGetConfigValues: jest.fn(
async (key: string, globalConfig?: boolean): Promise<string[]> => {
const configPath = globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath
const content = await fs.promises.readFile(configPath)
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): Promise<string[]> => {
const configPath = globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath
const content = await fs.promises.readFile(configPath)
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(), tryReset: jest.fn(),
version: jest.fn() version: jest.fn()
} }
@@ -830,6 +998,7 @@ async function setup(testName: string): Promise<void> {
async function getActualSshKeyPath(): Promise<string> { async function getActualSshKeyPath(): Promise<string> {
let actualTempFiles = (await fs.promises.readdir(runnerTemp)) let actualTempFiles = (await fs.promises.readdir(runnerTemp))
.filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
.sort() .sort()
.map(x => path.join(runnerTemp, x)) .map(x => path.join(runnerTemp, x))
if (actualTempFiles.length === 0) { if (actualTempFiles.length === 0) {
@@ -843,6 +1012,7 @@ async function getActualSshKeyPath(): Promise<string> {
async function getActualSshKnownHostsPath(): Promise<string> { async function getActualSshKnownHostsPath(): Promise<string> {
let actualTempFiles = (await fs.promises.readdir(runnerTemp)) let actualTempFiles = (await fs.promises.readdir(runnerTemp))
.filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
.sort() .sort()
.map(x => path.join(runnerTemp, x)) .map(x => path.join(runnerTemp, x))
if (actualTempFiles.length === 0) { if (actualTempFiles.length === 0) {

View File

@@ -471,6 +471,7 @@ async function setup(testName: string): Promise<void> {
configExists: jest.fn(), configExists: jest.fn(),
fetch: jest.fn(), fetch: jest.fn(),
getDefaultBranch: jest.fn(), getDefaultBranch: jest.fn(),
getSubmoduleConfigPaths: jest.fn(async () => []),
getWorkingDirectory: jest.fn(() => repositoryPath), getWorkingDirectory: jest.fn(() => repositoryPath),
init: jest.fn(), init: jest.fn(),
isDetached: jest.fn(), isDetached: jest.fn(),
@@ -493,12 +494,15 @@ async function setup(testName: string): Promise<void> {
return true return true
}), }),
tryConfigUnset: jest.fn(), tryConfigUnset: jest.fn(),
tryConfigUnsetValue: jest.fn(),
tryDisableAutomaticGarbageCollection: jest.fn(), tryDisableAutomaticGarbageCollection: jest.fn(),
tryGetFetchUrl: jest.fn(async () => { tryGetFetchUrl: jest.fn(async () => {
// Sanity check - this function shouldn't be called when the .git directory doesn't exist // Sanity check - this function shouldn't be called when the .git directory doesn't exist
await fs.promises.stat(path.join(repositoryPath, '.git')) await fs.promises.stat(path.join(repositoryPath, '.git'))
return repositoryUrl return repositoryUrl
}), }),
tryGetConfigValues: jest.fn(),
tryGetConfigKeys: jest.fn(),
tryReset: jest.fn(async () => { tryReset: jest.fn(async () => {
return true return true
}), }),

View File

@@ -17,7 +17,7 @@ fi
echo "Testing persisted credential" echo "Testing persisted credential"
pushd ./submodules-recursive/submodule-level-1/submodule-level-2 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 if [ "$?" != "0" ]; then
echo "Failed to validate persisted credential" echo "Failed to validate persisted credential"
popd popd

View File

@@ -17,7 +17,7 @@ fi
echo "Testing persisted credential" echo "Testing persisted credential"
pushd ./submodules-true/submodule-level-1 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 if [ "$?" != "0" ]; then
echo "Failed to validate persisted credential" echo "Failed to validate persisted credential"
popd popd

View File

@@ -104,6 +104,6 @@ outputs:
commit: commit:
description: 'The commit SHA that was checked out' description: 'The commit SHA that was checked out'
runs: runs:
using: node20 using: node24
main: dist/index.js main: dist/index.js
post: dist/index.js post: dist/index.js

436
dist/index.js vendored
View File

@@ -162,6 +162,7 @@ class GitAuthHelper {
this.sshKeyPath = ''; this.sshKeyPath = '';
this.sshKnownHostsPath = ''; this.sshKnownHostsPath = '';
this.temporaryHomePath = ''; this.temporaryHomePath = '';
this.credentialsConfigPath = ''; // Path to separate credentials config file in RUNNER_TEMP
this.git = gitCommandManager; this.git = gitCommandManager;
this.settings = gitSourceSettings || {}; this.settings = gitSourceSettings || {};
// Token auth header // Token auth header
@@ -229,15 +230,17 @@ class GitAuthHelper {
configureGlobalAuth() { configureGlobalAuth() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
// 'configureTempGlobalConfig' noops if already set, just returns the path // 'configureTempGlobalConfig' noops if already set, just returns the path
const newGitConfigPath = yield this.configureTempGlobalConfig(); yield this.configureTempGlobalConfig();
try { try {
// Configure the token // Configure the token
yield this.configureToken(newGitConfigPath, true); yield this.configureToken(true);
// Configure HTTPS instead of SSH // Configure HTTPS instead of SSH
yield this.git.tryConfigUnset(this.insteadOfKey, true); yield this.git.tryConfigUnset(this.insteadOfKey, true);
if (!this.settings.sshKey) { if (!this.settings.sshKey) {
for (const insteadOfValue of this.insteadOfValues) { 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() { configureSubmoduleAuth() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
// Remove possible previous HTTPS instead of SSH // Remove possible previous HTTPS instead of SSH
yield this.removeGitConfig(this.insteadOfKey, true); yield this.removeSubmoduleGitConfig(this.insteadOfKey);
if (this.settings.persistCredentials) { if (this.settings.persistCredentials) {
// Configure a placeholder value. This approach avoids the credential being captured // Get the credentials config file path in RUNNER_TEMP
// by process creation audit events, which are commonly logged. For more information, const credentialsConfigPath = this.getCredentialsConfigPath();
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing // Container credentials config path
const output = yield this.git.submoduleForeach( const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline // Get submodule config file paths.
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules); const configPaths = yield this.git.getSubmoduleConfigPaths(this.settings.nestedSubmodules);
// Replace the placeholder // For each submodule, configure includeIf entries pointing to the shared credentials file.
const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []; // Configure both host and container paths to support Docker container actions.
for (const configPath of configPaths) { for (const configPath of configPaths) {
core.debug(`Replacing token placeholder in '${configPath}'`); // Submodule Git directory
yield this.replaceTokenPlaceholder(configPath); 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) { if (this.settings.sshKey) {
// Configure core.sshCommand // 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() { configureSsh() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
if (!this.settings.sshKey) { if (!this.settings.sshKey) {
@@ -351,43 +373,88 @@ class GitAuthHelper {
} }
}); });
} }
configureToken(configPath, globalConfig) { /**
* 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* () { return __awaiter(this, void 0, void 0, function* () {
// Validate args // Get the credentials config file path in RUNNER_TEMP
assert.ok((configPath && globalConfig) || (!configPath && !globalConfig), 'Unexpected configureToken parameter combinations'); const credentialsConfigPath = this.getCredentialsConfigPath();
// Default config path // Write placeholder to the separate credentials config file using git config.
if (!configPath && !globalConfig) { // This approach avoids the credential being captured by process creation audit events,
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config'); // 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
// Configure a placeholder value. This approach avoids the credential being captured yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, false, // globalConfig?
// by process creation audit events, which are commonly logged. For more information, false, // add?
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing credentialsConfigPath);
yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig); // Replace the placeholder in the credentials config file
// Replace the placeholder let content = (yield fs.promises.readFile(credentialsConfigPath)).toString();
yield this.replaceTokenPlaceholder(configPath || '');
});
}
replaceTokenPlaceholder(configPath) {
return __awaiter(this, void 0, void 0, function* () {
assert.ok(configPath, 'configPath is not defined');
let content = (yield fs.promises.readFile(configPath)).toString();
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue); const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue);
if (placeholderIndex < 0 || if (placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) { 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'); assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined');
content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue); 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() { removeSsh() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
var _a; var _a, _b;
// SSH key // SSH key
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath; const keyPath = this.sshKeyPath || stateHelper.SshKeyPath;
if (keyPath) { if (keyPath) {
try { try {
core.info(`Removing SSH key '${keyPath}'`);
yield io.rmRF(keyPath); yield io.rmRF(keyPath);
} }
catch (err) { catch (err) {
@@ -399,37 +466,136 @@ class GitAuthHelper {
const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath; const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath;
if (knownHostsPath) { if (knownHostsPath) {
try { try {
core.info(`Removing SSH known hosts '${knownHostsPath}'`);
yield io.rmRF(knownHostsPath); yield io.rmRF(knownHostsPath);
} }
catch (_b) { catch (err) {
// Intentionally empty 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 // SSH command
core.info('Removing SSH command configuration');
yield this.removeGitConfig(SSH_COMMAND_KEY); 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() { removeToken() {
return __awaiter(this, void 0, void 0, function* () { 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.removeGitConfig(this.tokenConfigKey);
}); yield this.removeSubmoduleGitConfig(this.tokenConfigKey);
} // Collect credentials config paths that need to be removed
removeGitConfig(configKey_1) { const credentialsPaths = new Set();
return __awaiter(this, arguments, void 0, function* (configKey, submoduleOnly = false) { // Remove includeIf entries that point to git-credentials-*.config files
if (!submoduleOnly) { core.info('Removing includeIf entries pointing to credentials config files');
if ((yield this.git.configExists(configKey)) && const mainCredentialsPaths = yield this.removeIncludeIfCredentials();
!(yield this.git.tryConfigUnset(configKey))) { mainCredentialsPaths.forEach(path => credentialsPaths.add(path));
// Load the config contents // Remove submodule includeIf entries that point to git-credentials-*.config files
core.warning(`Failed to remove '${configKey}' from the git config`); 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`);
} }
} }
});
}
/**
* 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); const pattern = regexpHelper.escape(configKey);
yield this.git.submoduleForeach( 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); `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); yield this.execGit(args);
}); });
} }
config(configKey, configValue, globalConfig, add) { config(configKey, configValue, globalConfig, add, configFile) {
return __awaiter(this, void 0, void 0, function* () { 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) { if (add) {
args.push('--add'); args.push('--add');
} }
@@ -706,6 +878,16 @@ class GitCommandManager {
throw new Error('Unexpected output when retrieving default branch'); 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() { getWorkingDirectory() {
return this.workingDirectory; return this.workingDirectory;
} }
@@ -836,6 +1018,20 @@ class GitCommandManager {
return output.exitCode === 0; 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() { tryDisableAutomaticGarbageCollection() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true); const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true);
@@ -855,6 +1051,46 @@ class GitCommandManager {
return stdout; 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() { tryReset() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['reset', '--hard', 'HEAD'], true); const output = yield this.execGit(['reset', '--hard', 'HEAD'], true);
@@ -7802,7 +8038,7 @@ module.exports = __toCommonJS(dist_src_exports);
var import_universal_user_agent = __nccwpck_require__(5030); var import_universal_user_agent = __nccwpck_require__(5030);
// pkg/dist-src/version.js // pkg/dist-src/version.js
var VERSION = "9.0.5"; var VERSION = "9.0.6";
// pkg/dist-src/defaults.js // pkg/dist-src/defaults.js
var userAgent = `octokit-endpoint.js/${VERSION} ${(0, import_universal_user_agent.getUserAgent)()}`; var userAgent = `octokit-endpoint.js/${VERSION} ${(0, import_universal_user_agent.getUserAgent)()}`;
@@ -7907,9 +8143,9 @@ function addQueryParameters(url, parameters) {
} }
// pkg/dist-src/util/extract-url-variable-names.js // pkg/dist-src/util/extract-url-variable-names.js
var urlVariableRegex = /\{[^}]+\}/g; var urlVariableRegex = /\{[^{}}]+\}/g;
function removeNonChars(variableName) { function removeNonChars(variableName) {
return variableName.replace(/^\W+|\W+$/g, "").split(/,/); return variableName.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
} }
function extractUrlVariableNames(url) { function extractUrlVariableNames(url) {
const matches = url.match(urlVariableRegex); const matches = url.match(urlVariableRegex);
@@ -8095,7 +8331,7 @@ function parse(options) {
} }
if (url.endsWith("/graphql")) { if (url.endsWith("/graphql")) {
if (options.mediaType.previews?.length) { if (options.mediaType.previews?.length) {
const previewsFromAcceptHeader = headers.accept.match(/[\w-]+(?=-preview)/g) || []; const previewsFromAcceptHeader = headers.accept.match(/(?<![\w-])[\w-]+(?=-preview)/g) || [];
headers.accept = previewsFromAcceptHeader.concat(options.mediaType.previews).map((preview) => { headers.accept = previewsFromAcceptHeader.concat(options.mediaType.previews).map((preview) => {
const format = options.mediaType.format ? `.${options.mediaType.format}` : "+json"; const format = options.mediaType.format ? `.${options.mediaType.format}` : "+json";
return `application/vnd.github.${preview}-preview${format}`; return `application/vnd.github.${preview}-preview${format}`;
@@ -8344,7 +8580,7 @@ __export(dist_src_exports, {
module.exports = __toCommonJS(dist_src_exports); module.exports = __toCommonJS(dist_src_exports);
// pkg/dist-src/version.js // pkg/dist-src/version.js
var VERSION = "9.2.1"; var VERSION = "9.2.2";
// pkg/dist-src/normalize-paginated-list-response.js // pkg/dist-src/normalize-paginated-list-response.js
function normalizePaginatedListResponse(response) { function normalizePaginatedListResponse(response) {
@@ -8392,7 +8628,7 @@ function iterator(octokit, route, parameters) {
const response = await requestMethod({ method, url, headers }); const response = await requestMethod({ method, url, headers });
const normalizedResponse = normalizePaginatedListResponse(response); const normalizedResponse = normalizePaginatedListResponse(response);
url = ((normalizedResponse.headers.link || "").match( url = ((normalizedResponse.headers.link || "").match(
/<([^>]+)>;\s*rel="next"/ /<([^<>]+)>;\s*rel="next"/
) || [])[1]; ) || [])[1];
return { value: normalizedResponse }; return { value: normalizedResponse };
} catch (error) { } catch (error) {
@@ -10944,7 +11180,7 @@ var RequestError = class extends Error {
if (options.request.headers.authorization) { if (options.request.headers.authorization) {
requestCopy.headers = Object.assign({}, options.request.headers, { requestCopy.headers = Object.assign({}, options.request.headers, {
authorization: options.request.headers.authorization.replace( authorization: options.request.headers.authorization.replace(
/ .*$/, /(?<! ) .*$/,
" [REDACTED]" " [REDACTED]"
) )
}); });
@@ -11012,7 +11248,7 @@ var import_endpoint = __nccwpck_require__(9440);
var import_universal_user_agent = __nccwpck_require__(5030); var import_universal_user_agent = __nccwpck_require__(5030);
// pkg/dist-src/version.js // pkg/dist-src/version.js
var VERSION = "8.4.0"; var VERSION = "8.4.1";
// pkg/dist-src/is-plain-object.js // pkg/dist-src/is-plain-object.js
function isPlainObject(value) { function isPlainObject(value) {
@@ -11071,7 +11307,7 @@ function fetchWrapper(requestOptions) {
headers[keyAndValue[0]] = keyAndValue[1]; headers[keyAndValue[0]] = keyAndValue[1];
} }
if ("deprecation" in headers) { if ("deprecation" in headers) {
const matches = headers.link && headers.link.match(/<([^>]+)>; rel="deprecation"/); const matches = headers.link && headers.link.match(/<([^<>]+)>; rel="deprecation"/);
const deprecationLink = matches && matches.pop(); const deprecationLink = matches && matches.pop();
log.warn( log.warn(
`[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${headers.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}` `[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${headers.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}`
@@ -18725,7 +18961,7 @@ module.exports = {
const { parseSetCookie } = __nccwpck_require__(4408) const { parseSetCookie } = __nccwpck_require__(4408)
const { stringify, getHeadersList } = __nccwpck_require__(3121) const { stringify } = __nccwpck_require__(3121)
const { webidl } = __nccwpck_require__(1744) const { webidl } = __nccwpck_require__(1744)
const { Headers } = __nccwpck_require__(554) const { Headers } = __nccwpck_require__(554)
@@ -18801,14 +19037,13 @@ function getSetCookies (headers) {
webidl.brandCheck(headers, Headers, { strict: false }) webidl.brandCheck(headers, Headers, { strict: false })
const cookies = getHeadersList(headers).cookies const cookies = headers.getSetCookie()
if (!cookies) { if (!cookies) {
return [] return []
} }
// In older versions of undici, cookies is a list of name:value. return cookies.map((pair) => parseSetCookie(pair))
return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair))
} }
/** /**
@@ -19236,14 +19471,15 @@ module.exports = {
/***/ }), /***/ }),
/***/ 3121: /***/ 3121:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { /***/ ((module) => {
"use strict"; "use strict";
const assert = __nccwpck_require__(9491) /**
const { kHeadersList } = __nccwpck_require__(2785) * @param {string} value
* @returns {boolean}
*/
function isCTLExcludingHtab (value) { function isCTLExcludingHtab (value) {
if (value.length === 0) { if (value.length === 0) {
return false return false
@@ -19504,31 +19740,13 @@ function stringify (cookie) {
return out.join('; ') return out.join('; ')
} }
let kHeadersListNode
function getHeadersList (headers) {
if (headers[kHeadersList]) {
return headers[kHeadersList]
}
if (!kHeadersListNode) {
kHeadersListNode = Object.getOwnPropertySymbols(headers).find(
(symbol) => symbol.description === 'headers list'
)
assert(kHeadersListNode, 'Headers cannot be parsed')
}
const headersList = headers[kHeadersListNode]
assert(headersList)
return headersList
}
module.exports = { module.exports = {
isCTLExcludingHtab, isCTLExcludingHtab,
stringify, validateCookieName,
getHeadersList validateCookiePath,
validateCookieValue,
toIMFDate,
stringify
} }
@@ -21457,6 +21675,14 @@ const { isUint8Array, isArrayBuffer } = __nccwpck_require__(9830)
const { File: UndiciFile } = __nccwpck_require__(8511) const { File: UndiciFile } = __nccwpck_require__(8511)
const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(685) const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(685)
let random
try {
const crypto = __nccwpck_require__(6005)
random = (max) => crypto.randomInt(0, max)
} catch {
random = (max) => Math.floor(Math.random(max))
}
let ReadableStream = globalThis.ReadableStream let ReadableStream = globalThis.ReadableStream
/** @type {globalThis['File']} */ /** @type {globalThis['File']} */
@@ -21542,7 +21768,7 @@ function extractBody (object, keepalive = false) {
// Set source to a copy of the bytes held by object. // Set source to a copy of the bytes held by object.
source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength)) source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
} else if (util.isFormDataLike(object)) { } else if (util.isFormDataLike(object)) {
const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, '0')}` const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
const prefix = `--${boundary}\r\nContent-Disposition: form-data` const prefix = `--${boundary}\r\nContent-Disposition: form-data`
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */ /*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
@@ -23524,6 +23750,7 @@ const {
isValidHeaderName, isValidHeaderName,
isValidHeaderValue isValidHeaderValue
} = __nccwpck_require__(2538) } = __nccwpck_require__(2538)
const util = __nccwpck_require__(3837)
const { webidl } = __nccwpck_require__(1744) const { webidl } = __nccwpck_require__(1744)
const assert = __nccwpck_require__(9491) const assert = __nccwpck_require__(9491)
@@ -24077,6 +24304,9 @@ Object.defineProperties(Headers.prototype, {
[Symbol.toStringTag]: { [Symbol.toStringTag]: {
value: 'Headers', value: 'Headers',
configurable: true configurable: true
},
[util.inspect.custom]: {
enumerable: false
} }
}) })
@@ -33253,6 +33483,20 @@ class Pool extends PoolBase {
? { ...options.interceptors } ? { ...options.interceptors }
: undefined : undefined
this[kFactory] = factory this[kFactory] = factory
this.on('connectionError', (origin, targets, error) => {
// If a connection error occurs, we remove the client from the pool,
// and emit a connectionError event. They will not be re-used.
// Fixes https://github.com/nodejs/undici/issues/3895
for (const target of targets) {
// Do not use kRemoveClient here, as it will close the client,
// but the client cannot be closed in this state.
const idx = this[kClients].indexOf(target)
if (idx !== -1) {
this[kClients].splice(idx, 1)
}
}
})
} }
[kGetDispatcher] () { [kGetDispatcher] () {
@@ -36408,6 +36652,14 @@ module.exports = require("net");
/***/ }), /***/ }),
/***/ 6005:
/***/ ((module) => {
"use strict";
module.exports = require("node:crypto");
/***/ }),
/***/ 5673: /***/ 5673:
/***/ ((module) => { /***/ ((module) => {

334
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "checkout", "name": "checkout",
"version": "4.2.2", "version": "5.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "checkout", "name": "checkout",
"version": "4.2.2", "version": "5.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
@@ -18,7 +18,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/node": "^20.12.12", "@types/node": "^24.1.0",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.9.0", "@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0", "@typescript-eslint/parser": "^7.9.0",
@@ -129,13 +129,15 @@
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.24.2", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/highlight": "^7.24.2", "@babel/helper-validator-identifier": "^7.27.1",
"picocolors": "^1.0.0" "js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -310,19 +312,21 @@
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
"version": "7.24.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-validator-identifier": { "node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@@ -337,110 +341,28 @@
} }
}, },
"node_modules/@babel/helpers": { "node_modules/@babel/helpers": {
"version": "7.24.4", "version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
"integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/template": "^7.24.0", "@babel/template": "^7.27.2",
"@babel/traverse": "^7.24.1", "@babel/types": "^7.28.2"
"@babel/types": "^7.24.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/highlight": {
"version": "7.24.2",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
"integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/highlight/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/highlight/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.24.4", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
"integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.0"
},
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
}, },
@@ -626,26 +548,25 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.24.4", "version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
"integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
"dev": true, "dev": true,
"dependencies": { "license": "MIT",
"regenerator-runtime": "^0.14.0"
},
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.24.0", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.23.5", "@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.24.0", "@babel/parser": "^7.27.2",
"@babel/types": "^7.24.0" "@babel/types": "^7.27.1"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -682,14 +603,14 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.24.0", "version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.23.4", "@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.22.20", "@babel/helper-validator-identifier": "^7.27.1"
"to-fast-properties": "^2.0.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -749,10 +670,11 @@
} }
}, },
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -808,10 +730,11 @@
} }
}, },
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -1343,9 +1266,10 @@
} }
}, },
"node_modules/@octokit/endpoint": { "node_modules/@octokit/endpoint": {
"version": "9.0.5", "version": "9.0.6",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz",
"integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^13.1.0", "@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^6.0.0"
@@ -1373,9 +1297,10 @@
"integrity": "sha512-pGUdSP+eEPfZiQHNkZI0U01HLipxncisdJQB4G//OAmfeO8sqTQ9KRa0KF03TUPCziNsoXUrTg4B2Q1EX++T0Q==" "integrity": "sha512-pGUdSP+eEPfZiQHNkZI0U01HLipxncisdJQB4G//OAmfeO8sqTQ9KRa0KF03TUPCziNsoXUrTg4B2Q1EX++T0Q=="
}, },
"node_modules/@octokit/plugin-paginate-rest": { "node_modules/@octokit/plugin-paginate-rest": {
"version": "9.2.1", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz",
"integrity": "sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==", "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^12.6.0" "@octokit/types": "^12.6.0"
}, },
@@ -1427,12 +1352,13 @@
} }
}, },
"node_modules/@octokit/request": { "node_modules/@octokit/request": {
"version": "8.4.0", "version": "8.4.1",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz",
"integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@octokit/endpoint": "^9.0.1", "@octokit/endpoint": "^9.0.6",
"@octokit/request-error": "^5.1.0", "@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.1.0", "@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^6.0.0"
}, },
@@ -1441,9 +1367,10 @@
} }
}, },
"node_modules/@octokit/request-error": { "node_modules/@octokit/request-error": {
"version": "5.1.0", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz",
"integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==",
"license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^13.1.0", "@octokit/types": "^13.1.0",
"deprecation": "^2.0.0", "deprecation": "^2.0.0",
@@ -1588,12 +1515,12 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.12.12", "version": "24.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~7.8.0"
} }
}, },
"node_modules/@types/stack-utils": { "node_modules/@types/stack-utils": {
@@ -2238,10 +2165,11 @@
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
@@ -2502,10 +2430,11 @@
} }
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
"shebang-command": "^2.0.0", "shebang-command": "^2.0.0",
@@ -3175,10 +3104,11 @@
} }
}, },
"node_modules/eslint-plugin-import/node_modules/brace-expansion": { "node_modules/eslint-plugin-import/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -3273,10 +3203,11 @@
} }
}, },
"node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -3371,10 +3302,11 @@
} }
}, },
"node_modules/eslint/node_modules/brace-expansion": { "node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -3836,10 +3768,11 @@
} }
}, },
"node_modules/glob/node_modules/brace-expansion": { "node_modules/glob/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -4635,10 +4568,11 @@
} }
}, },
"node_modules/jake/node_modules/brace-expansion": { "node_modules/jake/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -5248,7 +5182,8 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
@@ -5528,12 +5463,13 @@
} }
}, },
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.5", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"braces": "^3.0.2", "braces": "^3.0.3",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
}, },
"engines": { "engines": {
@@ -5869,10 +5805,11 @@
} }
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true "dev": true,
"license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@@ -6115,12 +6052,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.2", "version": "1.5.2",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
@@ -6622,10 +6553,11 @@
} }
}, },
"node_modules/test-exclude/node_modules/brace-expansion": { "node_modules/test-exclude/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -6655,15 +6587,6 @@
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"dev": true "dev": true
}, },
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -6930,9 +6853,10 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "5.28.4", "version": "5.29.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@fastify/busboy": "^2.0.0" "@fastify/busboy": "^2.0.0"
}, },
@@ -6941,9 +6865,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "5.26.5", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
"dev": true "dev": true
}, },
"node_modules/universal-user-agent": { "node_modules/universal-user-agent": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "checkout", "name": "checkout",
"version": "4.2.2", "version": "5.0.0",
"description": "checkout action", "description": "checkout action",
"main": "lib/main.js", "main": "lib/main.js",
"scripts": { "scripts": {
@@ -37,7 +37,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/node": "^20.12.12", "@types/node": "^24.1.0",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.9.0", "@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0", "@typescript-eslint/parser": "^7.9.0",

View File

@@ -43,6 +43,7 @@ class GitAuthHelper {
private sshKeyPath = '' private sshKeyPath = ''
private sshKnownHostsPath = '' private sshKnownHostsPath = ''
private temporaryHomePath = '' private temporaryHomePath = ''
private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP
constructor( constructor(
gitCommandManager: IGitCommandManager, gitCommandManager: IGitCommandManager,
@@ -126,16 +127,21 @@ class GitAuthHelper {
async configureGlobalAuth(): Promise<void> { async configureGlobalAuth(): Promise<void> {
// 'configureTempGlobalConfig' noops if already set, just returns the path // 'configureTempGlobalConfig' noops if already set, just returns the path
const newGitConfigPath = await this.configureTempGlobalConfig() await this.configureTempGlobalConfig()
try { try {
// Configure the token // Configure the token
await this.configureToken(newGitConfigPath, true) await this.configureToken(true)
// Configure HTTPS instead of SSH // Configure HTTPS instead of SSH
await this.git.tryConfigUnset(this.insteadOfKey, true) await this.git.tryConfigUnset(this.insteadOfKey, true)
if (!this.settings.sshKey) { if (!this.settings.sshKey) {
for (const insteadOfValue of this.insteadOfValues) { 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) { } catch (err) {
@@ -150,24 +156,60 @@ class GitAuthHelper {
async configureSubmoduleAuth(): Promise<void> { async configureSubmoduleAuth(): Promise<void> {
// Remove possible previous HTTPS instead of SSH // Remove possible previous HTTPS instead of SSH
await this.removeGitConfig(this.insteadOfKey, true) await this.removeSubmoduleGitConfig(this.insteadOfKey)
if (this.settings.persistCredentials) { if (this.settings.persistCredentials) {
// Configure a placeholder value. This approach avoids the credential being captured // Get the credentials config file path in RUNNER_TEMP
// by process creation audit events, which are commonly logged. For more information, const credentialsConfigPath = this.getCredentialsConfigPath()
// 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( // Container credentials config path
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline const containerCredentialsPath = path.posix.join(
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, '/github/runner_temp',
path.basename(credentialsConfigPath)
)
// Get submodule config file paths.
const configPaths = await this.git.getSubmoduleConfigPaths(
this.settings.nestedSubmodules this.settings.nestedSubmodules
) )
// Replace the placeholder // For each submodule, configure includeIf entries pointing to the shared credentials file.
const configPaths: string[] = // Configure both host and container paths to support Docker container actions.
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
for (const configPath of configPaths) { for (const configPath of configPaths) {
core.debug(`Replacing token placeholder in '${configPath}'`) // Submodule Git directory
await this.replaceTokenPlaceholder(configPath) 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) { 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> { private async configureSsh(): Promise<void> {
if (!this.settings.sshKey) { if (!this.settings.sshKey) {
return return
@@ -272,57 +318,116 @@ class GitAuthHelper {
} }
} }
private async configureToken( /**
configPath?: string, * Configures token-based authentication by creating a credentials config file
globalConfig?: boolean * and setting up includeIf entries to reference it.
): Promise<void> { * @param globalConfig Whether to configure global config instead of local
// Validate args */
assert.ok( private async configureToken(globalConfig?: boolean): Promise<void> {
(configPath && globalConfig) || (!configPath && !globalConfig), // Get the credentials config file path in RUNNER_TEMP
'Unexpected configureToken parameter combinations' const credentialsConfigPath = this.getCredentialsConfigPath()
)
// Default config path // Write placeholder to the separate credentials config file using git config.
if (!configPath && !globalConfig) { // This approach avoids the credential being captured by process creation audit events,
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config') // 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
// 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
await this.git.config( await this.git.config(
this.tokenConfigKey, this.tokenConfigKey,
this.tokenPlaceholderConfigValue, this.tokenPlaceholderConfigValue,
globalConfig false, // globalConfig?
false, // add?
credentialsConfigPath
) )
// Replace the placeholder // Replace the placeholder in the credentials config file
await this.replaceTokenPlaceholder(configPath || '') let content = (await fs.promises.readFile(credentialsConfigPath)).toString()
}
private async replaceTokenPlaceholder(configPath: string): Promise<void> {
assert.ok(configPath, 'configPath is not defined')
let content = (await fs.promises.readFile(configPath)).toString()
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue) const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
if ( if (
placeholderIndex < 0 || placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue) 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') assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
content = content.replace( content = content.replace(
this.tokenPlaceholderConfigValue, this.tokenPlaceholderConfigValue,
this.tokenConfigValue 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> { private async removeSsh(): Promise<void> {
// SSH key // SSH key
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
if (keyPath) { if (keyPath) {
try { try {
core.info(`Removing SSH key '${keyPath}'`)
await io.rmRF(keyPath) await io.rmRF(keyPath)
} catch (err) { } catch (err) {
core.debug(`${(err as any)?.message ?? err}`) core.debug(`${(err as any)?.message ?? err}`)
@@ -335,40 +440,149 @@ class GitAuthHelper {
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
if (knownHostsPath) { if (knownHostsPath) {
try { try {
core.info(`Removing SSH known hosts '${knownHostsPath}'`)
await io.rmRF(knownHostsPath) await io.rmRF(knownHostsPath)
} catch { } catch (err) {
// Intentionally empty core.debug(`${(err as any)?.message ?? err}`)
core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`)
} }
} }
// SSH command // SSH command
core.info('Removing SSH command configuration')
await this.removeGitConfig(SSH_COMMAND_KEY) 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> { 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.removeGitConfig(this.tokenConfigKey)
} await this.removeSubmoduleGitConfig(this.tokenConfigKey)
private async removeGitConfig( // Collect credentials config paths that need to be removed
configKey: string, const credentialsPaths = new Set<string>()
submoduleOnly: boolean = false
): Promise<void> { // Remove includeIf entries that point to git-credentials-*.config files
if (!submoduleOnly) { core.info('Removing includeIf entries pointing to credentials config files')
if ( const mainCredentialsPaths = await this.removeIncludeIfCredentials()
(await this.git.configExists(configKey)) && mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
!(await this.git.tryConfigUnset(configKey))
) { // Remove submodule includeIf entries that point to git-credentials-*.config files
// Load the config contents const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true)
core.warning(`Failed to remove '${configKey}' from the git config`) for (const configPath of submoduleConfigPaths) {
} const submoduleCredentialsPaths =
await 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}'`)
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) const pattern = regexpHelper.escape(configKey)
await this.git.submoduleForeach( 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}' || :"`, `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
true 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
*/
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)
}
} }

View File

@@ -28,7 +28,8 @@ export interface IGitCommandManager {
configKey: string, configKey: string,
configValue: string, configValue: string,
globalConfig?: boolean, globalConfig?: boolean,
add?: boolean add?: boolean,
configFile?: string
): Promise<void> ): Promise<void>
configExists(configKey: string, globalConfig?: boolean): Promise<boolean> configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
fetch( fetch(
@@ -41,6 +42,7 @@ export interface IGitCommandManager {
} }
): Promise<void> ): Promise<void>
getDefaultBranch(repositoryUrl: string): Promise<string> getDefaultBranch(repositoryUrl: string): Promise<string>
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
getWorkingDirectory(): string getWorkingDirectory(): string
init(): Promise<void> init(): Promise<void>
isDetached(): Promise<boolean> isDetached(): Promise<boolean>
@@ -59,8 +61,24 @@ export interface IGitCommandManager {
tagExists(pattern: string): Promise<boolean> tagExists(pattern: string): Promise<boolean>
tryClean(): Promise<boolean> tryClean(): Promise<boolean>
tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean> tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
tryConfigUnsetValue(
configKey: string,
configValue: string,
globalConfig?: boolean,
configFile?: string
): Promise<boolean>
tryDisableAutomaticGarbageCollection(): Promise<boolean> tryDisableAutomaticGarbageCollection(): Promise<boolean>
tryGetFetchUrl(): Promise<string> tryGetFetchUrl(): Promise<string>
tryGetConfigValues(
configKey: string,
globalConfig?: boolean,
configFile?: string
): Promise<string[]>
tryGetConfigKeys(
pattern: string,
globalConfig?: boolean,
configFile?: string
): Promise<string[]>
tryReset(): Promise<boolean> tryReset(): Promise<boolean>
version(): Promise<GitVersion> version(): Promise<GitVersion>
} }
@@ -223,9 +241,15 @@ class GitCommandManager {
configKey: string, configKey: string,
configValue: string, configValue: string,
globalConfig?: boolean, globalConfig?: boolean,
add?: boolean add?: boolean,
configFile?: string
): Promise<void> { ): 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) { if (add) {
args.push('--add') args.push('--add')
} }
@@ -323,6 +347,21 @@ class GitCommandManager {
throw new Error('Unexpected output when retrieving default branch') 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 { getWorkingDirectory(): string {
return this.workingDirectory return this.workingDirectory
} }
@@ -455,6 +494,24 @@ class GitCommandManager {
return output.exitCode === 0 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> { async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
const output = await this.execGit( const output = await this.execGit(
['config', '--local', 'gc.auto', '0'], ['config', '--local', 'gc.auto', '0'],
@@ -481,6 +538,56 @@ class GitCommandManager {
return stdout 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> { async tryReset(): Promise<boolean> {
const output = await this.execGit(['reset', '--hard', 'HEAD'], true) const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
return output.exitCode === 0 return output.exitCode === 0

View File

@@ -120,7 +120,7 @@ function updateUsage(
} }
updateUsage( updateUsage(
'actions/checkout@v4', 'actions/checkout@v5',
path.join(__dirname, '..', '..', 'action.yml'), path.join(__dirname, '..', '..', 'action.yml'),
path.join(__dirname, '..', '..', 'README.md') path.join(__dirname, '..', '..', 'README.md')
) )