1
0
mirror of https://github.com/actions/checkout.git synced 2026-06-20 17:34:08 +08:00

Compare commits

..

3 Commits

Author SHA1 Message Date
eric sciple
1af3b93b68 update readme/changelog for v6 (#2311) 2025-11-20 10:20:04 -06:00
eric sciple
71cf2267d8 v6-beta (#2298) 2025-11-03 13:40:10 -06:00
eric sciple
069c695914 Persist creds to a separate file (#2286) 2025-11-03 13:08:38 -06:00
13 changed files with 1078 additions and 598 deletions

View File

@@ -1,234 +0,0 @@
- name: Setup Java JDK
uses: actions/setup-java@v5.0.0
with:
# The Java version to set up. Takes a whole or semver Java version. See examples of supported syntax in README file
java-version: # optional
# The path to the `.java-version` file. See examples of supported syntax in README file
java-version-file: # optional
# Java distribution. See the list of supported distributions in README file
distribution:
# The package type (jdk, jre, jdk+fx, jre+fx)
java-package: # optional, default is jdk
# The architecture of the package (defaults to the action runner's architecture)
architecture: # optional
# Path to where the compressed JDK is located
jdkFile: # optional
# Set this option if you want the action to check for the latest available version that satisfies the version spec
check-latest: # optional
# ID of the distributionManagement repository in the pom.xml file. Default is `github`
server-id: # optional, default is github
# Environment variable name for the username for authentication to the Apache Maven repository. Default is $GITHUB_ACTOR
server-username: # optional, default is GITHUB_ACTOR
# Environment variable name for password or token for authentication to the Apache Maven repository. Default is $GITHUB_TOKEN
server-password: # optional, default is GITHUB_TOKEN
# Path to where the settings.xml file will be written. Default is ~/.m2.
settings-path: # optional
# Overwrite the settings.xml file if it exists. Default is "true".
overwrite-settings: # optional, default is true
# GPG private key to import. Default is empty string.
gpg-private-key: # optional
# Environment variable name for the GPG private key passphrase. Default is $GPG_PASSPHRASE.
gpg-passphrase: # optional
# Name of the build platform to cache dependencies. It can be "maven", "gradle" or "sbt".
cache: # optional
# The path to a dependency file: pom.xml, build.gradle, build.sbt, etc. This option can be used with the `cache` option. If this option is omitted, the action searches for the dependency file in the entire repository. This option supports wildcards and a list of file names for caching multiple dependencies.
cache-dependency-path: # optional
# Workaround to pass job status to post job step. This variable is not intended for manual setting
job-status: # optional, default is ${{ job.status }}
# The token used to authenticate when fetching version manifests hosted on github.com, such as for the Microsoft Build of OpenJDK. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting.
token: # optional, default is ${{ github.server_url == 'https://github.com' && github.token || '' }}
# Name of Maven Toolchain ID if the default name of "${distribution}_${java-version}" is not wanted. See examples of supported syntax in Advanced Usage file
mvn-toolchain-id: # optional
# Name of Maven Toolchain Vendor if the default name of "${distribution}" is not wanted. See examples of supported syntax in Advanced Usage file
mvn-toolchain-vendor: # optional
- name: Setup Java JDK
uses: actions/setup-java@v5.0.0
with:
# The Java version to set up. Takes a whole or semver Java version. See examples of supported syntax in README file
java-version: # optional
# The path to the `.java-version` file. See examples of supported syntax in README file
java-version-file: # optional
# Java distribution. See the list of supported distributions in README file
distribution:
# The package type (jdk, jre, jdk+fx, jre+fx)
java-package: # optional, default is jdk
# The architecture of the package (defaults to the action runner's architecture)
architecture: # optional
# Path to where the compressed JDK is located
jdkFile: # optional
# Set this option if you want the action to check for the latest available version that satisfies the version spec
check-latest: # optional
# ID of the distributionManagement repository in the pom.xml file. Default is `github`
server-id: # optional, default is github
# Environment variable name for the username for authentication to the Apache Maven repository. Default is $GITHUB_ACTOR
server-username: # optional, default is GITHUB_ACTOR
# Environment variable name for password or token for authentication to the Apache Maven repository. Default is $GITHUB_TOKEN
server-password: # optional, default is GITHUB_TOKEN
# Path to where the settings.xml file will be written. Default is ~/.m2.
settings-path: # optional
# Overwrite the settings.xml file if it exists. Default is "true".
overwrite-settings: # optional, default is true
# GPG private key to import. Default is empty string.
gpg-private-key: # optional
# Environment variable name for the GPG private key passphrase. Default is $GPG_PASSPHRASE.
gpg-passphrase: # optional
# Name of the build platform to cache dependencies. It can be "maven", "gradle" or "sbt".
cache: # optional
# The path to a dependency file: pom.xml, build.gradle, build.sbt, etc. This option can be used with the `cache` option. If this option is omitted, the action searches for the dependency file in the entire repository. This option supports wildcards and a list of file names for caching multiple dependencies.
cache-dependency-path: # optional
# Workaround to pass job status to post job step. This variable is not intended for manual setting
job-status: # optional, default is ${{ job.status }}
# The token used to authenticate when fetching version manifests hosted on github.com, such as for the Microsoft Build of OpenJDK. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting.
token: # optional, default is ${{ github.server_url == 'https://github.com' && github.token || '' }}
# Name of Maven Toolchain ID if the default name of "${distribution}_${java-version}" is not wanted. See examples of supported syntax in Advanced Usage file
mvn-toolchain-id: # optional
# Name of Maven Toolchain Vendor if the default name of "${distribution}" is not wanted. See examples of supported syntax in Advanced Usage file
mvn-toolchain-vendor: # optional
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v5.0.0
with:
# Optional SDK version(s) to use. If not provided, will install global.json version when available. Examples: 2.2.104, 3.1, 3.1.x, 3.x, 6.0.2xx
dotnet-version: # optional
# Optional quality of the build. The possible values are: daily, signed, validated, preview, ga.
dotnet-quality: # optional
# Optional global.json location, if your global.json isn't located in the root of the repo.
global-json-file: # optional
# Optional package source for which to set up authentication. Will consult any existing NuGet.config in the root of the repo and provide a temporary NuGet.config using the NUGET_AUTH_TOKEN environment variable as a ClearTextPassword
source-url: # optional
# Optional OWNER for using packages from GitHub Package Registry organizations/users other than the current repository's owner. Only used if a GPR URL is also provided in source-url
owner: # optional
# Optional NuGet.config location, if your NuGet.config isn't located in the root of the repo.
config-file: # optional
# Optional input to enable caching of the NuGet global-packages folder
cache: # optional
# Used to specify the path to a dependency file: packages.lock.json. Supports wildcards or a list of file names for caching multiple dependencies.
cache-dependency-path: # optional
- name: Custom Models Action
# You may pin to the exact commit or the version.
# uses: datarobot-oss/custom-models-action@0a5d55659824973261d6b49389569dd4e28795bf
uses: datarobot-oss/custom-models-action@v1.6.0
with:
# DataRobot authentication API token.
api-token:
# DataRobot frontend web server.
webserver:
# The branch for which pull request and push events will trigger the action.
branch:
# Determines the namespace under which models and deployments will be created, updated and
deleted.
namespace: # optional
# Whether to detected local deleted model definitions and consequently delete them
in DataRobot.
allow-model-deletion: # optional, default is false
# Whether to detect local deleted deployment definitions and consequently delete them
in DataRobot.
allow-deployment-deletion: # optional, default is false
# Whether to handle custom inference models only, without deployments..
models-only: # optional, default is false
# Whether a request to an HTTPS URL will be made without a certificate verification.
skip-cert-verification: # optional, default is false
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '24 19 * * 1'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@@ -1,105 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow lets you generate SLSA provenance file for your project.
# The generation satisfies level 3 for the provenance requirements - see https://slsa.dev/spec/v0.1/requirements
# The project is an initiative of the OpenSSF (openssf.org) and is developed at
# https://github.com/slsa-framework/slsa-github-generator.
# The provenance file can be verified using https://github.com/slsa-framework/slsa-verifier.
# For more information about SLSA and how it improves the supply-chain, visit slsa.dev.
name: SLSA generic generator
on:
workflow_dispatch:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
outputs:
digests: ${{ steps.hash.outputs.digests }}
steps:
- uses: actions/checkout@v4
# ========================================================
#
# Step 1: Build your artifacts.
#
# ========================================================
- name: Build artifacts
run: |
# These are some amazing artifacts.
echo "artifact1" > artifact1
echo "artifact2" > artifact2
# ========================================================
#
# Step 2: Add a step to generate the provenance subjects
# as shown below. Update the sha256 sum arguments
# to include all binaries that you generate
# provenance for.
#
# ========================================================
- name: Generate subject for provenance
id: hash
run: |
set -euo pipefail
# List the artifacts the provenance will refer to.
files=$(ls artifact*)
# Generate the subjects (base64 encoded).
echo "hashes=$(sha256sum $files | base64 -w0)" >> "${GITHUB_OUTPUT}"
provenance:
needs: [build]
permissions: - name: Deploy HelmFile
# You may pin to the exact commit or the version.
# uses: cloudposse/github-action-deploy-helmfile@fcc0ea83519505047bd34a4e017f1d0c3516a5cc
uses: cloudposse/github-action-deploy-helmfile@0.7.0
with:
# Cluster name
cluster:
# AWS region
aws-region: # optional, default is us-east-1
# The path where lives the helmfile.
helmfile-path: # optional, default is deploy
# Helmfile name
helmfile: # optional, default is helmfile.yaml
# Operation with helmfiles. (valid options - `deploy`, `destroy`)
operation: # default is deploy
# Helmfile environment
environment: # optional, default is preview
# Git SHA
gitref-sha: # optional, default is
# Kubernetes namespace
namespace:
# Docker image
image:
# Docker image tag
image-tag:
# Debug mode
debug: # optional, default is false
# The name of the label used to describe the helm release
release_label_name: # optional, default is release
# YAML string with extra values to use in a helmfile deploy
values_yaml: # optional
# Helm version
helm_version: # optional, default is 3.11.1
# Helmfile version
helmfile_version: # optional, default is 0.143.5
# Kubectl version
kubectl_version: # optional, default is 1.26.3
# Kubectl version
chamber_version: # optional, default is 2.11.1
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0
with:
base64-subjects: "${{ needs.build.outputs.digests }}"
upload-assets: true # Optional: Upload to a new release

View File

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

View File

@@ -1,8 +1,17 @@
# Changelog
## V6.0.0
* Persist creds to a separate file by @ericsciple in https://github.com/actions/checkout/pull/2286
* Update README to include Node.js 24 support details and requirements by @salmanmkc in https://github.com/actions/checkout/pull/2248
## V5.0.1
* Port v6 cleanup to v5 by @ericsciple in https://github.com/actions/checkout/pull/2301
## V5.0.0
* Update actions checkout to use node 24 by @salmanmkc in https://github.com/actions/checkout/pull/2226
## V4.3.1
* Port v6 cleanup to v4 by @ericsciple in https://github.com/actions/checkout/pull/2305
## V4.3.0
* docs: update README.md by @motss in https://github.com/actions/checkout/pull/1971

View File

@@ -1,6 +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)
# Checkout V5
# Checkout v6
## What's new
- Updated `persist-credentials` to store the credentials under `$RUNNER_TEMP` instead of directly in the local git config.
- This requires a minimum Actions Runner version of [v2.329.0](https://github.com/actions/runner/releases/tag/v2.329.0) to access the persisted credentials for [Docker container action](https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action) scenarios.
# Checkout v5
## What's new
@@ -8,7 +15,7 @@
- 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.

View File

@@ -86,16 +86,29 @@ describe('git-auth-helper tests', () => {
// Act
await authHelper.configureAuth()
// Assert config
const configContent = (
// Assert config - check that .git/config contains includeIf entries
const localConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
expect(
localConfigContent.indexOf('includeIf.gitdir:')
).toBeGreaterThanOrEqual(0)
// Assert credentials config file contains the actual credentials
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
f => f.startsWith('git-credentials-') && f.endsWith('.config')
)
expect(credentialsFiles.length).toBe(1)
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
const credentialsContent = (
await fs.promises.readFile(credentialsConfigPath)
).toString()
const basicCredential = Buffer.from(
`x-access-token:${settings.authToken}`,
'utf8'
).toString('base64')
expect(
configContent.indexOf(
credentialsContent.indexOf(
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
@@ -120,7 +133,7 @@ describe('git-auth-helper tests', () => {
'inject https://github.com as github server url'
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
await testAuthHeader(
configureAuth_AcceptsGitHubServerUrl,
configureAuth_AcceptsGitHubServerUrlSetToGHEC,
'https://github.com'
)
})
@@ -141,12 +154,17 @@ describe('git-auth-helper tests', () => {
// Act
await authHelper.configureAuth()
// Assert config
const configContent = (
await fs.promises.readFile(localGitConfigPath)
// Assert config - check credentials config file (not local .git/config)
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
f => f.startsWith('git-credentials-') && f.endsWith('.config')
)
expect(credentialsFiles.length).toBe(1)
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
const credentialsContent = (
await fs.promises.readFile(credentialsConfigPath)
).toString()
expect(
configContent.indexOf(
credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION`
)
).toBeGreaterThanOrEqual(0)
@@ -251,13 +269,16 @@ describe('git-auth-helper tests', () => {
expectedSshCommand
)
// Asserty git config
// Assert git config
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
.toString()
.split('\n')
.filter(x => x)
expect(gitConfigLines).toHaveLength(1)
expect(gitConfigLines[0]).toMatch(/^http\./)
// Should have includeIf entries pointing to credentials file
expect(gitConfigLines.length).toBeGreaterThan(0)
expect(
gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0)
).toBeTruthy()
})
const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
@@ -419,8 +440,20 @@ describe('git-auth-helper tests', () => {
expect(
configContent.indexOf('value-from-global-config')
).toBeGreaterThanOrEqual(0)
// Global config should have include.path pointing to credentials file
expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
// Check credentials in the separate config file
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
f => f.startsWith('git-credentials-') && f.endsWith('.config')
)
expect(credentialsFiles.length).toBeGreaterThan(0)
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
const credentialsContent = (
await fs.promises.readFile(credentialsConfigPath)
).toString()
expect(
configContent.indexOf(
credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
@@ -463,8 +496,20 @@ describe('git-auth-helper tests', () => {
const configContent = (
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
).toString()
// Global config should have include.path pointing to credentials file
expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
// Check credentials in the separate config file
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
f => f.startsWith('git-credentials-') && f.endsWith('.config')
)
expect(credentialsFiles.length).toBeGreaterThan(0)
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
const credentialsContent = (
await fs.promises.readFile(credentialsConfigPath)
).toString()
expect(
configContent.indexOf(
credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
@@ -550,15 +595,15 @@ describe('git-auth-helper tests', () => {
await authHelper.configureSubmoduleAuth()
// Assert
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
// Should configure insteadOf (2 calls for two values)
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(
/url.*insteadOf.*git@github.com:/
)
expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
/url.*insteadOf.*org-123456@github.com:/
)
}
@@ -589,12 +634,12 @@ describe('git-auth-helper tests', () => {
await authHelper.configureSubmoduleAuth()
// Assert
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
// Should configure sshCommand (1 call)
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/)
}
)
@@ -660,19 +705,201 @@ describe('git-auth-helper tests', () => {
await setup(removeAuth_removesToken)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
let gitConfigContent = (
// Verify includeIf entries exist in local config
let localConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
expect(
localConfigContent.indexOf('includeIf.gitdir:')
).toBeGreaterThanOrEqual(0)
// Verify both host and container includeIf entries are present
const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
expect(
localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
).toBeGreaterThanOrEqual(0)
expect(
localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
).toBeGreaterThanOrEqual(0)
// Verify credentials file exists
let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
f => f.startsWith('git-credentials-') && f.endsWith('.config')
)
expect(credentialsFiles.length).toBe(1)
const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
// Verify credentials file contains the auth token
let credentialsContent = (
await fs.promises.readFile(credentialsFilePath)
).toString()
const basicCredential = Buffer.from(
`x-access-token:${settings.authToken}`,
'utf8'
).toString('base64')
expect(
credentialsContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
// Verify the includeIf entries point to the credentials file
const containerCredentialsPath = path.posix.join(
'/github/runner_temp',
path.basename(credentialsFilePath)
)
expect(
localConfigContent.indexOf(credentialsFilePath)
).toBeGreaterThanOrEqual(0)
expect(
localConfigContent.indexOf(containerCredentialsPath)
).toBeGreaterThanOrEqual(0)
// Act
await authHelper.removeAuth()
// Assert git config
gitConfigContent = (
// Assert all includeIf entries removed from local git config
localConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0)
expect(
localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
).toBeLessThan(0)
expect(
localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
).toBeLessThan(0)
expect(localConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
expect(localConfigContent.indexOf(containerCredentialsPath)).toBeLessThan(0)
// Assert credentials config file deleted
credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
f => f.startsWith('git-credentials-') && f.endsWith('.config')
)
expect(credentialsFiles.length).toBe(0)
// Verify credentials file no longer exists on disk
try {
await fs.promises.stat(credentialsFilePath)
throw new Error('Credentials file should have been deleted')
} catch (err) {
if ((err as any)?.code !== 'ENOENT') {
throw err
}
}
})
const removeAuth_removesTokenFromSubmodules =
'removeAuth removes token from submodules'
it(removeAuth_removesTokenFromSubmodules, async () => {
// Arrange
await setup(removeAuth_removesTokenFromSubmodules)
// Create fake submodule config paths
const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
const submodule2Dir = path.join(workspace, '.git', 'modules', 'submodule-2')
const submodule1ConfigPath = path.join(submodule1Dir, 'config')
const submodule2ConfigPath = path.join(submodule2Dir, 'config')
await fs.promises.mkdir(submodule1Dir, {recursive: true})
await fs.promises.mkdir(submodule2Dir, {recursive: true})
await fs.promises.writeFile(submodule1ConfigPath, '')
await fs.promises.writeFile(submodule2ConfigPath, '')
// Mock getSubmoduleConfigPaths to return our fake submodules (for both configure and remove)
const mockGetSubmoduleConfigPaths =
git.getSubmoduleConfigPaths as jest.Mock<any, any>
mockGetSubmoduleConfigPaths.mockResolvedValue([
submodule1ConfigPath,
submodule2ConfigPath
])
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
await authHelper.configureSubmoduleAuth()
// Verify credentials file exists
let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
f => f.startsWith('git-credentials-') && f.endsWith('.config')
)
expect(credentialsFiles.length).toBe(1)
const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
// Verify submodule 1 config has includeIf entries
let submodule1Content = (
await fs.promises.readFile(submodule1ConfigPath)
).toString()
const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
expect(
submodule1Content.indexOf(`includeIf.gitdir:${submodule1GitDir}.path`)
).toBeGreaterThanOrEqual(0)
expect(
submodule1Content.indexOf(credentialsFilePath)
).toBeGreaterThanOrEqual(0)
// Verify submodule 2 config has includeIf entries
let submodule2Content = (
await fs.promises.readFile(submodule2ConfigPath)
).toString()
const submodule2GitDir = submodule2Dir.replace(/\\/g, '/')
expect(
submodule2Content.indexOf(`includeIf.gitdir:${submodule2GitDir}.path`)
).toBeGreaterThanOrEqual(0)
expect(
submodule2Content.indexOf(credentialsFilePath)
).toBeGreaterThanOrEqual(0)
// Verify both host and container paths are in each submodule config
const containerCredentialsPath = path.posix.join(
'/github/runner_temp',
path.basename(credentialsFilePath)
)
expect(
submodule1Content.indexOf(containerCredentialsPath)
).toBeGreaterThanOrEqual(0)
expect(
submodule2Content.indexOf(containerCredentialsPath)
).toBeGreaterThanOrEqual(0)
// Act - ensure mock persists for removeAuth
mockGetSubmoduleConfigPaths.mockResolvedValue([
submodule1ConfigPath,
submodule2ConfigPath
])
await authHelper.removeAuth()
// Assert submodule 1 includeIf entries removed
submodule1Content = (
await fs.promises.readFile(submodule1ConfigPath)
).toString()
expect(submodule1Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
expect(submodule1Content.indexOf(credentialsFilePath)).toBeLessThan(0)
expect(submodule1Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
// Assert submodule 2 includeIf entries removed
submodule2Content = (
await fs.promises.readFile(submodule2ConfigPath)
).toString()
expect(submodule2Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
expect(submodule2Content.indexOf(credentialsFilePath)).toBeLessThan(0)
expect(submodule2Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
// Assert credentials config file deleted
credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
f => f.startsWith('git-credentials-') && f.endsWith('.config')
)
expect(credentialsFiles.length).toBe(0)
// Verify credentials file no longer exists on disk
try {
await fs.promises.stat(credentialsFilePath)
throw new Error('Credentials file should have been deleted')
} catch (err) {
if ((err as any)?.code !== 'ENOENT') {
throw err
}
}
})
const removeGlobalConfig_removesOverride =
@@ -701,6 +928,52 @@ describe('git-auth-helper tests', () => {
}
}
})
const testCredentialsConfigPath_matchesCredentialsConfigPaths =
'testCredentialsConfigPath matches credentials config paths'
it(testCredentialsConfigPath_matchesCredentialsConfigPaths, async () => {
// Arrange
await setup(testCredentialsConfigPath_matchesCredentialsConfigPaths)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Get a real credentials config path
const credentialsConfigPath = await (
authHelper as any
).getCredentialsConfigPath()
// Act & Assert
expect(
(authHelper as any).testCredentialsConfigPath(credentialsConfigPath)
).toBe(true)
expect(
(authHelper as any).testCredentialsConfigPath(
'/some/path/git-credentials-12345678-abcd-1234-5678-123456789012.config'
)
).toBe(true)
expect(
(authHelper as any).testCredentialsConfigPath(
'/some/path/git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
)
).toBe(true)
// Test invalid paths
expect(
(authHelper as any).testCredentialsConfigPath(
'/some/path/other-config.config'
)
).toBe(false)
expect(
(authHelper as any).testCredentialsConfigPath(
'/some/path/git-credentials-invalid.config'
)
).toBe(false)
expect(
(authHelper as any).testCredentialsConfigPath(
'/some/path/git-credentials-.config'
)
).toBe(false)
expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
})
})
async function setup(testName: string): Promise<void> {
@@ -715,6 +988,7 @@ async function setup(testName: string): Promise<void> {
await fs.promises.mkdir(tempHomedir, {recursive: true})
process.env['RUNNER_TEMP'] = runnerTemp
process.env['HOME'] = tempHomedir
process.env['GITHUB_WORKSPACE'] = workspace
// Create git config
globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
@@ -733,10 +1007,20 @@ async function setup(testName: string): Promise<void> {
checkout: jest.fn(),
checkoutDetach: jest.fn(),
config: jest.fn(
async (key: string, value: string, globalConfig?: boolean) => {
const configPath = globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath
async (
key: string,
value: string,
globalConfig?: boolean,
add?: boolean,
configFile?: string
) => {
const configPath =
configFile ||
(globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath)
// Ensure directory exists
await fs.promises.mkdir(path.dirname(configPath), {recursive: true})
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
}
),
@@ -756,6 +1040,7 @@ async function setup(testName: string): Promise<void> {
env: {},
fetch: jest.fn(),
getDefaultBranch: jest.fn(),
getSubmoduleConfigPaths: jest.fn(async () => []),
getWorkingDirectory: jest.fn(() => workspace),
init: jest.fn(),
isDetached: jest.fn(),
@@ -794,8 +1079,72 @@ async function setup(testName: string): Promise<void> {
return true
}
),
tryConfigUnsetValue: jest.fn(
async (
key: string,
value: string,
globalConfig?: boolean,
configPath?: string
): Promise<boolean> => {
const targetConfigPath =
configPath ||
(globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath)
let content = await fs.promises.readFile(targetConfigPath)
let lines = content
.toString()
.split('\n')
.filter(x => x)
.filter(x => !(x.startsWith(key) && x.includes(value)))
await fs.promises.writeFile(targetConfigPath, lines.join('\n'))
return true
}
),
tryDisableAutomaticGarbageCollection: jest.fn(),
tryGetFetchUrl: jest.fn(),
tryGetConfigValues: jest.fn(
async (
key: string,
globalConfig?: boolean,
configPath?: string
): Promise<string[]> => {
const targetConfigPath =
configPath ||
(globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath)
const content = await fs.promises.readFile(targetConfigPath)
const lines = content
.toString()
.split('\n')
.filter(x => x && x.startsWith(key))
.map(x => x.substring(key.length).trim())
return lines
}
),
tryGetConfigKeys: jest.fn(
async (
pattern: string,
globalConfig?: boolean,
configPath?: string
): Promise<string[]> => {
const targetConfigPath =
configPath ||
(globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath)
const content = await fs.promises.readFile(targetConfigPath)
const lines = content
.toString()
.split('\n')
.filter(x => x)
const keys = lines
.filter(x => new RegExp(pattern).test(x.split(' ')[0]))
.map(x => x.split(' ')[0])
return [...new Set(keys)] // Remove duplicates
}
),
tryReset: jest.fn(),
version: jest.fn()
}
@@ -830,6 +1179,7 @@ async function setup(testName: string): Promise<void> {
async function getActualSshKeyPath(): Promise<string> {
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
.filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
.sort()
.map(x => path.join(runnerTemp, x))
if (actualTempFiles.length === 0) {
@@ -843,6 +1193,7 @@ async function getActualSshKeyPath(): Promise<string> {
async function getActualSshKnownHostsPath(): Promise<string> {
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
.filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
.sort()
.map(x => path.join(runnerTemp, x))
if (actualTempFiles.length === 0) {

View File

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

View File

@@ -17,7 +17,7 @@ fi
echo "Testing persisted credential"
pushd ./submodules-recursive/submodule-level-1/submodule-level-2
git config --local --name-only --get-regexp http.+extraheader && git fetch
git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
if [ "$?" != "0" ]; then
echo "Failed to validate persisted credential"
popd

View File

@@ -17,7 +17,7 @@ fi
echo "Testing persisted credential"
pushd ./submodules-true/submodule-level-1
git config --local --name-only --get-regexp http.+extraheader && git fetch
git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
if [ "$?" != "0" ]; then
echo "Failed to validate persisted credential"
popd

338
dist/index.js vendored
View File

@@ -162,6 +162,7 @@ class GitAuthHelper {
this.sshKeyPath = '';
this.sshKnownHostsPath = '';
this.temporaryHomePath = '';
this.credentialsConfigPath = ''; // Path to separate credentials config file in RUNNER_TEMP
this.git = gitCommandManager;
this.settings = gitSourceSettings || {};
// Token auth header
@@ -229,15 +230,17 @@ class GitAuthHelper {
configureGlobalAuth() {
return __awaiter(this, void 0, void 0, function* () {
// 'configureTempGlobalConfig' noops if already set, just returns the path
const newGitConfigPath = yield this.configureTempGlobalConfig();
yield this.configureTempGlobalConfig();
try {
// Configure the token
yield this.configureToken(newGitConfigPath, true);
yield this.configureToken(true);
// Configure HTTPS instead of SSH
yield this.git.tryConfigUnset(this.insteadOfKey, true);
if (!this.settings.sshKey) {
for (const insteadOfValue of this.insteadOfValues) {
yield this.git.config(this.insteadOfKey, insteadOfValue, true, true);
yield this.git.config(this.insteadOfKey, insteadOfValue, true, // globalConfig?
true // add?
);
}
}
}
@@ -252,19 +255,34 @@ class GitAuthHelper {
configureSubmoduleAuth() {
return __awaiter(this, void 0, void 0, function* () {
// Remove possible previous HTTPS instead of SSH
yield this.removeGitConfig(this.insteadOfKey, true);
yield this.removeSubmoduleGitConfig(this.insteadOfKey);
if (this.settings.persistCredentials) {
// Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
const output = yield this.git.submoduleForeach(
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules);
// Replace the placeholder
const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
// Get the credentials config file path in RUNNER_TEMP
const credentialsConfigPath = this.getCredentialsConfigPath();
// Container credentials config path
const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
// Get submodule config file paths.
const configPaths = yield this.git.getSubmoduleConfigPaths(this.settings.nestedSubmodules);
// For each submodule, configure includeIf entries pointing to the shared credentials file.
// Configure both host and container paths to support Docker container actions.
for (const configPath of configPaths) {
core.debug(`Replacing token placeholder in '${configPath}'`);
yield this.replaceTokenPlaceholder(configPath);
// Submodule Git directory
let submoduleGitDir = path.dirname(configPath); // The config file is at .git/modules/submodule-name/config
submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
// Configure host includeIf
yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig?
false, // add?
configPath);
// Container submodule git directory
const githubWorkspace = process.env['GITHUB_WORKSPACE'];
assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined');
let relativeSubmoduleGitDir = path.relative(githubWorkspace, submoduleGitDir);
relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir);
// Configure container includeIf
yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig?
false, // add?
configPath);
}
if (this.settings.sshKey) {
// Configure core.sshCommand
@@ -295,6 +313,10 @@ class GitAuthHelper {
}
});
}
/**
* Configures SSH authentication by writing the SSH key and known hosts,
* and setting up the GIT_SSH_COMMAND environment variable.
*/
configureSsh() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.settings.sshKey) {
@@ -351,43 +373,88 @@ class GitAuthHelper {
}
});
}
configureToken(configPath, globalConfig) {
/**
* 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* () {
// Validate args
assert.ok((configPath && globalConfig) || (!configPath && !globalConfig), 'Unexpected configureToken parameter combinations');
// Default config path
if (!configPath && !globalConfig) {
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config');
}
// Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig);
// Replace the placeholder
yield this.replaceTokenPlaceholder(configPath || '');
});
}
replaceTokenPlaceholder(configPath) {
return __awaiter(this, void 0, void 0, function* () {
assert.ok(configPath, 'configPath is not defined');
let content = (yield fs.promises.readFile(configPath)).toString();
// Get the credentials config file path in RUNNER_TEMP
const credentialsConfigPath = this.getCredentialsConfigPath();
// Write placeholder to the separate credentials config file using git config.
// This approach avoids the credential being captured by process creation audit events,
// which are commonly logged. For more information, refer to
// https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, false, // globalConfig?
false, // add?
credentialsConfigPath);
// Replace the placeholder in the credentials config file
let content = (yield fs.promises.readFile(credentialsConfigPath)).toString();
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue);
if (placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) {
throw new Error(`Unable to replace auth placeholder in ${configPath}`);
throw new Error(`Unable to replace auth placeholder in ${credentialsConfigPath}`);
}
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined');
content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue);
yield fs.promises.writeFile(configPath, content);
yield fs.promises.writeFile(credentialsConfigPath, content);
// Add include or includeIf to reference the credentials config
if (globalConfig) {
// Global config file is temporary
yield this.git.config('include.path', credentialsConfigPath, true // globalConfig?
);
}
else {
// Host git directory
let gitDir = path.join(this.git.getWorkingDirectory(), '.git');
gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
// Configure host includeIf
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`;
yield this.git.config(hostIncludeKey, credentialsConfigPath);
// Container git directory
const workingDirectory = this.git.getWorkingDirectory();
const githubWorkspace = process.env['GITHUB_WORKSPACE'];
assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined');
let relativePath = path.relative(githubWorkspace, workingDirectory);
relativePath = relativePath.replace(/\\/g, '/'); // Use forward slashes, even on Windows
const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git');
// Container credentials config path
const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
// Configure container includeIf
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`;
yield this.git.config(containerIncludeKey, containerCredentialsPath);
}
});
}
/**
* Gets or creates the path to the credentials config file in RUNNER_TEMP.
* @returns The absolute path to the credentials config file
*/
getCredentialsConfigPath() {
if (this.credentialsConfigPath) {
return this.credentialsConfigPath;
}
const runnerTemp = process.env['RUNNER_TEMP'] || '';
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
// Create a unique filename for this checkout instance
const configFileName = `git-credentials-${(0, uuid_1.v4)()}.config`;
this.credentialsConfigPath = path.join(runnerTemp, configFileName);
core.debug(`Credentials config path: ${this.credentialsConfigPath}`);
return this.credentialsConfigPath;
}
/**
* Removes SSH authentication configuration by cleaning up SSH keys,
* known hosts files, and SSH command configurations.
*/
removeSsh() {
return __awaiter(this, void 0, void 0, function* () {
var _a;
var _a, _b;
// SSH key
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath;
if (keyPath) {
try {
core.info(`Removing SSH key '${keyPath}'`);
yield io.rmRF(keyPath);
}
catch (err) {
@@ -399,37 +466,136 @@ class GitAuthHelper {
const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath;
if (knownHostsPath) {
try {
core.info(`Removing SSH known hosts '${knownHostsPath}'`);
yield io.rmRF(knownHostsPath);
}
catch (_b) {
// Intentionally empty
catch (err) {
core.debug(`${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`);
}
}
// SSH command
core.info('Removing SSH command configuration');
yield this.removeGitConfig(SSH_COMMAND_KEY);
yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY);
});
}
/**
* Removes token-based authentication by cleaning up HTTP headers,
* includeIf entries, and credentials config files.
*/
removeToken() {
return __awaiter(this, void 0, void 0, function* () {
// HTTP extra header
var _a;
// Remove HTTP extra header
core.info('Removing HTTP extra header');
yield this.removeGitConfig(this.tokenConfigKey);
});
}
removeGitConfig(configKey_1) {
return __awaiter(this, arguments, void 0, function* (configKey, submoduleOnly = false) {
if (!submoduleOnly) {
if ((yield this.git.configExists(configKey)) &&
!(yield this.git.tryConfigUnset(configKey))) {
// Load the config contents
core.warning(`Failed to remove '${configKey}' from the git config`);
yield this.removeSubmoduleGitConfig(this.tokenConfigKey);
// Collect credentials config paths that need to be removed
const credentialsPaths = new Set();
// Remove includeIf entries that point to git-credentials-*.config files
core.info('Removing includeIf entries pointing to credentials config files');
const mainCredentialsPaths = yield this.removeIncludeIfCredentials();
mainCredentialsPaths.forEach(path => credentialsPaths.add(path));
// Remove submodule includeIf entries that point to git-credentials-*.config files
const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true);
for (const configPath of submoduleConfigPaths) {
const submoduleCredentialsPaths = yield this.removeIncludeIfCredentials(configPath);
submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path));
}
// Remove credentials config files
for (const credentialsPath of credentialsPaths) {
// Only remove credentials config files if they are under RUNNER_TEMP
const runnerTemp = process.env['RUNNER_TEMP'];
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
if (credentialsPath.startsWith(runnerTemp)) {
try {
core.info(`Removing credentials config '${credentialsPath}'`);
yield io.rmRF(credentialsPath);
}
catch (err) {
core.debug(`${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
core.warning(`Failed to remove credentials config '${credentialsPath}'`);
}
}
else {
core.debug(`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`);
}
}
});
}
/**
* Removes a git config key from the local repository config.
* @param configKey The git config key to remove
*/
removeGitConfig(configKey) {
return __awaiter(this, void 0, void 0, function* () {
if ((yield this.git.configExists(configKey)) &&
!(yield this.git.tryConfigUnset(configKey))) {
// Load the config contents
core.warning(`Failed to remove '${configKey}' from the git config`);
}
});
}
/**
* Removes a git config key from all submodule configs.
* @param configKey The git config key to remove
*/
removeSubmoduleGitConfig(configKey) {
return __awaiter(this, void 0, void 0, function* () {
const pattern = regexpHelper.escape(configKey);
yield this.git.submoduleForeach(
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
// Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true);
});
}
/**
* Removes includeIf entries that point to git-credentials-*.config files.
* @param configPath Optional path to a specific git config file to operate on
* @returns Array of unique credentials config file paths that were found and removed
*/
removeIncludeIfCredentials(configPath) {
return __awaiter(this, void 0, void 0, function* () {
const credentialsPaths = new Set();
try {
// Get all includeIf.gitdir keys
const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig?
configPath);
for (const key of keys) {
// Get all values for this key
const values = yield this.git.tryGetConfigValues(key, false, // globalConfig?
configPath);
if (values.length > 0) {
// Remove only values that match git-credentials-<uuid>.config pattern
for (const value of values) {
if (this.testCredentialsConfigPath(value)) {
credentialsPaths.add(value);
yield this.git.tryConfigUnsetValue(key, value, false, configPath);
}
}
}
}
}
catch (err) {
// Ignore errors - this is cleanup code
if (configPath) {
core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`);
}
else {
core.debug(`Error during includeIf cleanup: ${err}`);
}
}
return Array.from(credentialsPaths);
});
}
/**
* Tests if a path matches the git-credentials-*.config pattern.
* @param path The path to test
* @returns True if the path matches the credentials config pattern
*/
testCredentialsConfigPath(path) {
return /git-credentials-[0-9a-f-]+\.config$/i.test(path);
}
}
@@ -627,9 +793,15 @@ class GitCommandManager {
yield this.execGit(args);
});
}
config(configKey, configValue, globalConfig, add) {
config(configKey, configValue, globalConfig, add, configFile) {
return __awaiter(this, void 0, void 0, function* () {
const args = ['config', globalConfig ? '--global' : '--local'];
const args = ['config'];
if (configFile) {
args.push('--file', configFile);
}
else {
args.push(globalConfig ? '--global' : '--local');
}
if (add) {
args.push('--add');
}
@@ -706,6 +878,16 @@ class GitCommandManager {
throw new Error('Unexpected output when retrieving default branch');
});
}
getSubmoduleConfigPaths(recursive) {
return __awaiter(this, void 0, void 0, function* () {
// Get submodule config file paths.
// Use `--show-origin` to get the config file path for each submodule.
const output = yield this.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, recursive);
// Extract config file paths from the output (lines starting with "file:").
const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
return configPaths;
});
}
getWorkingDirectory() {
return this.workingDirectory;
}
@@ -836,6 +1018,20 @@ class GitCommandManager {
return output.exitCode === 0;
});
}
tryConfigUnsetValue(configKey, configValue, globalConfig, configFile) {
return __awaiter(this, void 0, void 0, function* () {
const args = ['config'];
if (configFile) {
args.push('--file', configFile);
}
else {
args.push(globalConfig ? '--global' : '--local');
}
args.push('--unset', configKey, configValue);
const output = yield this.execGit(args, true);
return output.exitCode === 0;
});
}
tryDisableAutomaticGarbageCollection() {
return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true);
@@ -855,6 +1051,46 @@ class GitCommandManager {
return stdout;
});
}
tryGetConfigValues(configKey, globalConfig, configFile) {
return __awaiter(this, void 0, void 0, function* () {
const args = ['config'];
if (configFile) {
args.push('--file', configFile);
}
else {
args.push(globalConfig ? '--global' : '--local');
}
args.push('--get-all', configKey);
const output = yield this.execGit(args, true);
if (output.exitCode !== 0) {
return [];
}
return output.stdout
.trim()
.split('\n')
.filter(value => value.trim());
});
}
tryGetConfigKeys(pattern, globalConfig, configFile) {
return __awaiter(this, void 0, void 0, function* () {
const args = ['config'];
if (configFile) {
args.push('--file', configFile);
}
else {
args.push(globalConfig ? '--global' : '--local');
}
args.push('--name-only', '--get-regexp', pattern);
const output = yield this.execGit(args, true);
if (output.exitCode !== 0) {
return [];
}
return output.stdout
.trim()
.split('\n')
.filter(key => key.trim());
});
}
tryReset() {
return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['reset', '--hard', 'HEAD'], true);

View File

@@ -1,108 +0,0 @@
- uses: actions/checkout@v5
with:
# Repository name with owner. For example, actions/checkout
# Default: ${{ github.repository }}
repository: ''
# The branch, tag or SHA to checkout. When checking out the repository that
# triggered a workflow, this defaults to the reference or SHA for that event.
# Otherwise, uses the default branch.
ref: ''
# Personal access token (PAT) used to fetch the repository. The PAT is configured
# with the local git config, which enables your scripts to run authenticated git
# commands. The post-job step removes the PAT.
#
# We recommend using a service account with the least permissions necessary. Also
# when generating a new PAT, select the least scopes necessary.
#
# [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
#
# Default: ${{ github.token }}
token: ''
# SSH key used to fetch the repository. The SSH key is configured with the local
# git config, which enables your scripts to run authenticated git commands. The
# post-job step removes the SSH key.
#
# We recommend using a service account with the least permissions necessary.
#
# [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
ssh-key: ''
# Known hosts in addition to the user and global host key database. The public SSH
# keys for a host may be obtained using the utility `ssh-keyscan`. For example,
# `ssh-keyscan github.com`. The public key for github.com is always implicitly
# added.
ssh-known-hosts: ''
# Whether to perform strict host key checking. When true, adds the options
# `StrictHostKeyChecking=yes` and `CheckHostIP=no` to the SSH command line. Use
# the input `ssh-known-hosts` to configure additional hosts.
# Default: true
ssh-strict: ''
# The user to use when connecting to the remote SSH host. By default 'git' is
# used.
# Default: git
ssh-user: ''
# Whether to configure the token or SSH key with the local git config
# Default: true
persist-credentials: ''
# Relative path under $GITHUB_WORKSPACE to place the repository
path: ''
# Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching
# Default: true
clean: ''
# Partially clone against a given filter. Overrides sparse-checkout if set.
# Default: null
filter: ''
# Do a sparse checkout on given patterns. Each pattern should be separated with
# new lines.
# Default: null
sparse-checkout: ''
# Specifies whether to use cone-mode when doing a sparse checkout.
# Default: true
sparse-checkout-cone-mode: ''
# Number of commits to fetch. 0 indicates all history for all branches and tags.
# Default: 1
fetch-depth: ''
# Whether to fetch tags, even if fetch-depth > 0.
# Default: false
fetch-tags: ''
# Whether to show progress status output when fetching.
# Default: true
show-progress: ''
# Whether to download Git-LFS files
# Default: false
lfs: ''
# Whether to checkout submodules: `true` to checkout submodules or `recursive` to
# recursively checkout submodules.
#
# When the `ssh-key` input is not provided, SSH URLs beginning with
# `git@github.com:` are converted to HTTPS.
#
# Default: false
submodules: ''
# Add repository path as safe.directory for Git global config by running `git
# config --global --add safe.directory <path>`
# Default: true
set-safe-directory: ''
# The base URL for the GitHub instance that you are trying to clone from, will use
# environment defaults to fetch from the same instance that the workflow is
# running from unless specified. Example URLs are https://github.com or
# https://my-ghes-server.example.com
github-server-url: ''

View File

@@ -43,6 +43,7 @@ class GitAuthHelper {
private sshKeyPath = ''
private sshKnownHostsPath = ''
private temporaryHomePath = ''
private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP
constructor(
gitCommandManager: IGitCommandManager,
@@ -126,16 +127,21 @@ class GitAuthHelper {
async configureGlobalAuth(): Promise<void> {
// 'configureTempGlobalConfig' noops if already set, just returns the path
const newGitConfigPath = await this.configureTempGlobalConfig()
await this.configureTempGlobalConfig()
try {
// Configure the token
await this.configureToken(newGitConfigPath, true)
await this.configureToken(true)
// Configure HTTPS instead of SSH
await this.git.tryConfigUnset(this.insteadOfKey, true)
if (!this.settings.sshKey) {
for (const insteadOfValue of this.insteadOfValues) {
await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
await this.git.config(
this.insteadOfKey,
insteadOfValue,
true, // globalConfig?
true // add?
)
}
}
} catch (err) {
@@ -150,24 +156,60 @@ class GitAuthHelper {
async configureSubmoduleAuth(): Promise<void> {
// Remove possible previous HTTPS instead of SSH
await this.removeGitConfig(this.insteadOfKey, true)
await this.removeSubmoduleGitConfig(this.insteadOfKey)
if (this.settings.persistCredentials) {
// Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
const output = await this.git.submoduleForeach(
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
// Get the credentials config file path in RUNNER_TEMP
const credentialsConfigPath = this.getCredentialsConfigPath()
// Container credentials config path
const containerCredentialsPath = path.posix.join(
'/github/runner_temp',
path.basename(credentialsConfigPath)
)
// Get submodule config file paths.
const configPaths = await this.git.getSubmoduleConfigPaths(
this.settings.nestedSubmodules
)
// Replace the placeholder
const configPaths: string[] =
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
// For each submodule, configure includeIf entries pointing to the shared credentials file.
// Configure both host and container paths to support Docker container actions.
for (const configPath of configPaths) {
core.debug(`Replacing token placeholder in '${configPath}'`)
await this.replaceTokenPlaceholder(configPath)
// Submodule Git directory
let submoduleGitDir = path.dirname(configPath) // The config file is at .git/modules/submodule-name/config
submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
// Configure host includeIf
await this.git.config(
`includeIf.gitdir:${submoduleGitDir}.path`,
credentialsConfigPath,
false, // globalConfig?
false, // add?
configPath
)
// Container submodule git directory
const githubWorkspace = process.env['GITHUB_WORKSPACE']
assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
let relativeSubmoduleGitDir = path.relative(
githubWorkspace,
submoduleGitDir
)
relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
const containerSubmoduleGitDir = path.posix.join(
'/github/workspace',
relativeSubmoduleGitDir
)
// Configure container includeIf
await this.git.config(
`includeIf.gitdir:${containerSubmoduleGitDir}.path`,
containerCredentialsPath,
false, // globalConfig?
false, // add?
configPath
)
}
if (this.settings.sshKey) {
@@ -201,6 +243,10 @@ class GitAuthHelper {
}
}
/**
* Configures SSH authentication by writing the SSH key and known hosts,
* and setting up the GIT_SSH_COMMAND environment variable.
*/
private async configureSsh(): Promise<void> {
if (!this.settings.sshKey) {
return
@@ -272,57 +318,116 @@ class GitAuthHelper {
}
}
private async configureToken(
configPath?: string,
globalConfig?: boolean
): Promise<void> {
// Validate args
assert.ok(
(configPath && globalConfig) || (!configPath && !globalConfig),
'Unexpected configureToken parameter combinations'
)
/**
* Configures token-based authentication by creating a credentials config file
* and setting up includeIf entries to reference it.
* @param globalConfig Whether to configure global config instead of local
*/
private async configureToken(globalConfig?: boolean): Promise<void> {
// Get the credentials config file path in RUNNER_TEMP
const credentialsConfigPath = this.getCredentialsConfigPath()
// Default config path
if (!configPath && !globalConfig) {
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
}
// Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
// Write placeholder to the separate credentials config file using git config.
// This approach avoids the credential being captured by process creation audit events,
// which are commonly logged. For more information, refer to
// https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
await this.git.config(
this.tokenConfigKey,
this.tokenPlaceholderConfigValue,
globalConfig
false, // globalConfig?
false, // add?
credentialsConfigPath
)
// Replace the placeholder
await this.replaceTokenPlaceholder(configPath || '')
}
private async replaceTokenPlaceholder(configPath: string): Promise<void> {
assert.ok(configPath, 'configPath is not defined')
let content = (await fs.promises.readFile(configPath)).toString()
// Replace the placeholder in the credentials config file
let content = (await fs.promises.readFile(credentialsConfigPath)).toString()
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
if (
placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
) {
throw new Error(`Unable to replace auth placeholder in ${configPath}`)
throw new Error(
`Unable to replace auth placeholder in ${credentialsConfigPath}`
)
}
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
content = content.replace(
this.tokenPlaceholderConfigValue,
this.tokenConfigValue
)
await fs.promises.writeFile(configPath, content)
await fs.promises.writeFile(credentialsConfigPath, content)
// Add include or includeIf to reference the credentials config
if (globalConfig) {
// Global config file is temporary
await this.git.config(
'include.path',
credentialsConfigPath,
true // globalConfig?
)
} else {
// Host git directory
let gitDir = path.join(this.git.getWorkingDirectory(), '.git')
gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
// Configure host includeIf
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
await this.git.config(hostIncludeKey, credentialsConfigPath)
// Container git directory
const workingDirectory = this.git.getWorkingDirectory()
const githubWorkspace = process.env['GITHUB_WORKSPACE']
assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
let relativePath = path.relative(githubWorkspace, workingDirectory)
relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows
const containerGitDir = path.posix.join(
'/github/workspace',
relativePath,
'.git'
)
// Container credentials config path
const containerCredentialsPath = path.posix.join(
'/github/runner_temp',
path.basename(credentialsConfigPath)
)
// Configure container includeIf
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
await this.git.config(containerIncludeKey, containerCredentialsPath)
}
}
/**
* Gets or creates the path to the credentials config file in RUNNER_TEMP.
* @returns The absolute path to the credentials config file
*/
private getCredentialsConfigPath(): string {
if (this.credentialsConfigPath) {
return this.credentialsConfigPath
}
const runnerTemp = process.env['RUNNER_TEMP'] || ''
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
// Create a unique filename for this checkout instance
const configFileName = `git-credentials-${uuid()}.config`
this.credentialsConfigPath = path.join(runnerTemp, configFileName)
core.debug(`Credentials config path: ${this.credentialsConfigPath}`)
return this.credentialsConfigPath
}
/**
* Removes SSH authentication configuration by cleaning up SSH keys,
* known hosts files, and SSH command configurations.
*/
private async removeSsh(): Promise<void> {
// SSH key
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
if (keyPath) {
try {
core.info(`Removing SSH key '${keyPath}'`)
await io.rmRF(keyPath)
} catch (err) {
core.debug(`${(err as any)?.message ?? err}`)
@@ -335,40 +440,149 @@ class GitAuthHelper {
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
if (knownHostsPath) {
try {
core.info(`Removing SSH known hosts '${knownHostsPath}'`)
await io.rmRF(knownHostsPath)
} catch {
// Intentionally empty
} catch (err) {
core.debug(`${(err as any)?.message ?? err}`)
core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`)
}
}
// SSH command
core.info('Removing SSH command configuration')
await this.removeGitConfig(SSH_COMMAND_KEY)
await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY)
}
/**
* Removes token-based authentication by cleaning up HTTP headers,
* includeIf entries, and credentials config files.
*/
private async removeToken(): Promise<void> {
// HTTP extra header
// Remove HTTP extra header
core.info('Removing HTTP extra header')
await this.removeGitConfig(this.tokenConfigKey)
}
await this.removeSubmoduleGitConfig(this.tokenConfigKey)
private async removeGitConfig(
configKey: string,
submoduleOnly: boolean = false
): Promise<void> {
if (!submoduleOnly) {
if (
(await this.git.configExists(configKey)) &&
!(await this.git.tryConfigUnset(configKey))
) {
// Load the config contents
core.warning(`Failed to remove '${configKey}' from the git config`)
}
// Collect credentials config paths that need to be removed
const credentialsPaths = new Set<string>()
// Remove includeIf entries that point to git-credentials-*.config files
core.info('Removing includeIf entries pointing to credentials config files')
const mainCredentialsPaths = await this.removeIncludeIfCredentials()
mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
// Remove submodule includeIf entries that point to git-credentials-*.config files
const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true)
for (const configPath of submoduleConfigPaths) {
const submoduleCredentialsPaths =
await this.removeIncludeIfCredentials(configPath)
submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
}
// Remove credentials config files
for (const credentialsPath of credentialsPaths) {
// Only remove credentials config files if they are under RUNNER_TEMP
const runnerTemp = process.env['RUNNER_TEMP']
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
if (credentialsPath.startsWith(runnerTemp)) {
try {
core.info(`Removing credentials config '${credentialsPath}'`)
await io.rmRF(credentialsPath)
} catch (err) {
core.debug(`${(err as any)?.message ?? err}`)
core.warning(
`Failed to remove credentials config '${credentialsPath}'`
)
}
} else {
core.debug(
`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`
)
}
}
}
/**
* Removes a git config key from the local repository config.
* @param configKey The git config key to remove
*/
private async removeGitConfig(configKey: string): Promise<void> {
if (
(await this.git.configExists(configKey)) &&
!(await this.git.tryConfigUnset(configKey))
) {
// Load the config contents
core.warning(`Failed to remove '${configKey}' from the git config`)
}
}
/**
* Removes a git config key from all submodule configs.
* @param configKey The git config key to remove
*/
private async removeSubmoduleGitConfig(configKey: string): Promise<void> {
const pattern = regexpHelper.escape(configKey)
await this.git.submoduleForeach(
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
// Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
true
)
}
/**
* Removes includeIf entries 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,
configValue: string,
globalConfig?: boolean,
add?: boolean
add?: boolean,
configFile?: string
): Promise<void>
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
fetch(
@@ -41,6 +42,7 @@ export interface IGitCommandManager {
}
): Promise<void>
getDefaultBranch(repositoryUrl: string): Promise<string>
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
getWorkingDirectory(): string
init(): Promise<void>
isDetached(): Promise<boolean>
@@ -59,8 +61,24 @@ export interface IGitCommandManager {
tagExists(pattern: string): Promise<boolean>
tryClean(): Promise<boolean>
tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
tryConfigUnsetValue(
configKey: string,
configValue: string,
globalConfig?: boolean,
configFile?: string
): Promise<boolean>
tryDisableAutomaticGarbageCollection(): Promise<boolean>
tryGetFetchUrl(): Promise<string>
tryGetConfigValues(
configKey: string,
globalConfig?: boolean,
configFile?: string
): Promise<string[]>
tryGetConfigKeys(
pattern: string,
globalConfig?: boolean,
configFile?: string
): Promise<string[]>
tryReset(): Promise<boolean>
version(): Promise<GitVersion>
}
@@ -223,9 +241,15 @@ class GitCommandManager {
configKey: string,
configValue: string,
globalConfig?: boolean,
add?: boolean
add?: boolean,
configFile?: string
): Promise<void> {
const args: string[] = ['config', globalConfig ? '--global' : '--local']
const args: string[] = ['config']
if (configFile) {
args.push('--file', configFile)
} else {
args.push(globalConfig ? '--global' : '--local')
}
if (add) {
args.push('--add')
}
@@ -323,6 +347,21 @@ class GitCommandManager {
throw new Error('Unexpected output when retrieving default branch')
}
async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> {
// Get submodule config file paths.
// Use `--show-origin` to get the config file path for each submodule.
const output = await this.submoduleForeach(
`git config --local --show-origin --name-only --get-regexp remote.origin.url`,
recursive
)
// Extract config file paths from the output (lines starting with "file:").
const configPaths =
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
return configPaths
}
getWorkingDirectory(): string {
return this.workingDirectory
}
@@ -455,6 +494,24 @@ class GitCommandManager {
return output.exitCode === 0
}
async tryConfigUnsetValue(
configKey: string,
configValue: string,
globalConfig?: boolean,
configFile?: string
): Promise<boolean> {
const args = ['config']
if (configFile) {
args.push('--file', configFile)
} else {
args.push(globalConfig ? '--global' : '--local')
}
args.push('--unset', configKey, configValue)
const output = await this.execGit(args, true)
return output.exitCode === 0
}
async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
const output = await this.execGit(
['config', '--local', 'gc.auto', '0'],
@@ -481,6 +538,56 @@ class GitCommandManager {
return stdout
}
async tryGetConfigValues(
configKey: string,
globalConfig?: boolean,
configFile?: string
): Promise<string[]> {
const args = ['config']
if (configFile) {
args.push('--file', configFile)
} else {
args.push(globalConfig ? '--global' : '--local')
}
args.push('--get-all', configKey)
const output = await this.execGit(args, true)
if (output.exitCode !== 0) {
return []
}
return output.stdout
.trim()
.split('\n')
.filter(value => value.trim())
}
async tryGetConfigKeys(
pattern: string,
globalConfig?: boolean,
configFile?: string
): Promise<string[]> {
const args = ['config']
if (configFile) {
args.push('--file', configFile)
} else {
args.push(globalConfig ? '--global' : '--local')
}
args.push('--name-only', '--get-regexp', pattern)
const output = await this.execGit(args, true)
if (output.exitCode !== 0) {
return []
}
return output.stdout
.trim()
.split('\n')
.filter(key => key.trim())
}
async tryReset(): Promise<boolean> {
const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
return output.exitCode === 0