mirror of
https://github.com/actions/checkout.git
synced 2026-06-20 17:34:08 +08:00
Compare commits
21 Commits
2316c2a206
...
v7.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c091bb21b | ||
|
|
1044a6dea9 | ||
|
|
f0282184c7 | ||
|
|
d914b262ff | ||
|
|
537c7ef99c | ||
|
|
130a169078 | ||
|
|
7d09575332 | ||
|
|
0f9f3aa320 | ||
|
|
f9e715a95f | ||
|
|
df4cb1c069 | ||
|
|
1cce3390c2 | ||
|
|
900f2210b1 | ||
|
|
0c366fd6a8 | ||
|
|
de0fac2e45 | ||
|
|
064fe7f331 | ||
|
|
8e8c483db8 | ||
|
|
033fa0dc0b | ||
|
|
c2d88d3ecc | ||
|
|
1af3b93b68 | ||
|
|
71cf2267d8 | ||
|
|
069c695914 |
2
.github/workflows/check-dist.yml
vendored
2
.github/workflows/check-dist.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.6
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set Node.js 24.x
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
234
.github/workflows/codeql.yml
vendored
234
.github/workflows/codeql.yml
vendored
@@ -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}}"
|
||||
@@ -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
|
||||
2
.github/workflows/licensed.yml
vendored
2
.github/workflows/licensed.yml
vendored
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check licenses
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.6
|
||||
- uses: actions/checkout@v6
|
||||
- run: npm ci
|
||||
- run: npm run licensed-check
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checking out
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Publish
|
||||
id: publish
|
||||
uses: actions/publish-immutable-action@0.0.3
|
||||
uses: actions/publish-immutable-action@v0.0.4
|
||||
|
||||
52
.github/workflows/test.yml
vendored
52
.github/workflows/test.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: actions/checkout@v4.1.6
|
||||
- uses: actions/checkout@v6
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run format-check
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Basic checkout
|
||||
- name: Checkout basic
|
||||
@@ -87,6 +87,17 @@ jobs:
|
||||
- name: Verify fetch filter
|
||||
run: __test__/verify-fetch-filter.sh
|
||||
|
||||
# Fetch tags
|
||||
- name: Checkout with fetch-tags
|
||||
uses: ./
|
||||
with:
|
||||
ref: test-data/v2/basic
|
||||
path: fetch-tags-test
|
||||
fetch-tags: true
|
||||
- name: Verify fetch-tags
|
||||
shell: bash
|
||||
run: __test__/verify-fetch-tags.sh
|
||||
|
||||
# Sparse checkout
|
||||
- name: Sparse checkout
|
||||
uses: ./
|
||||
@@ -165,6 +176,22 @@ jobs:
|
||||
- name: Verify submodules recursive
|
||||
run: __test__/verify-submodules-recursive.sh
|
||||
|
||||
# Worktree credentials
|
||||
- name: Checkout for worktree test
|
||||
uses: ./
|
||||
with:
|
||||
path: worktree-test
|
||||
- name: Verify worktree credentials
|
||||
shell: bash
|
||||
run: __test__/verify-worktree.sh worktree-test worktree-branch
|
||||
|
||||
# Worktree credentials in container step
|
||||
- name: Verify worktree credentials in container step
|
||||
if: runner.os == 'Linux'
|
||||
uses: docker://bitnami/git:latest
|
||||
with:
|
||||
args: bash __test__/verify-worktree.sh worktree-test container-worktree-branch
|
||||
|
||||
# Basic checkout using REST API
|
||||
- name: Remove basic
|
||||
if: runner.os != 'windows'
|
||||
@@ -202,7 +229,7 @@ jobs:
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
@@ -234,7 +261,7 @@ jobs:
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
@@ -264,7 +291,7 @@ jobs:
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: localClone
|
||||
|
||||
@@ -291,8 +318,8 @@ jobs:
|
||||
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
|
||||
|
||||
# needed to make checkout post cleanup succeed
|
||||
- name: Fix Checkout v4
|
||||
uses: actions/checkout@v4.1.6
|
||||
- name: Fix Checkout v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: localClone
|
||||
|
||||
@@ -301,13 +328,16 @@ jobs:
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v6
|
||||
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 +355,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
|
||||
|
||||
2
.github/workflows/update-main-version.yml
vendored
2
.github/workflows/update-main-version.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
# Note this update workflow can also be used as a rollback tool.
|
||||
# For that reason, it's best to pin `actions/checkout` to a known, stable version
|
||||
# (typically, about two releases back).
|
||||
- uses: actions/checkout@v4.1.6
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Git config
|
||||
|
||||
2
.github/workflows/update-test-ubuntu-git.yml
vendored
2
.github/workflows/update-test-ubuntu-git.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Use `docker/login-action` to log in to GHCR.io.
|
||||
# Once published, the packages are scoped to the account defined here.
|
||||
|
||||
@@ -11,4 +11,5 @@ allowed:
|
||||
- unlicense
|
||||
|
||||
reviewed:
|
||||
npm:
|
||||
npm:
|
||||
- "@actions/http-client" # MIT
|
||||
|
||||
2
.licenses/npm/@actions/core.dep.yml
generated
2
.licenses/npm/@actions/core.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/core"
|
||||
version: 1.10.1
|
||||
version: 3.0.1
|
||||
type: npm
|
||||
summary: Actions core lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/core
|
||||
|
||||
2
.licenses/npm/@actions/exec.dep.yml
generated
2
.licenses/npm/@actions/exec.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/exec"
|
||||
version: 1.1.1
|
||||
version: 3.0.0
|
||||
type: npm
|
||||
summary: Actions exec lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/exec
|
||||
|
||||
2
.licenses/npm/@actions/github.dep.yml
generated
2
.licenses/npm/@actions/github.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/github"
|
||||
version: 6.0.0
|
||||
version: 9.1.1
|
||||
type: npm
|
||||
summary: Actions github lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/github
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: "@actions/http-client"
|
||||
version: 2.2.1
|
||||
version: 3.0.2
|
||||
type: npm
|
||||
summary: Actions Http Client
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client
|
||||
license: mit
|
||||
license: other
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
32
.licenses/npm/@actions/http-client-4.0.1.dep.yml
generated
Normal file
32
.licenses/npm/@actions/http-client-4.0.1.dep.yml
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "@actions/http-client"
|
||||
version: 4.0.1
|
||||
type: npm
|
||||
summary: Actions Http Client
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client
|
||||
license: other
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
Actions Http Client for Node.js
|
||||
|
||||
Copyright (c) GitHub, Inc.
|
||||
|
||||
All rights reserved.
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
||||
2
.licenses/npm/@actions/io.dep.yml
generated
2
.licenses/npm/@actions/io.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/io"
|
||||
version: 1.1.3
|
||||
version: 3.0.2
|
||||
type: npm
|
||||
summary: Actions io lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/io
|
||||
|
||||
2
.licenses/npm/@actions/tool-cache.dep.yml
generated
2
.licenses/npm/@actions/tool-cache.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/tool-cache"
|
||||
version: 2.0.1
|
||||
version: 4.0.0
|
||||
type: npm
|
||||
summary: Actions tool-cache lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/tool-cache
|
||||
|
||||
30
.licenses/npm/@fastify/busboy.dep.yml
generated
30
.licenses/npm/@fastify/busboy.dep.yml
generated
@@ -1,30 +0,0 @@
|
||||
---
|
||||
name: "@fastify/busboy"
|
||||
version: 2.1.1
|
||||
type: npm
|
||||
summary: A streaming parser for HTML form data for node.js
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |-
|
||||
Copyright Brian White. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
notices: []
|
||||
2
.licenses/npm/@octokit/auth-token.dep.yml
generated
2
.licenses/npm/@octokit/auth-token.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@octokit/auth-token"
|
||||
version: 4.0.0
|
||||
version: 6.0.0
|
||||
type: npm
|
||||
summary: GitHub API token authentication for browsers and Node.js
|
||||
homepage:
|
||||
|
||||
2
.licenses/npm/@octokit/core.dep.yml
generated
2
.licenses/npm/@octokit/core.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@octokit/core"
|
||||
version: 5.2.0
|
||||
version: 7.0.6
|
||||
type: npm
|
||||
summary: Extendable client for GitHub's REST & GraphQL APIs
|
||||
homepage:
|
||||
|
||||
4
.licenses/npm/@octokit/endpoint.dep.yml
generated
4
.licenses/npm/@octokit/endpoint.dep.yml
generated
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/endpoint"
|
||||
version: 9.0.6
|
||||
version: 11.0.3
|
||||
type: npm
|
||||
summary: Turns REST API endpoints into generic request options
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
2
.licenses/npm/@octokit/graphql.dep.yml
generated
2
.licenses/npm/@octokit/graphql.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@octokit/graphql"
|
||||
version: 7.1.0
|
||||
version: 9.0.3
|
||||
type: npm
|
||||
summary: GitHub GraphQL API client for browsers and Node
|
||||
homepage:
|
||||
|
||||
20
.licenses/npm/@octokit/openapi-types-22.1.0.dep.yml
generated
20
.licenses/npm/@octokit/openapi-types-22.1.0.dep.yml
generated
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: "@octokit/openapi-types"
|
||||
version: 22.1.0
|
||||
type: npm
|
||||
summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |-
|
||||
Copyright 2020 Gregor Martynus
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
name: "@octokit/openapi-types"
|
||||
version: 20.0.0
|
||||
version: 27.0.0
|
||||
type: npm
|
||||
summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |-
|
||||
Copyright 2020 Gregor Martynus
|
||||
text: |
|
||||
Copyright (c) GitHub 2025 - Licensed as MIT.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/plugin-paginate-rest"
|
||||
version: 9.2.2
|
||||
version: 14.0.0
|
||||
type: npm
|
||||
summary: Octokit plugin to paginate REST API endpoint responses
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@octokit/plugin-rest-endpoint-methods"
|
||||
version: 10.4.1
|
||||
version: 17.0.0
|
||||
type: npm
|
||||
summary: Octokit plugin adding one method for all of api.github.com REST API endpoints
|
||||
homepage:
|
||||
|
||||
4
.licenses/npm/@octokit/request-error.dep.yml
generated
4
.licenses/npm/@octokit/request-error.dep.yml
generated
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/request-error"
|
||||
version: 5.1.1
|
||||
version: 7.1.0
|
||||
type: npm
|
||||
summary: Error class for Octokit request errors
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
4
.licenses/npm/@octokit/request.dep.yml
generated
4
.licenses/npm/@octokit/request.dep.yml
generated
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: "@octokit/request"
|
||||
version: 8.4.1
|
||||
version: 10.0.10
|
||||
type: npm
|
||||
summary: Send parameterized requests to GitHub's APIs with sensible defaults in browsers
|
||||
and Node
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
20
.licenses/npm/@octokit/types-13.4.1.dep.yml
generated
20
.licenses/npm/@octokit/types-13.4.1.dep.yml
generated
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: "@octokit/types"
|
||||
version: 13.4.1
|
||||
type: npm
|
||||
summary: Shared TypeScript definitions for Octokit projects
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
MIT License Copyright (c) 2019 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@octokit/types"
|
||||
version: 12.6.0
|
||||
version: 16.0.0
|
||||
type: npm
|
||||
summary: Shared TypeScript definitions for Octokit projects
|
||||
homepage:
|
||||
2
.licenses/npm/before-after-hook.dep.yml
generated
2
.licenses/npm/before-after-hook.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: before-after-hook
|
||||
version: 2.2.3
|
||||
version: 4.0.0
|
||||
type: npm
|
||||
summary: asynchronous before/error/after hooks for internal functionality
|
||||
homepage:
|
||||
|
||||
47
.licenses/npm/content-type.dep.yml
generated
Normal file
47
.licenses/npm/content-type.dep.yml
generated
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: content-type
|
||||
version: 2.0.0
|
||||
type: npm
|
||||
summary: Create and parse HTTP Content-Type header
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2015 Douglas Christopher Wilson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: |-
|
||||
[MIT](LICENSE)
|
||||
|
||||
[npm-image]: https://img.shields.io/npm/v/content-type
|
||||
[npm-url]: https://npmjs.org/package/content-type
|
||||
[downloads-image]: https://img.shields.io/npm/dm/content-type
|
||||
[downloads-url]: https://npmjs.org/package/content-type
|
||||
[build-image]: https://img.shields.io/github/actions/workflow/status/jshttp/content-type/ci.yml?branch=master
|
||||
[build-url]: https://github.com/jshttp/content-type/actions/workflows/ci.yml?query=branch%3Amaster
|
||||
[coverage-image]: https://img.shields.io/codecov/c/gh/jshttp/content-type
|
||||
[coverage-url]: https://codecov.io/gh/jshttp/content-type
|
||||
[license-image]: http://img.shields.io/npm/l/content-type.svg?style=flat
|
||||
[license-url]: LICENSE
|
||||
notices: []
|
||||
28
.licenses/npm/deprecation.dep.yml
generated
28
.licenses/npm/deprecation.dep.yml
generated
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: deprecation
|
||||
version: 2.3.1
|
||||
type: npm
|
||||
summary: Log a deprecation message with stack
|
||||
homepage: https://github.com/gr2m/deprecation#readme
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Gregor Martynus and contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[ISC](LICENSE)"
|
||||
notices: []
|
||||
@@ -1,16 +1,17 @@
|
||||
---
|
||||
name: uuid
|
||||
version: 3.4.0
|
||||
name: json-with-bigint
|
||||
version: 3.5.8
|
||||
type: npm
|
||||
summary: RFC4122 (v1, v4, and v5) UUIDs
|
||||
summary: JS library that allows you to easily serialize and deserialize data with
|
||||
BigInt values
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The MIT License (MIT)
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2010-2016 Robert Kieffer and other contributors
|
||||
Copyright (c) 2023 Ivan Korolenko
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -29,11 +30,4 @@ licenses:
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
notices:
|
||||
- sources: AUTHORS
|
||||
text: |-
|
||||
Robert Kieffer <robert@broofa.com>
|
||||
Christoph Tavan <dev@tavan.de>
|
||||
AJ ONeal <coolaj86@gmail.com>
|
||||
Vincent Voyer <vincent@zeroload.net>
|
||||
Roman Shtylman <shtylman@gmail.com>
|
||||
notices: []
|
||||
26
.licenses/npm/once.dep.yml
generated
26
.licenses/npm/once.dep.yml
generated
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: once
|
||||
version: 1.4.0
|
||||
type: npm
|
||||
summary: Run a function exactly one time
|
||||
homepage: https://github.com/isaacs/once#readme
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
notices: []
|
||||
2
.licenses/npm/semver.dep.yml
generated
2
.licenses/npm/semver.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: semver
|
||||
version: 6.3.1
|
||||
version: 7.8.4
|
||||
type: npm
|
||||
summary: The semantic version parser used by npm.
|
||||
homepage:
|
||||
|
||||
2
.licenses/npm/undici.dep.yml
generated
2
.licenses/npm/undici.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: undici
|
||||
version: 5.29.0
|
||||
version: 6.27.0
|
||||
type: npm
|
||||
summary: An HTTP/1.1 client, written from scratch for Node.js
|
||||
homepage: https://undici.nodejs.org
|
||||
|
||||
6
.licenses/npm/universal-user-agent.dep.yml
generated
6
.licenses/npm/universal-user-agent.dep.yml
generated
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: universal-user-agent
|
||||
version: 6.0.1
|
||||
version: 7.0.3
|
||||
type: npm
|
||||
summary: Get a user agent string in both browser and node
|
||||
summary: Get a user agent string across all JavaScript Runtime Environments
|
||||
homepage:
|
||||
license: isc
|
||||
licenses:
|
||||
@@ -10,7 +10,7 @@ licenses:
|
||||
text: |
|
||||
# [ISC License](https://spdx.org/licenses/ISC)
|
||||
|
||||
Copyright (c) 2018, Gregor Martynus (https://github.com/gr2m)
|
||||
Copyright (c) 2018-2021, Gregor Martynus (https://github.com/gr2m)
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
|
||||
20
.licenses/npm/uuid-8.3.2.dep.yml
generated
20
.licenses/npm/uuid-8.3.2.dep.yml
generated
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: uuid
|
||||
version: 8.3.2
|
||||
type: npm
|
||||
summary: RFC4122 (v1, v4, and v5) UUIDs
|
||||
homepage: https://github.com/uuidjs/uuid#readme
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010-2020 Robert Kieffer and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
||||
20
.licenses/npm/uuid-9.0.1.dep.yml
generated
20
.licenses/npm/uuid-9.0.1.dep.yml
generated
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: uuid
|
||||
version: 9.0.1
|
||||
type: npm
|
||||
summary: RFC4122 (v1, v4, and v5) UUIDs
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010-2020 Robert Kieffer and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
||||
26
.licenses/npm/wrappy.dep.yml
generated
26
.licenses/npm/wrappy.dep.yml
generated
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: wrappy
|
||||
version: 1.0.2
|
||||
type: npm
|
||||
summary: Callback wrapping utility
|
||||
homepage: https://github.com/npm/wrappy
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
notices: []
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,10 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
## V5.0.0
|
||||
## v7.0.0
|
||||
* Block checking out fork PR for pull_request_target and workflow_run by @aiqiaoy in https://github.com/actions/checkout/pull/2454
|
||||
* Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the minor-actions-dependencies group across 1 directory by @dependabot[bot] in https://github.com/actions/checkout/pull/2458
|
||||
* Bump flatted from 3.3.1 to 3.4.2 by @dependabot[bot] in https://github.com/actions/checkout/pull/2460
|
||||
* Bump js-yaml from 4.1.0 to 4.2.0 by @dependabot[bot] in https://github.com/actions/checkout/pull/2461
|
||||
* Bump @actions/core and @actions/tool-cache and Remove uuid by @dependabot[bot] in https://github.com/actions/checkout/pull/2459
|
||||
* upgrade module to esm and update dependencies by @aiqiaoy in https://github.com/actions/checkout/pull/2463
|
||||
* Bump the minor-npm-dependencies group across 1 directory with 3 updates by @dependabot[bot] in https://github.com/actions/checkout/pull/2462
|
||||
|
||||
## v6.0.3
|
||||
* Fix checkout init for SHA-256 repositories by @yaananth in https://github.com/actions/checkout/pull/2439
|
||||
* fix: expand merge commit SHA regex and add SHA-256 test cases by @yaananth in https://github.com/actions/checkout/pull/2414
|
||||
|
||||
## v6.0.2
|
||||
* Fix tag handling: preserve annotations and explicit fetch-tags by @ericsciple in https://github.com/actions/checkout/pull/2356
|
||||
|
||||
## v6.0.1
|
||||
* Add worktree support for persist-credentials includeIf by @ericsciple in https://github.com/actions/checkout/pull/2327
|
||||
|
||||
## 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
|
||||
## 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
|
||||
|
||||
65
README.md
65
README.md
@@ -1,14 +1,30 @@
|
||||
[](https://github.com/actions/checkout/actions/workflows/test.yml)
|
||||
|
||||
# Checkout V5
|
||||
# Checkout v7
|
||||
|
||||
## What's new
|
||||
|
||||
- Safer fork pull request handling: checkout now refuses to check out fork pull request code by default when the workflow is triggered by `pull_request_target` or `workflow_run`. These triggers run with the base repository's `GITHUB_TOKEN`, secrets, and runner access, where executing a fork's code commonly leads to "pwn request" vulnerabilities.
|
||||
- To opt in after [reviewing the risks](https://gh.io/securely-using-pull_request_target), set the new `allow-unsafe-pr-checkout: true` input.
|
||||
- Migrated `actions/checkout` to ESM to support new versions of the `@actions/*` packages.
|
||||
- Updated direct and transitive dependencies, including security fixes for known vulnerabilities.
|
||||
|
||||
# Checkout v6
|
||||
|
||||
## What's new
|
||||
|
||||
- Improved credential security: `persist-credentials` now stores credentials in a separate file under `$RUNNER_TEMP` instead of directly in `.git/config`
|
||||
- No workflow changes required — `git fetch`, `git push`, etc. continue to work automatically
|
||||
- Running authenticated git commands from a [Docker container action](https://docs.github.com/actions/sharing-automations/creating-actions/creating-a-docker-container-action) requires Actions Runner [v2.329.0](https://github.com/actions/runner/releases/tag/v2.329.0) or later
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -44,7 +60,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
|
||||
<!-- start usage -->
|
||||
```yaml
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
# Repository name with owner. For example, actions/checkout
|
||||
# Default: ${{ github.repository }}
|
||||
@@ -152,6 +168,15 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
# running from unless specified. Example URLs are https://github.com or
|
||||
# https://my-ghes-server.example.com
|
||||
github-server-url: ''
|
||||
|
||||
# Required to check out fork pull request code from a workflow triggered by
|
||||
# `pull_request_target` or `workflow_run`. These workflows run with the base
|
||||
# repository's GITHUB_TOKEN, secrets, default-branch cache scope, and runner
|
||||
# access; fetching and executing a fork's code in that trusted context commonly
|
||||
# leads to "pwn request" vulnerabilities. Set to `true` only after reviewing the
|
||||
# risks at https://gh.io/securely-using-pull_request_target.
|
||||
# Default: false
|
||||
allow-unsafe-pr-checkout: ''
|
||||
```
|
||||
<!-- end usage -->
|
||||
|
||||
@@ -183,7 +208,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Fetch only the root files
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: .
|
||||
```
|
||||
@@ -191,7 +216,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Fetch only the root files and `.github` and `src` folder
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github
|
||||
@@ -201,7 +226,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Fetch only a single file
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: |
|
||||
README.md
|
||||
@@ -211,7 +236,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Fetch all history for all tags and branches
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
```
|
||||
@@ -219,7 +244,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Checkout a different branch
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: my-branch
|
||||
```
|
||||
@@ -227,7 +252,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Checkout HEAD^
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- run: git checkout HEAD^
|
||||
@@ -237,12 +262,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
path: main
|
||||
|
||||
- name: Checkout tools repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: my-org/my-tools
|
||||
path: my-tools
|
||||
@@ -253,10 +278,10 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Checkout tools repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: my-org/my-tools
|
||||
path: my-tools
|
||||
@@ -267,12 +292,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
path: main
|
||||
|
||||
- name: Checkout private tools
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: my-org/my-private-tools
|
||||
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
|
||||
@@ -285,7 +310,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Checkout pull request HEAD commit instead of merge commit
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
```
|
||||
@@ -301,7 +326,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
```
|
||||
|
||||
## Push a commit using the built-in token
|
||||
@@ -312,7 +337,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
- run: |
|
||||
date > generated.txt
|
||||
# Note: the following account information will not work on GHES
|
||||
@@ -334,7 +359,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- run: |
|
||||
|
||||
@@ -1,12 +1,46 @@
|
||||
import * as core from '@actions/core'
|
||||
import {
|
||||
jest,
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
afterAll
|
||||
} from '@jest/globals'
|
||||
import * as fs from 'fs'
|
||||
import * as gitAuthHelper from '../lib/git-auth-helper'
|
||||
import * as io from '@actions/io'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import * as stateHelper from '../lib/state-helper'
|
||||
import {IGitCommandManager} from '../lib/git-command-manager'
|
||||
import {IGitSourceSettings} from '../lib/git-source-settings'
|
||||
import {fileURLToPath} from 'url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// Mock @actions/core before loading git-auth-helper
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
setSecret: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
info: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
setFailed: jest.fn()
|
||||
}))
|
||||
|
||||
// Mock state-helper
|
||||
jest.unstable_mockModule('../src/state-helper.js', () => ({
|
||||
setSshKeyPath: jest.fn(),
|
||||
setSshKnownHostsPath: jest.fn(),
|
||||
IsPost: false,
|
||||
RepositoryPath: ''
|
||||
}))
|
||||
|
||||
// Dynamic imports after mocking
|
||||
const core = await import('@actions/core')
|
||||
const gitAuthHelper = await import('../src/git-auth-helper.js')
|
||||
type IGitCommandManager =
|
||||
import('../src/git-command-manager.js').IGitCommandManager
|
||||
type IGitSourceSettings =
|
||||
import('../src/git-source-settings.js').IGitSourceSettings
|
||||
|
||||
const isWindows = process.platform === 'win32'
|
||||
const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
|
||||
@@ -32,25 +66,12 @@ describe('git-auth-helper tests', () => {
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock setSecret
|
||||
jest.spyOn(core, 'setSecret').mockImplementation((secret: string) => {})
|
||||
|
||||
// Mock error/warning/info/debug
|
||||
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
||||
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
||||
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
||||
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
||||
|
||||
// Mock state helper
|
||||
jest.spyOn(stateHelper, 'setSshKeyPath').mockImplementation(jest.fn())
|
||||
jest
|
||||
.spyOn(stateHelper, 'setSshKnownHostsPath')
|
||||
.mockImplementation(jest.fn())
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Unregister mocks
|
||||
jest.restoreAllMocks()
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Restore HOME
|
||||
if (originalHome) {
|
||||
@@ -86,16 +107,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 +154,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 +175,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)
|
||||
@@ -211,7 +250,7 @@ describe('git-auth-helper tests', () => {
|
||||
await authHelper.configureAuth()
|
||||
|
||||
// Assert secret
|
||||
const setSecretSpy = core.setSecret as jest.Mock<any, any>
|
||||
const setSecretSpy = core.setSecret as jest.Mock<any>
|
||||
expect(setSecretSpy).toHaveBeenCalledTimes(1)
|
||||
const expectedSecret = Buffer.from(
|
||||
`x-access-token:${settings.authToken}`,
|
||||
@@ -251,13 +290,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 +461,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 +517,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)
|
||||
@@ -484,7 +550,7 @@ describe('git-auth-helper tests', () => {
|
||||
settings.sshKey = ''
|
||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
await authHelper.configureAuth()
|
||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any>
|
||||
mockSubmoduleForeach.mockClear() // reset calls
|
||||
|
||||
// Act
|
||||
@@ -517,7 +583,7 @@ describe('git-auth-helper tests', () => {
|
||||
settings.persistCredentials = false
|
||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
await authHelper.configureAuth()
|
||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any>
|
||||
mockSubmoduleForeach.mockClear() // reset calls
|
||||
|
||||
// Act
|
||||
@@ -543,22 +609,22 @@ describe('git-auth-helper tests', () => {
|
||||
settings.sshKey = ''
|
||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
await authHelper.configureAuth()
|
||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any>
|
||||
mockSubmoduleForeach.mockClear() // reset calls
|
||||
|
||||
// Act
|
||||
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:/
|
||||
)
|
||||
}
|
||||
@@ -582,19 +648,19 @@ describe('git-auth-helper tests', () => {
|
||||
)
|
||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
await authHelper.configureAuth()
|
||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any>
|
||||
mockSubmoduleForeach.mockClear() // reset calls
|
||||
|
||||
// Act
|
||||
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 +726,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>
|
||||
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 +949,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 +1009,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 +1028,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 +1061,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,11 +1100,75 @@ 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()
|
||||
}
|
||||
} as unknown as IGitCommandManager & {env: {[key: string]: string}}
|
||||
|
||||
settings = {
|
||||
authToken: 'some auth token',
|
||||
@@ -824,12 +1194,14 @@ async function setup(testName: string): Promise<void> {
|
||||
sshUser: '',
|
||||
workflowOrganizationId: 123456,
|
||||
setSafeDirectory: true,
|
||||
githubServerUrl: githubServerUrl
|
||||
githubServerUrl: githubServerUrl,
|
||||
allowUnsafePrCheckout: false
|
||||
}
|
||||
}
|
||||
|
||||
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 +1215,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) {
|
||||
|
||||
@@ -1,26 +1,51 @@
|
||||
import * as exec from '@actions/exec'
|
||||
import * as fshelper from '../lib/fs-helper'
|
||||
import * as commandManager from '../lib/git-command-manager'
|
||||
import {
|
||||
jest,
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
afterAll
|
||||
} from '@jest/globals'
|
||||
|
||||
let git: commandManager.IGitCommandManager
|
||||
let mockExec = jest.fn()
|
||||
// Mock @actions/exec
|
||||
const mockExec = jest.fn()
|
||||
jest.unstable_mockModule('@actions/exec', () => ({
|
||||
exec: mockExec
|
||||
}))
|
||||
|
||||
// Mock fs-helper
|
||||
const mockFileExistsSync = jest.fn()
|
||||
const mockDirectoryExistsSync = jest.fn()
|
||||
jest.unstable_mockModule('../src/fs-helper.js', () => ({
|
||||
fileExistsSync: mockFileExistsSync,
|
||||
directoryExistsSync: mockDirectoryExistsSync
|
||||
}))
|
||||
|
||||
// Dynamic imports after mocking
|
||||
const commandManager = await import('../src/git-command-manager.js')
|
||||
type IGitCommandManager =
|
||||
import('../src/git-command-manager.js').IGitCommandManager
|
||||
|
||||
let git: IGitCommandManager
|
||||
|
||||
describe('git-auth-helper tests', () => {
|
||||
beforeAll(async () => {})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
||||
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
|
||||
mockFileExistsSync.mockReset()
|
||||
mockDirectoryExistsSync.mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {})
|
||||
|
||||
it('branch list matches', async () => {
|
||||
mockExec.mockImplementation((path, args, options) => {
|
||||
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||
console.log(args, options.listeners.stdout)
|
||||
|
||||
if (args.includes('version')) {
|
||||
@@ -36,7 +61,7 @@ describe('git-auth-helper tests', () => {
|
||||
|
||||
return 1
|
||||
})
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
// exec.exec is already mockExec
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
const doSparseCheckout = false
|
||||
@@ -53,7 +78,7 @@ describe('git-auth-helper tests', () => {
|
||||
})
|
||||
|
||||
it('ambiguous ref name output is captured', async () => {
|
||||
mockExec.mockImplementation((path, args, options) => {
|
||||
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||
console.log(args, options.listeners.stdout)
|
||||
|
||||
if (args.includes('version')) {
|
||||
@@ -72,7 +97,7 @@ describe('git-auth-helper tests', () => {
|
||||
|
||||
return 1
|
||||
})
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
// exec.exec is already mockExec
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
const doSparseCheckout = false
|
||||
@@ -91,9 +116,9 @@ describe('git-auth-helper tests', () => {
|
||||
|
||||
describe('Test fetchDepth and fetchTags options', () => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
||||
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
|
||||
mockExec.mockImplementation((path, args, options) => {
|
||||
mockFileExistsSync.mockReset()
|
||||
mockDirectoryExistsSync.mockReset()
|
||||
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||
console.log(args, options.listeners.stdout)
|
||||
|
||||
if (args.includes('version')) {
|
||||
@@ -105,11 +130,11 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is true', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
it('should call execGit with the correct arguments when fetchDepth is 0', async () => {
|
||||
// exec.exec is already mockExec
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
const doSparseCheckout = false
|
||||
@@ -122,45 +147,7 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
const refSpec = ['refspec1', 'refspec2']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
fetchDepth: 0,
|
||||
fetchTags: true
|
||||
}
|
||||
|
||||
await git.fetch(refSpec, options)
|
||||
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[
|
||||
'-c',
|
||||
'protocol.version=2',
|
||||
'fetch',
|
||||
'--prune',
|
||||
'--no-recurse-submodules',
|
||||
'--filter=filterValue',
|
||||
'origin',
|
||||
'refspec1',
|
||||
'refspec2'
|
||||
],
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is false', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
const doSparseCheckout = false
|
||||
git = await commandManager.createCommandManager(
|
||||
workingDirectory,
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
const refSpec = ['refspec1', 'refspec2']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
fetchDepth: 0,
|
||||
fetchTags: false
|
||||
fetchDepth: 0
|
||||
}
|
||||
|
||||
await git.fetch(refSpec, options)
|
||||
@@ -183,8 +170,46 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is false', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
it('should call execGit with the correct arguments when fetchDepth is 0 and refSpec includes tags', async () => {
|
||||
// exec.exec is already mockExec
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
const doSparseCheckout = false
|
||||
git = await commandManager.createCommandManager(
|
||||
workingDirectory,
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
fetchDepth: 0
|
||||
}
|
||||
|
||||
await git.fetch(refSpec, options)
|
||||
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[
|
||||
'-c',
|
||||
'protocol.version=2',
|
||||
'fetch',
|
||||
'--no-tags',
|
||||
'--prune',
|
||||
'--no-recurse-submodules',
|
||||
'--filter=filterValue',
|
||||
'origin',
|
||||
'refspec1',
|
||||
'refspec2',
|
||||
'+refs/tags/*:refs/tags/*'
|
||||
],
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when fetchDepth is 1', async () => {
|
||||
// exec.exec is already mockExec
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
@@ -197,8 +222,7 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
const refSpec = ['refspec1', 'refspec2']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
fetchDepth: 1,
|
||||
fetchTags: false
|
||||
fetchDepth: 1
|
||||
}
|
||||
|
||||
await git.fetch(refSpec, options)
|
||||
@@ -222,8 +246,8 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is true', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
it('should call execGit with the correct arguments when fetchDepth is 1 and refSpec includes tags', async () => {
|
||||
// exec.exec is already mockExec
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
@@ -233,11 +257,10 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
const refSpec = ['refspec1', 'refspec2']
|
||||
const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
fetchDepth: 1,
|
||||
fetchTags: true
|
||||
fetchDepth: 1
|
||||
}
|
||||
|
||||
await git.fetch(refSpec, options)
|
||||
@@ -248,20 +271,22 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
'-c',
|
||||
'protocol.version=2',
|
||||
'fetch',
|
||||
'--no-tags',
|
||||
'--prune',
|
||||
'--no-recurse-submodules',
|
||||
'--filter=filterValue',
|
||||
'--depth=1',
|
||||
'origin',
|
||||
'refspec1',
|
||||
'refspec2'
|
||||
'refspec2',
|
||||
'+refs/tags/*:refs/tags/*'
|
||||
],
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when showProgress is true', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
// exec.exec is already mockExec
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
@@ -299,7 +324,7 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when fetchDepth is 42 and showProgress is true', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
// exec.exec is already mockExec
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
@@ -338,8 +363,8 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when fetchTags is true and showProgress is true', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
it('should call execGit with the correct arguments when showProgress is true and refSpec includes tags', async () => {
|
||||
// exec.exec is already mockExec
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
@@ -349,10 +374,9 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
const refSpec = ['refspec1', 'refspec2']
|
||||
const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
fetchTags: true,
|
||||
showProgress: true
|
||||
}
|
||||
|
||||
@@ -364,15 +388,187 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
'-c',
|
||||
'protocol.version=2',
|
||||
'fetch',
|
||||
'--no-tags',
|
||||
'--prune',
|
||||
'--no-recurse-submodules',
|
||||
'--progress',
|
||||
'--filter=filterValue',
|
||||
'origin',
|
||||
'refspec1',
|
||||
'refspec2'
|
||||
'refspec2',
|
||||
'+refs/tags/*:refs/tags/*'
|
||||
],
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('repository initialization object format', () => {
|
||||
beforeEach(async () => {
|
||||
mockFileExistsSync.mockReset()
|
||||
mockDirectoryExistsSync.mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('initializes SHA-256 repositories with the matching object format', async () => {
|
||||
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||
if (args.includes('version')) {
|
||||
options.listeners.stdout(Buffer.from('git version 2.50.1'))
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
// exec.exec is already mockExec
|
||||
|
||||
git = await commandManager.createCommandManager('test', false, false)
|
||||
|
||||
await git.init('sha256')
|
||||
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
['init', '--object-format=sha256', 'test'],
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('initializes SHA-1 repositories with existing default arguments', async () => {
|
||||
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||
if (args.includes('version')) {
|
||||
options.listeners.stdout(Buffer.from('git version 2.50.1'))
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
// exec.exec is already mockExec
|
||||
|
||||
git = await commandManager.createCommandManager('test', false, false)
|
||||
|
||||
await git.init('sha1')
|
||||
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
['init', 'test'],
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('git user-agent with orchestration ID', () => {
|
||||
beforeEach(async () => {
|
||||
mockFileExistsSync.mockReset()
|
||||
mockDirectoryExistsSync.mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
// Clean up environment variable to prevent test pollution
|
||||
delete process.env['ACTIONS_ORCHESTRATION_ID']
|
||||
})
|
||||
|
||||
it('should include orchestration ID in user-agent when ACTIONS_ORCHESTRATION_ID is set', async () => {
|
||||
const orchId = 'test-orch-id-12345'
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = orchId
|
||||
|
||||
let capturedEnv: any = null
|
||||
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||
if (args.includes('version')) {
|
||||
options.listeners.stdout(Buffer.from('2.18'))
|
||||
}
|
||||
// Capture env on any command
|
||||
capturedEnv = options.env
|
||||
return 0
|
||||
})
|
||||
// exec.exec is already mockExec
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
const doSparseCheckout = false
|
||||
git = await commandManager.createCommandManager(
|
||||
workingDirectory,
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
|
||||
// Call a git command to trigger env capture after user-agent is set
|
||||
await git.init()
|
||||
|
||||
// Verify the user agent includes the orchestration ID
|
||||
expect(git).toBeDefined()
|
||||
expect(capturedEnv).toBeDefined()
|
||||
expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
|
||||
`git/2.18 (github-actions-checkout) actions_orchestration_id/${orchId}`
|
||||
)
|
||||
})
|
||||
|
||||
it('should sanitize invalid characters in orchestration ID', async () => {
|
||||
const orchId = 'test (with) special/chars'
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = orchId
|
||||
|
||||
let capturedEnv: any = null
|
||||
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||
if (args.includes('version')) {
|
||||
options.listeners.stdout(Buffer.from('2.18'))
|
||||
}
|
||||
// Capture env on any command
|
||||
capturedEnv = options.env
|
||||
return 0
|
||||
})
|
||||
// exec.exec is already mockExec
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
const doSparseCheckout = false
|
||||
git = await commandManager.createCommandManager(
|
||||
workingDirectory,
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
|
||||
// Call a git command to trigger env capture after user-agent is set
|
||||
await git.init()
|
||||
|
||||
// Verify the user agent has sanitized orchestration ID (spaces, parentheses, slash replaced)
|
||||
expect(git).toBeDefined()
|
||||
expect(capturedEnv).toBeDefined()
|
||||
expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
|
||||
'git/2.18 (github-actions-checkout) actions_orchestration_id/test__with__special_chars'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not modify user-agent when ACTIONS_ORCHESTRATION_ID is not set', async () => {
|
||||
delete process.env['ACTIONS_ORCHESTRATION_ID']
|
||||
|
||||
let capturedEnv: any = null
|
||||
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||
if (args.includes('version')) {
|
||||
options.listeners.stdout(Buffer.from('2.18'))
|
||||
}
|
||||
// Capture env on any command
|
||||
capturedEnv = options.env
|
||||
return 0
|
||||
})
|
||||
// exec.exec is already mockExec
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
const doSparseCheckout = false
|
||||
git = await commandManager.createCommandManager(
|
||||
workingDirectory,
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
|
||||
// Call a git command to trigger env capture after user-agent is set
|
||||
await git.init()
|
||||
|
||||
// Verify the user agent does NOT contain orchestration ID
|
||||
expect(git).toBeDefined()
|
||||
expect(capturedEnv).toBeDefined()
|
||||
expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
|
||||
'git/2.18 (github-actions-checkout)'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
import * as core from '@actions/core'
|
||||
import {
|
||||
jest,
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
afterEach
|
||||
} from '@jest/globals'
|
||||
import * as fs from 'fs'
|
||||
import * as gitDirectoryHelper from '../lib/git-directory-helper'
|
||||
import * as io from '@actions/io'
|
||||
import * as path from 'path'
|
||||
import {IGitCommandManager} from '../lib/git-command-manager'
|
||||
import {fileURLToPath} from 'url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// Mock @actions/core before loading git-directory-helper
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
error: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
info: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
setFailed: jest.fn(),
|
||||
startGroup: jest.fn(),
|
||||
endGroup: jest.fn()
|
||||
}))
|
||||
|
||||
// Dynamic imports after mocking
|
||||
const core = await import('@actions/core')
|
||||
const gitDirectoryHelper = await import('../src/git-directory-helper.js')
|
||||
|
||||
type IGitCommandManager =
|
||||
import('../src/git-command-manager.js').IGitCommandManager
|
||||
|
||||
const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper')
|
||||
let repositoryPath: string
|
||||
@@ -19,16 +46,11 @@ describe('git-directory-helper tests', () => {
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock error/warning/info/debug
|
||||
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
||||
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
||||
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
||||
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Unregister mocks
|
||||
jest.restoreAllMocks()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const cleansWhenCleanTrue = 'cleans when clean true'
|
||||
@@ -81,7 +103,7 @@ describe('git-directory-helper tests', () => {
|
||||
// Arrange
|
||||
await setup(doesNotCheckoutDetachWhenNotAlreadyDetached)
|
||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||
const mockIsDetached = git.isDetached as jest.Mock<any, any>
|
||||
const mockIsDetached = git.isDetached as jest.Mock<any>
|
||||
mockIsDetached.mockImplementation(async () => {
|
||||
return true
|
||||
})
|
||||
@@ -132,7 +154,7 @@ describe('git-directory-helper tests', () => {
|
||||
// Arrange
|
||||
await setup(removesContentsWhenCleanFails)
|
||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||
let mockTryClean = git.tryClean as jest.Mock<any, any>
|
||||
let mockTryClean = git.tryClean as jest.Mock<any>
|
||||
mockTryClean.mockImplementation(async () => {
|
||||
return false
|
||||
})
|
||||
@@ -210,7 +232,7 @@ describe('git-directory-helper tests', () => {
|
||||
// Arrange
|
||||
await setup(removesContentsWhenResetFails)
|
||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||
let mockTryReset = git.tryReset as jest.Mock<any, any>
|
||||
let mockTryReset = git.tryReset as jest.Mock<any>
|
||||
mockTryReset.mockImplementation(async () => {
|
||||
return false
|
||||
})
|
||||
@@ -260,7 +282,7 @@ describe('git-directory-helper tests', () => {
|
||||
// Arrange
|
||||
await setup(removesLocalBranches)
|
||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||
const mockBranchList = git.branchList as jest.Mock<any, any>
|
||||
const mockBranchList = git.branchList as jest.Mock<any>
|
||||
mockBranchList.mockImplementation(async (remote: boolean) => {
|
||||
return remote ? [] : ['local-branch-1', 'local-branch-2']
|
||||
})
|
||||
@@ -291,7 +313,7 @@ describe('git-directory-helper tests', () => {
|
||||
|
||||
//mock bad submodule
|
||||
|
||||
const submoduleStatus = git.submoduleStatus as jest.Mock<any, any>
|
||||
const submoduleStatus = git.submoduleStatus as jest.Mock<any>
|
||||
submoduleStatus.mockImplementation(async (remote: boolean) => {
|
||||
return false
|
||||
})
|
||||
@@ -319,7 +341,7 @@ describe('git-directory-helper tests', () => {
|
||||
await setup(doesNotCleanWhenSubmoduleStatusIsTrue)
|
||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||
|
||||
const submoduleStatus = git.submoduleStatus as jest.Mock<any, any>
|
||||
const submoduleStatus = git.submoduleStatus as jest.Mock<any>
|
||||
submoduleStatus.mockImplementation(async (remote: boolean) => {
|
||||
return true
|
||||
})
|
||||
@@ -381,7 +403,7 @@ describe('git-directory-helper tests', () => {
|
||||
// Arrange
|
||||
await setup(removesAncestorRemoteBranch)
|
||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||
const mockBranchList = git.branchList as jest.Mock<any, any>
|
||||
const mockBranchList = git.branchList as jest.Mock<any>
|
||||
mockBranchList.mockImplementation(async (remote: boolean) => {
|
||||
return remote ? ['origin/remote-branch-1', 'origin/remote-branch-2'] : []
|
||||
})
|
||||
@@ -411,7 +433,7 @@ describe('git-directory-helper tests', () => {
|
||||
// Arrange
|
||||
await setup(removesDescendantRemoteBranches)
|
||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||
const mockBranchList = git.branchList as jest.Mock<any, any>
|
||||
const mockBranchList = git.branchList as jest.Mock<any>
|
||||
mockBranchList.mockImplementation(async (remote: boolean) => {
|
||||
return remote
|
||||
? ['origin/remote-branch-1/conflict', 'origin/remote-branch-2']
|
||||
@@ -471,6 +493,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,15 +516,18 @@ 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
|
||||
}),
|
||||
version: jest.fn()
|
||||
}
|
||||
} as unknown as IGitCommandManager
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {GitVersion} from '../src/git-version'
|
||||
import {MinimumGitSparseCheckoutVersion} from '../src/git-command-manager'
|
||||
import {describe, it, expect} from '@jest/globals'
|
||||
import {GitVersion} from '../src/git-version.js'
|
||||
import {MinimumGitSparseCheckoutVersion} from '../src/git-command-manager.js'
|
||||
|
||||
describe('git-version tests', () => {
|
||||
it('basics', async () => {
|
||||
|
||||
112
__test__/github-api-helper.test.ts
Normal file
112
__test__/github-api-helper.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals'
|
||||
|
||||
// Mock @actions/core
|
||||
const mockDebug = jest.fn()
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
debug: mockDebug,
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
error: jest.fn()
|
||||
}))
|
||||
|
||||
// Mock @actions/github
|
||||
const mockGetOctokit = jest.fn()
|
||||
jest.unstable_mockModule('@actions/github', () => ({
|
||||
getOctokit: mockGetOctokit
|
||||
}))
|
||||
|
||||
// Dynamic imports after mocking
|
||||
const githubApiHelper = await import('../src/github-api-helper.js')
|
||||
|
||||
describe('github-api-helper object format', () => {
|
||||
let request: jest.Mock<any>
|
||||
|
||||
function mockHashAlgorithmApi(hashAlgorithm: string): void {
|
||||
request = jest.fn(async () => ({
|
||||
data: {
|
||||
hash_algorithm: hashAlgorithm
|
||||
}
|
||||
}))
|
||||
mockGetOctokit.mockReturnValue({
|
||||
request
|
||||
} as any)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockDebug.mockClear()
|
||||
mockGetOctokit.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('detects SHA-256 from the repository hash algorithm endpoint', async () => {
|
||||
mockHashAlgorithmApi('sha256')
|
||||
|
||||
await expect(
|
||||
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
||||
).resolves.toEqual({format: 'sha256', succeeded: true})
|
||||
|
||||
expect(mockGetOctokit).toHaveBeenCalledWith(
|
||||
'token',
|
||||
expect.objectContaining({baseUrl: 'https://api.github.com'})
|
||||
)
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
'GET /repos/{owner}/{repo}/hash-algorithm',
|
||||
{owner: 'owner', repo: 'repo'}
|
||||
)
|
||||
})
|
||||
|
||||
it('detects SHA-1 from the repository hash algorithm endpoint', async () => {
|
||||
mockHashAlgorithmApi('sha1')
|
||||
|
||||
await expect(
|
||||
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
||||
).resolves.toEqual({format: 'sha1', succeeded: true})
|
||||
})
|
||||
|
||||
it('detects object format from an existing commit without API calls', async () => {
|
||||
const commitSha =
|
||||
'9422233ca7ee1b17f1e905d0e141faf0c401556c41cdc6acd71c6bd685da2e92'
|
||||
|
||||
await expect(
|
||||
githubApiHelper.tryGetRepositoryObjectFormat(
|
||||
'token',
|
||||
'owner',
|
||||
'repo',
|
||||
undefined,
|
||||
commitSha
|
||||
)
|
||||
).resolves.toEqual({format: 'sha256', succeeded: true})
|
||||
|
||||
expect(mockGetOctokit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns unsuccessful when the hash algorithm endpoint value is not recognized', async () => {
|
||||
mockHashAlgorithmApi('unknown')
|
||||
|
||||
await expect(
|
||||
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
||||
).resolves.toEqual({format: '', succeeded: false})
|
||||
expect(mockDebug).toHaveBeenCalledWith(
|
||||
'Unable to determine repository object format from hash-algorithm endpoint'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns unsuccessful when the hash algorithm API lookup fails', async () => {
|
||||
request = jest.fn(async () => {
|
||||
throw new Error('not found')
|
||||
})
|
||||
mockGetOctokit.mockReturnValue({
|
||||
request
|
||||
} as any)
|
||||
|
||||
await expect(
|
||||
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
||||
).resolves.toEqual({format: '', succeeded: false})
|
||||
expect(mockDebug).toHaveBeenCalledWith(
|
||||
'Unable to determine repository object format from hash-algorithm endpoint: not found'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,13 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fsHelper from '../lib/fs-helper'
|
||||
import * as github from '@actions/github'
|
||||
import * as inputHelper from '../lib/input-helper'
|
||||
import {
|
||||
jest,
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
afterAll
|
||||
} from '@jest/globals'
|
||||
import * as path from 'path'
|
||||
import * as workflowContextHelper from '../lib/workflow-context-helper'
|
||||
import {IGitSourceSettings} from '../lib/git-source-settings'
|
||||
|
||||
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||
const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
|
||||
@@ -12,42 +15,58 @@ const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
|
||||
// Inputs for mock @actions/core
|
||||
let inputs = {} as any
|
||||
|
||||
// Shallow clone original @actions/github context
|
||||
let originalContext = {...github.context}
|
||||
// Mutable mock github context
|
||||
const mockGithubContext: any = {
|
||||
ref: 'refs/heads/some-ref',
|
||||
sha: '1234567890123456789012345678901234567890',
|
||||
repo: {owner: 'some-owner', repo: 'some-repo'},
|
||||
eventName: '',
|
||||
payload: {}
|
||||
}
|
||||
|
||||
// Mock @actions/core before loading input-helper
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
getInput: jest.fn((name: string) => inputs[name]),
|
||||
getBooleanInput: jest.fn((name: string) => inputs[name]),
|
||||
getMultilineInput: jest.fn((name: string) =>
|
||||
inputs[name] ? String(inputs[name]).split('\n').filter(Boolean) : []
|
||||
),
|
||||
error: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
info: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
setFailed: jest.fn(),
|
||||
setOutput: jest.fn(),
|
||||
setSecret: jest.fn()
|
||||
}))
|
||||
|
||||
// Mock @actions/github before loading input-helper
|
||||
jest.unstable_mockModule('@actions/github', () => ({
|
||||
context: mockGithubContext,
|
||||
getOctokit: jest.fn()
|
||||
}))
|
||||
|
||||
// Mock fs-helper
|
||||
const mockDirectoryExistsSync = jest.fn((p: string) => p === gitHubWorkspace)
|
||||
jest.unstable_mockModule('../src/fs-helper.js', () => ({
|
||||
directoryExistsSync: mockDirectoryExistsSync,
|
||||
fileExistsSync: jest.fn()
|
||||
}))
|
||||
|
||||
// Mock workflow-context-helper
|
||||
const mockGetOrganizationId = jest.fn(async () => 123456)
|
||||
jest.unstable_mockModule('../src/workflow-context-helper.js', () => ({
|
||||
getOrganizationId: mockGetOrganizationId
|
||||
}))
|
||||
|
||||
// Dynamic imports after mocking
|
||||
const core = await import('@actions/core')
|
||||
const inputHelper = await import('../src/input-helper.js')
|
||||
type IGitSourceSettings =
|
||||
import('../src/git-source-settings.js').IGitSourceSettings
|
||||
|
||||
describe('input-helper tests', () => {
|
||||
beforeAll(() => {
|
||||
// Mock getInput
|
||||
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
|
||||
return inputs[name]
|
||||
})
|
||||
|
||||
// Mock error/warning/info/debug
|
||||
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
||||
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
||||
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
||||
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
||||
|
||||
// Mock github context
|
||||
jest.spyOn(github.context, 'repo', 'get').mockImplementation(() => {
|
||||
return {
|
||||
owner: 'some-owner',
|
||||
repo: 'some-repo'
|
||||
}
|
||||
})
|
||||
github.context.ref = 'refs/heads/some-ref'
|
||||
github.context.sha = '1234567890123456789012345678901234567890'
|
||||
|
||||
// Mock ./fs-helper directoryExistsSync()
|
||||
jest
|
||||
.spyOn(fsHelper, 'directoryExistsSync')
|
||||
.mockImplementation((path: string) => path == gitHubWorkspace)
|
||||
|
||||
// Mock ./workflowContextHelper getOrganizationId()
|
||||
jest
|
||||
.spyOn(workflowContextHelper, 'getOrganizationId')
|
||||
.mockImplementation(() => Promise.resolve(123456))
|
||||
|
||||
// GitHub workspace
|
||||
process.env['GITHUB_WORKSPACE'] = gitHubWorkspace
|
||||
})
|
||||
@@ -55,6 +74,15 @@ describe('input-helper tests', () => {
|
||||
beforeEach(() => {
|
||||
// Reset inputs
|
||||
inputs = {}
|
||||
jest.clearAllMocks()
|
||||
// Re-apply default mocks
|
||||
;(core.getInput as jest.Mock<any>).mockImplementation(
|
||||
(name: string) => inputs[name]
|
||||
)
|
||||
mockDirectoryExistsSync.mockImplementation(
|
||||
(p: string) => p === gitHubWorkspace
|
||||
)
|
||||
mockGetOrganizationId.mockResolvedValue(123456)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
@@ -65,11 +93,8 @@ describe('input-helper tests', () => {
|
||||
}
|
||||
|
||||
// Restore @actions/github context
|
||||
github.context.ref = originalContext.ref
|
||||
github.context.sha = originalContext.sha
|
||||
|
||||
// Restore
|
||||
jest.restoreAllMocks()
|
||||
mockGithubContext.ref = 'refs/heads/some-ref'
|
||||
mockGithubContext.sha = '1234567890123456789012345678901234567890'
|
||||
})
|
||||
|
||||
it('sets defaults', async () => {
|
||||
@@ -91,18 +116,19 @@ describe('input-helper tests', () => {
|
||||
expect(settings.repositoryOwner).toBe('some-owner')
|
||||
expect(settings.repositoryPath).toBe(gitHubWorkspace)
|
||||
expect(settings.setSafeDirectory).toBe(true)
|
||||
expect(settings.allowUnsafePrCheckout).toBe(false)
|
||||
})
|
||||
|
||||
it('qualifies ref', async () => {
|
||||
let originalRef = github.context.ref
|
||||
let originalRef = mockGithubContext.ref
|
||||
try {
|
||||
github.context.ref = 'some-unqualified-ref'
|
||||
mockGithubContext.ref = 'some-unqualified-ref'
|
||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||
expect(settings).toBeTruthy()
|
||||
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
|
||||
expect(settings.ref).toBe('refs/heads/some-unqualified-ref')
|
||||
} finally {
|
||||
github.context.ref = originalRef
|
||||
mockGithubContext.ref = originalRef
|
||||
}
|
||||
})
|
||||
|
||||
@@ -133,6 +159,16 @@ describe('input-helper tests', () => {
|
||||
expect(settings.commit).toBe('1111111111222222222233333333334444444444')
|
||||
})
|
||||
|
||||
it('sets ref to empty when explicit sha-256', async () => {
|
||||
inputs.ref =
|
||||
'1111111111222222222233333333334444444444555555555566666666667777'
|
||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||
expect(settings.ref).toBeFalsy()
|
||||
expect(settings.commit).toBe(
|
||||
'1111111111222222222233333333334444444444555555555566666666667777'
|
||||
)
|
||||
})
|
||||
|
||||
it('sets sha to empty when explicit ref', async () => {
|
||||
inputs.ref = 'refs/heads/some-other-ref'
|
||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||
|
||||
@@ -1,13 +1,46 @@
|
||||
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals'
|
||||
import * as assert from 'assert'
|
||||
import * as refHelper from '../lib/ref-helper'
|
||||
import {IGitCommandManager} from '../lib/git-command-manager'
|
||||
|
||||
// Mutable mock github context
|
||||
const mockGithubContext: any = {
|
||||
eventName: '',
|
||||
payload: {},
|
||||
repo: {owner: 'some-owner', repo: 'some-repo'},
|
||||
ref: '',
|
||||
sha: ''
|
||||
}
|
||||
|
||||
// Mock @actions/core
|
||||
const mockDebug = jest.fn()
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
debug: mockDebug,
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
error: jest.fn(),
|
||||
setFailed: jest.fn()
|
||||
}))
|
||||
|
||||
// Mock @actions/github
|
||||
const mockGetOctokit = jest.fn()
|
||||
jest.unstable_mockModule('@actions/github', () => ({
|
||||
context: mockGithubContext,
|
||||
getOctokit: mockGetOctokit
|
||||
}))
|
||||
|
||||
// Dynamic imports after mocking
|
||||
const refHelper = await import('../src/ref-helper.js')
|
||||
type IGitCommandManager =
|
||||
import('../src/git-command-manager.js').IGitCommandManager
|
||||
|
||||
const commit = '1234567890123456789012345678901234567890'
|
||||
const sha256Commit =
|
||||
'1234567890123456789012345678901234567890123456789012345678901234'
|
||||
let git: IGitCommandManager
|
||||
|
||||
describe('ref-helper tests', () => {
|
||||
beforeEach(() => {
|
||||
git = {} as unknown as IGitCommandManager
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo requires git', async () => {
|
||||
@@ -37,6 +70,12 @@ describe('ref-helper tests', () => {
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo sha-256 only', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(git, '', sha256Commit)
|
||||
expect(checkoutInfo.ref).toBe(sha256Commit)
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/heads/', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
@@ -152,7 +191,20 @@ describe('ref-helper tests', () => {
|
||||
it('getRefSpec sha + refs/tags/', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit)
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe(`+${commit}:refs/tags/my-tag`)
|
||||
expect(refSpec[0]).toBe(`+refs/tags/my-tag:refs/tags/my-tag`)
|
||||
})
|
||||
|
||||
it('getRefSpec sha + refs/tags/ with fetchTags', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit, true)
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
|
||||
})
|
||||
|
||||
it('getRefSpec sha + refs/heads/ with fetchTags', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/heads/my/branch', commit, true)
|
||||
expect(refSpec.length).toBe(2)
|
||||
expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
|
||||
expect(refSpec[1]).toBe(`+${commit}:refs/remotes/origin/my/branch`)
|
||||
})
|
||||
|
||||
it('getRefSpec sha only', async () => {
|
||||
@@ -168,6 +220,13 @@ describe('ref-helper tests', () => {
|
||||
expect(refSpec[1]).toBe('+refs/tags/my-ref*:refs/tags/my-ref*')
|
||||
})
|
||||
|
||||
it('getRefSpec unqualified ref only with fetchTags', async () => {
|
||||
const refSpec = refHelper.getRefSpec('my-ref', '', true)
|
||||
expect(refSpec.length).toBe(2)
|
||||
expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
|
||||
expect(refSpec[1]).toBe('+refs/heads/my-ref*:refs/remotes/origin/my-ref*')
|
||||
})
|
||||
|
||||
it('getRefSpec refs/heads/ only', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '')
|
||||
expect(refSpec.length).toBe(1)
|
||||
@@ -187,4 +246,155 @@ describe('ref-helper tests', () => {
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe('+refs/tags/my-tag:refs/tags/my-tag')
|
||||
})
|
||||
|
||||
it('getRefSpec refs/tags/ only with fetchTags', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/tags/my-tag', '', true)
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
|
||||
})
|
||||
|
||||
it('getRefSpec refs/heads/ only with fetchTags', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '', true)
|
||||
expect(refSpec.length).toBe(2)
|
||||
expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
|
||||
expect(refSpec[1]).toBe(
|
||||
'+refs/heads/my/branch:refs/remotes/origin/my/branch'
|
||||
)
|
||||
})
|
||||
|
||||
describe('checkCommitInfo', () => {
|
||||
const repositoryOwner = 'some-owner'
|
||||
const repositoryName = 'some-repo'
|
||||
const ref = 'refs/pull/123/merge'
|
||||
const sha1Head = '1111111111222222222233333333334444444444'
|
||||
const sha1Base = 'aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd'
|
||||
const sha256Head =
|
||||
'1111111111222222222233333333334444444444555555555566666666667777'
|
||||
const sha256Base =
|
||||
'aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffff0000'
|
||||
let repoGetSpy: jest.Mock<any>
|
||||
let originalEventName: string
|
||||
let originalPayload: unknown
|
||||
let originalRef: string
|
||||
let originalSha: string
|
||||
|
||||
function setPullRequestContext(
|
||||
expectedHeadSha: string,
|
||||
expectedBaseSha: string,
|
||||
mergeCommit: string
|
||||
): void {
|
||||
mockGithubContext.eventName = 'pull_request'
|
||||
mockGithubContext.ref = ref
|
||||
mockGithubContext.sha = mergeCommit
|
||||
mockGithubContext.payload = {
|
||||
action: 'synchronize',
|
||||
after: expectedHeadSha,
|
||||
number: 123,
|
||||
pull_request: {
|
||||
base: {
|
||||
sha: expectedBaseSha
|
||||
}
|
||||
},
|
||||
repository: {
|
||||
private: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
originalEventName = mockGithubContext.eventName
|
||||
originalPayload = mockGithubContext.payload
|
||||
originalRef = mockGithubContext.ref
|
||||
originalSha = mockGithubContext.sha
|
||||
|
||||
mockGithubContext.repo = {
|
||||
owner: repositoryOwner,
|
||||
repo: repositoryName
|
||||
}
|
||||
|
||||
repoGetSpy = jest.fn(async () => ({}))
|
||||
mockGetOctokit.mockReturnValue({
|
||||
rest: {
|
||||
repos: {
|
||||
get: repoGetSpy
|
||||
}
|
||||
}
|
||||
} as any)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockGithubContext.eventName = originalEventName
|
||||
mockGithubContext.payload = originalPayload
|
||||
mockGithubContext.ref = originalRef
|
||||
mockGithubContext.sha = originalSha
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns early for SHA-1 merge commit', async () => {
|
||||
setPullRequestContext(sha1Head, sha1Base, commit)
|
||||
|
||||
await refHelper.checkCommitInfo(
|
||||
'token',
|
||||
`Merge ${sha1Head} into ${sha1Base}`,
|
||||
repositoryOwner,
|
||||
repositoryName,
|
||||
ref,
|
||||
commit
|
||||
)
|
||||
|
||||
expect(mockGetOctokit).not.toHaveBeenCalled()
|
||||
expect(repoGetSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('matches SHA-256 merge commit info', async () => {
|
||||
const actualHeadSha =
|
||||
'9999999999888888888877777777776666666666555555555544444444443333'
|
||||
setPullRequestContext(sha256Head, sha256Base, sha256Commit)
|
||||
|
||||
await refHelper.checkCommitInfo(
|
||||
'token',
|
||||
`Merge ${actualHeadSha} into ${sha256Base}`,
|
||||
repositoryOwner,
|
||||
repositoryName,
|
||||
ref,
|
||||
sha256Commit
|
||||
)
|
||||
|
||||
expect(mockGetOctokit).toHaveBeenCalledWith(
|
||||
'token',
|
||||
expect.objectContaining({
|
||||
userAgent: expect.stringContaining(
|
||||
`expected_head_sha=${sha256Head};actual_head_sha=${actualHeadSha}`
|
||||
)
|
||||
})
|
||||
)
|
||||
expect(repoGetSpy).toHaveBeenCalledWith({
|
||||
owner: repositoryOwner,
|
||||
repo: repositoryName
|
||||
})
|
||||
expect(mockDebug).toHaveBeenCalledWith(
|
||||
`Expected head sha ${sha256Head}; actual head sha ${actualHeadSha}`
|
||||
)
|
||||
expect(mockDebug).not.toHaveBeenCalledWith('Unexpected message format')
|
||||
})
|
||||
|
||||
it('does not match 50-char hex as a valid merge', async () => {
|
||||
const invalidHeadSha =
|
||||
'99999999998888888888777777777766666666665555555555'
|
||||
setPullRequestContext(sha1Head, sha1Base, commit)
|
||||
|
||||
await refHelper.checkCommitInfo(
|
||||
'token',
|
||||
`Merge ${invalidHeadSha} into ${sha1Base}`,
|
||||
repositoryOwner,
|
||||
repositoryName,
|
||||
ref,
|
||||
commit
|
||||
)
|
||||
|
||||
expect(mockGetOctokit).not.toHaveBeenCalled()
|
||||
expect(repoGetSpy).not.toHaveBeenCalled()
|
||||
expect(mockDebug).toHaveBeenCalledWith('Unexpected message format')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
import * as core from '@actions/core'
|
||||
import {RetryHelper} from '../lib/retry-helper'
|
||||
import {
|
||||
jest,
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
afterAll
|
||||
} from '@jest/globals'
|
||||
|
||||
let info: string[] = []
|
||||
|
||||
// Mock @actions/core before loading retry-helper
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
info: jest.fn((message: string) => {
|
||||
info.push(message)
|
||||
}),
|
||||
debug: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
error: jest.fn()
|
||||
}))
|
||||
|
||||
// Dynamic imports after mocking
|
||||
const {RetryHelper} = await import('../src/retry-helper.js')
|
||||
|
||||
let info: string[]
|
||||
let retryHelper: any
|
||||
|
||||
describe('retry-helper tests', () => {
|
||||
beforeAll(() => {
|
||||
// Mock @actions/core info()
|
||||
jest.spyOn(core, 'info').mockImplementation((message: string) => {
|
||||
info.push(message)
|
||||
})
|
||||
|
||||
retryHelper = new RetryHelper(3, 0, 0)
|
||||
})
|
||||
|
||||
@@ -20,7 +36,6 @@ describe('retry-helper tests', () => {
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
// Restore
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
|
||||
284
__test__/unsafe-pr-checkout-helper.test.ts
Normal file
284
__test__/unsafe-pr-checkout-helper.test.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import {
|
||||
jest,
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeAll,
|
||||
afterEach,
|
||||
afterAll
|
||||
} from '@jest/globals'
|
||||
|
||||
const BASE_REPO_ID = 100
|
||||
const FORK_REPO_ID = 200
|
||||
const PR_HEAD_SHA = '1111111111111111111111111111111111111111'
|
||||
const PR_MERGE_SHA = '2222222222222222222222222222222222222222'
|
||||
const SAFE_BASE_SHA = '3333333333333333333333333333333333333333'
|
||||
const WORKFLOW_RUN_HEAD_COMMIT_SHA = '4444444444444444444444444444444444444444'
|
||||
const BASE_QUALIFIED_REPO = 'some-owner/some-repo'
|
||||
const FORK_QUALIFIED_REPO = 'another-repo/fork'
|
||||
|
||||
// Mutable mock context
|
||||
const mockContext: any = {
|
||||
eventName: '',
|
||||
payload: {},
|
||||
repo: {owner: 'some-owner', repo: 'some-repo'},
|
||||
ref: '',
|
||||
sha: ''
|
||||
}
|
||||
|
||||
jest.unstable_mockModule('@actions/github', () => ({
|
||||
context: mockContext
|
||||
}))
|
||||
|
||||
// Dynamic imports after mocking
|
||||
const {assertSafePrCheckout} =
|
||||
await import('../src/unsafe-pr-checkout-helper.js')
|
||||
|
||||
const originalEventName = mockContext.eventName
|
||||
const originalPayload = mockContext.payload
|
||||
|
||||
function setContext(eventName: string, payload: object): void {
|
||||
mockContext.eventName = eventName
|
||||
mockContext.payload = payload
|
||||
}
|
||||
|
||||
function forkPullRequestTargetPayload(): object {
|
||||
return {
|
||||
repository: {id: BASE_REPO_ID},
|
||||
pull_request: {
|
||||
head: {
|
||||
sha: PR_HEAD_SHA,
|
||||
repo: {id: FORK_REPO_ID, full_name: FORK_QUALIFIED_REPO}
|
||||
},
|
||||
merge_commit_sha: PR_MERGE_SHA
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sameRepoPullRequestTargetPayload(): object {
|
||||
return {
|
||||
repository: {id: BASE_REPO_ID},
|
||||
pull_request: {
|
||||
head: {
|
||||
sha: PR_HEAD_SHA,
|
||||
repo: {id: BASE_REPO_ID, full_name: BASE_QUALIFIED_REPO}
|
||||
},
|
||||
merge_commit_sha: PR_MERGE_SHA
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function forkWorkflowRunPayload(): object {
|
||||
return {
|
||||
repository: {id: BASE_REPO_ID},
|
||||
workflow_run: {
|
||||
event: 'pull_request',
|
||||
head_commit: {id: WORKFLOW_RUN_HEAD_COMMIT_SHA},
|
||||
head_repository: {id: FORK_REPO_ID, full_name: FORK_QUALIFIED_REPO}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('unsafe-pr-checkout-helper', () => {
|
||||
beforeAll(() => {
|
||||
mockContext.repo = {owner: 'some-owner', repo: 'some-repo'}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockContext.eventName = originalEventName
|
||||
mockContext.payload = originalPayload
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
mockContext.eventName = originalEventName
|
||||
mockContext.payload = originalPayload
|
||||
})
|
||||
|
||||
it('allows pull_request events untouched', () => {
|
||||
setContext('pull_request', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: 'attacker/fork',
|
||||
ref: 'refs/pull/1/merge',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('allows pull_request_target default checkout (base branch)', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: 'refs/heads/main',
|
||||
commit: SAFE_BASE_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('allows same-repo pull_request_target checkout of PR head', () => {
|
||||
setContext('pull_request_target', sameRepoPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: PR_HEAD_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('refuses pull_request_target fork PR head SHA checkout', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: PR_HEAD_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow(/Refusing to check out fork pull request code/)
|
||||
})
|
||||
|
||||
it('refuses pull_request_target fork PR merge_commit_sha checkout', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: PR_MERGE_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow(/allow-unsafe-pr-checkout/)
|
||||
})
|
||||
|
||||
it('refuses pull_request_target fork PR ref pattern (head)', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: 'refs/pull/42/head',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('refuses pull_request_target fork PR ref pattern (merge)', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: 'refs/pull/42/merge',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('refuses pull_request_target when repository points at the fork', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: FORK_QUALIFIED_REPO,
|
||||
ref: 'refs/heads/main',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('allows pull_request_target checkout of an unrelated third-party repo', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: 'some-other/unrelated',
|
||||
ref: 'refs/heads/main',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('refuses pull_request_target ignoring repository case differences', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: FORK_QUALIFIED_REPO.toUpperCase(),
|
||||
ref: '',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('refuses pull_request_target ignoring commit SHA case differences', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: PR_HEAD_SHA.toUpperCase(),
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('allows pull_request_target fork PR checkout when opted in', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: 'refs/pull/42/merge',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: true
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('refuses workflow_run fork PR head_commit.id checkout', () => {
|
||||
setContext('workflow_run', forkWorkflowRunPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: WORKFLOW_RUN_HEAD_COMMIT_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('refuses workflow_run with pull_request_target underlying event', () => {
|
||||
const payload = forkWorkflowRunPayload() as {
|
||||
workflow_run: {event: string}
|
||||
}
|
||||
payload.workflow_run.event = 'pull_request_target'
|
||||
setContext('workflow_run', payload)
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: WORKFLOW_RUN_HEAD_COMMIT_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('allows workflow_run same-repo PR (head_repository.id matches base)', () => {
|
||||
const payload = forkWorkflowRunPayload() as {
|
||||
workflow_run: {head_repository: {id: number}}
|
||||
}
|
||||
payload.workflow_run.head_repository.id = BASE_REPO_ID
|
||||
setContext('workflow_run', payload)
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: WORKFLOW_RUN_HEAD_COMMIT_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as urlHelper from '../src/url-helper'
|
||||
import {jest, describe, it, expect, beforeEach, afterAll} from '@jest/globals'
|
||||
import * as urlHelper from '../src/url-helper.js'
|
||||
|
||||
describe('getServerUrl tests', () => {
|
||||
it('basics', async () => {
|
||||
|
||||
9
__test__/verify-fetch-tags.sh
Executable file
9
__test__/verify-fetch-tags.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Verify tags were fetched
|
||||
TAG_COUNT=$(git -C ./fetch-tags-test tag | wc -l)
|
||||
if [ "$TAG_COUNT" -eq 0 ]; then
|
||||
echo "Expected tags to be fetched, but found none"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found $TAG_COUNT tags"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
51
__test__/verify-worktree.sh
Executable file
51
__test__/verify-worktree.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Verify worktree credentials
|
||||
# This test verifies that git credentials work in worktrees created after checkout
|
||||
# Usage: verify-worktree.sh <checkout-path> <worktree-name>
|
||||
|
||||
CHECKOUT_PATH="$1"
|
||||
WORKTREE_NAME="$2"
|
||||
|
||||
if [ -z "$CHECKOUT_PATH" ] || [ -z "$WORKTREE_NAME" ]; then
|
||||
echo "Usage: verify-worktree.sh <checkout-path> <worktree-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$CHECKOUT_PATH"
|
||||
|
||||
# Add safe directory for container environments
|
||||
git config --global --add safe.directory "*" 2>/dev/null || true
|
||||
|
||||
# Show the includeIf configuration
|
||||
echo "Git config includeIf entries:"
|
||||
git config --list --show-origin | grep -i include || true
|
||||
|
||||
# Create the worktree
|
||||
echo "Creating worktree..."
|
||||
git worktree add "../$WORKTREE_NAME" HEAD --detach
|
||||
|
||||
# Change to worktree directory
|
||||
cd "../$WORKTREE_NAME"
|
||||
|
||||
# Verify we're in a worktree
|
||||
echo "Verifying worktree gitdir:"
|
||||
cat .git
|
||||
|
||||
# Verify credentials are available in worktree by checking extraheader is configured
|
||||
echo "Checking credentials in worktree..."
|
||||
if git config --list --show-origin | grep -q "extraheader"; then
|
||||
echo "Credentials are configured in worktree"
|
||||
else
|
||||
echo "ERROR: Credentials are NOT configured in worktree"
|
||||
echo "Full git config:"
|
||||
git config --list --show-origin
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify fetch works in the worktree
|
||||
echo "Fetching in worktree..."
|
||||
git fetch origin
|
||||
|
||||
echo "Worktree credentials test passed!"
|
||||
@@ -98,6 +98,15 @@ inputs:
|
||||
github-server-url:
|
||||
description: 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
|
||||
required: false
|
||||
allow-unsafe-pr-checkout:
|
||||
description: >
|
||||
Required to check out fork pull request code from a workflow triggered by
|
||||
`pull_request_target` or `workflow_run`. These workflows run with the
|
||||
base repository's GITHUB_TOKEN, secrets, default-branch cache scope, and
|
||||
runner access; fetching and executing a fork's code in that trusted
|
||||
context commonly leads to "pwn request" vulnerabilities. Set to `true`
|
||||
only after reviewing the risks at https://gh.io/securely-using-pull_request_target.
|
||||
default: false
|
||||
outputs:
|
||||
ref:
|
||||
description: 'The branch, tag or SHA that was checked out'
|
||||
|
||||
52522
dist/index.js
vendored
52522
dist/index.js
vendored
File diff suppressed because one or more lines are too long
3
dist/package.json
vendored
Normal file
3
dist/package.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
fakeTimers: {},
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
testRunner: 'jest-circus/runner',
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
||||
24
jest.config.ts
Normal file
24
jest.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
roots: ['<rootDir>'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
diagnostics: {
|
||||
ignoreCodes: [151002]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
transformIgnorePatterns: ['node_modules/(?!(@actions)/)'],
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1'
|
||||
},
|
||||
verbose: true
|
||||
}
|
||||
804
package-lock.json
generated
804
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "checkout",
|
||||
"version": "5.0.0",
|
||||
"version": "7.0.0",
|
||||
"description": "checkout action",
|
||||
"type": "module",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc && ncc build && node lib/misc/generate-docs.js",
|
||||
"build": "tsc && ncc build src/main.ts -o dist && node lib/misc/generate-docs.js",
|
||||
"format": "prettier --write '**/*.ts'",
|
||||
"format-check": "prettier --check '**/*.ts'",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"test": "jest",
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
||||
"licensed-check": "src/misc/licensed-check.sh",
|
||||
"licensed-generate": "src/misc/licensed-generate.sh"
|
||||
},
|
||||
@@ -27,29 +28,30 @@
|
||||
"url": "https://github.com/actions/checkout/issues"
|
||||
},
|
||||
"homepage": "https://github.com/actions/checkout#readme",
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
"@actions/io": "^1.1.3",
|
||||
"@actions/tool-cache": "^2.0.1",
|
||||
"uuid": "^9.0.1"
|
||||
"@actions/core": "^3.0.1",
|
||||
"@actions/exec": "^3.0.0",
|
||||
"@actions/github": "^9.1.1",
|
||||
"@actions/io": "^3.0.2",
|
||||
"@actions/tool-cache": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^24.1.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
||||
"@typescript-eslint/parser": "^7.9.0",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"@vercel/ncc": "^0.44.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-github": "^4.10.2",
|
||||
"eslint-plugin-jest": "^28.8.2",
|
||||
"jest": "^29.7.0",
|
||||
"jest-circus": "^29.7.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "^3.3.3",
|
||||
"ts-jest": "^29.2.5",
|
||||
"js-yaml": "^4.2.0",
|
||||
"prettier": "^3.8.4",
|
||||
"ts-jest": "^29.4.11",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
108
release,tagset
108
release,tagset
@@ -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: ''
|
||||
@@ -5,12 +5,12 @@ import * as fs from 'fs'
|
||||
import * as io from '@actions/io'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import * as regexpHelper from './regexp-helper'
|
||||
import * as stateHelper from './state-helper'
|
||||
import * as urlHelper from './url-helper'
|
||||
import {v4 as uuid} from 'uuid'
|
||||
import {IGitCommandManager} from './git-command-manager'
|
||||
import {IGitSourceSettings} from './git-source-settings'
|
||||
import * as regexpHelper from './regexp-helper.js'
|
||||
import * as stateHelper from './state-helper.js'
|
||||
import * as urlHelper from './url-helper.js'
|
||||
import {randomUUID} from 'crypto'
|
||||
import {IGitCommandManager} from './git-command-manager.js'
|
||||
import {IGitSourceSettings} from './git-source-settings.js'
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
const SSH_COMMAND_KEY = 'core.sshCommand'
|
||||
@@ -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,
|
||||
@@ -89,7 +90,7 @@ class GitAuthHelper {
|
||||
// Create a temp home directory
|
||||
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
||||
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||
const uniqueId = uuid()
|
||||
const uniqueId = randomUUID()
|
||||
this.temporaryHomePath = path.join(runnerTemp, uniqueId)
|
||||
await fs.promises.mkdir(this.temporaryHomePath, {recursive: true})
|
||||
|
||||
@@ -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
|
||||
@@ -209,7 +255,7 @@ class GitAuthHelper {
|
||||
// Write key
|
||||
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
||||
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||
const uniqueId = uuid()
|
||||
const uniqueId = randomUUID()
|
||||
this.sshKeyPath = path.join(runnerTemp, uniqueId)
|
||||
stateHelper.setSshKeyPath(this.sshKeyPath)
|
||||
await fs.promises.mkdir(runnerTemp, {recursive: true})
|
||||
@@ -272,57 +318,127 @@ 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)
|
||||
|
||||
// Configure host includeIf for worktrees
|
||||
const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`
|
||||
await this.git.config(hostWorktreeIncludeKey, 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)
|
||||
|
||||
// Configure container includeIf for worktrees
|
||||
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`
|
||||
await this.git.config(
|
||||
containerWorktreeIncludeKey,
|
||||
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-${randomUUID()}.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 +451,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as exec from '@actions/exec'
|
||||
import * as fs from 'fs'
|
||||
import * as fshelper from './fs-helper'
|
||||
import * as fshelper from './fs-helper.js'
|
||||
import * as io from '@actions/io'
|
||||
import * as path from 'path'
|
||||
import * as refHelper from './ref-helper'
|
||||
import * as regexpHelper from './regexp-helper'
|
||||
import * as retryHelper from './retry-helper'
|
||||
import {GitVersion} from './git-version'
|
||||
import * as refHelper from './ref-helper.js'
|
||||
import * as regexpHelper from './regexp-helper.js'
|
||||
import * as retryHelper from './retry-helper.js'
|
||||
import {GitVersion} from './git-version.js'
|
||||
|
||||
// Auth header not supported before 2.9
|
||||
// Wire protocol v2 not supported before 2.18
|
||||
@@ -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(
|
||||
@@ -36,13 +37,13 @@ export interface IGitCommandManager {
|
||||
options: {
|
||||
filter?: string
|
||||
fetchDepth?: number
|
||||
fetchTags?: boolean
|
||||
showProgress?: boolean
|
||||
}
|
||||
): Promise<void>
|
||||
getDefaultBranch(repositoryUrl: string): Promise<string>
|
||||
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
|
||||
getWorkingDirectory(): string
|
||||
init(): Promise<void>
|
||||
init(objectFormat?: string): Promise<void>
|
||||
isDetached(): Promise<boolean>
|
||||
lfsFetch(ref: string): Promise<void>
|
||||
lfsInstall(): Promise<void>
|
||||
@@ -59,8 +60,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 +240,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')
|
||||
}
|
||||
@@ -256,14 +279,13 @@ class GitCommandManager {
|
||||
options: {
|
||||
filter?: string
|
||||
fetchDepth?: number
|
||||
fetchTags?: boolean
|
||||
showProgress?: boolean
|
||||
}
|
||||
): Promise<void> {
|
||||
const args = ['-c', 'protocol.version=2', 'fetch']
|
||||
if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
|
||||
args.push('--no-tags')
|
||||
}
|
||||
// Always use --no-tags for explicit control over tag fetching
|
||||
// Tags are fetched explicitly via refspec when needed
|
||||
args.push('--no-tags')
|
||||
|
||||
args.push('--prune', '--no-recurse-submodules')
|
||||
if (options.showProgress) {
|
||||
@@ -323,12 +345,33 @@ 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
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.execGit(['init', this.workingDirectory])
|
||||
async init(objectFormat?: string): Promise<void> {
|
||||
const args = ['init']
|
||||
if (objectFormat === 'sha256') {
|
||||
args.push('--object-format=sha256')
|
||||
}
|
||||
args.push(this.workingDirectory)
|
||||
|
||||
await this.execGit(args)
|
||||
}
|
||||
|
||||
async isDetached(): Promise<boolean> {
|
||||
@@ -455,6 +498,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 +542,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
|
||||
@@ -623,7 +734,19 @@ class GitCommandManager {
|
||||
}
|
||||
}
|
||||
// Set the user agent
|
||||
const gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
|
||||
let gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
|
||||
|
||||
// Append orchestration ID if set
|
||||
const orchId = process.env['ACTIONS_ORCHESTRATION_ID']
|
||||
if (orchId) {
|
||||
// Sanitize the orchestration ID to ensure it contains only valid characters
|
||||
// Valid characters: 0-9, a-z, _, -, .
|
||||
const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_')
|
||||
if (sanitizedId) {
|
||||
gitHttpUserAgent = `${gitHttpUserAgent} actions_orchestration_id/${sanitizedId}`
|
||||
}
|
||||
}
|
||||
|
||||
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
|
||||
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as assert from 'assert'
|
||||
import * as core from '@actions/core'
|
||||
import * as fs from 'fs'
|
||||
import * as fsHelper from './fs-helper'
|
||||
import * as fsHelper from './fs-helper.js'
|
||||
import * as io from '@actions/io'
|
||||
import * as path from 'path'
|
||||
import {IGitCommandManager} from './git-command-manager'
|
||||
import {IGitCommandManager} from './git-command-manager.js'
|
||||
|
||||
export async function prepareExistingDirectory(
|
||||
git: IGitCommandManager | undefined,
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fsHelper from './fs-helper'
|
||||
import * as gitAuthHelper from './git-auth-helper'
|
||||
import * as gitCommandManager from './git-command-manager'
|
||||
import * as gitDirectoryHelper from './git-directory-helper'
|
||||
import * as githubApiHelper from './github-api-helper'
|
||||
import * as fsHelper from './fs-helper.js'
|
||||
import * as gitAuthHelper from './git-auth-helper.js'
|
||||
import * as gitCommandManager from './git-command-manager.js'
|
||||
import * as gitDirectoryHelper from './git-directory-helper.js'
|
||||
import * as githubApiHelper from './github-api-helper.js'
|
||||
import * as io from '@actions/io'
|
||||
import * as path from 'path'
|
||||
import * as refHelper from './ref-helper'
|
||||
import * as stateHelper from './state-helper'
|
||||
import * as urlHelper from './url-helper'
|
||||
import * as refHelper from './ref-helper.js'
|
||||
import * as stateHelper from './state-helper.js'
|
||||
import * as urlHelper from './url-helper.js'
|
||||
import {
|
||||
MinimumGitSparseCheckoutVersion,
|
||||
IGitCommandManager
|
||||
} from './git-command-manager'
|
||||
import {IGitSourceSettings} from './git-source-settings'
|
||||
} from './git-command-manager.js'
|
||||
import {IGitSourceSettings} from './git-source-settings.js'
|
||||
|
||||
export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
// Repository URL
|
||||
@@ -109,8 +109,25 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
if (
|
||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||
) {
|
||||
core.startGroup('Determining repository object format')
|
||||
const objectFormatResult =
|
||||
await githubApiHelper.tryGetRepositoryObjectFormat(
|
||||
settings.authToken,
|
||||
settings.repositoryOwner,
|
||||
settings.repositoryName,
|
||||
settings.githubServerUrl,
|
||||
settings.commit
|
||||
)
|
||||
const objectFormat = objectFormatResult.succeeded
|
||||
? objectFormatResult.format
|
||||
: ''
|
||||
if (objectFormat === 'sha256') {
|
||||
core.info('Detected SHA-256 repository object format')
|
||||
}
|
||||
core.endGroup()
|
||||
|
||||
core.startGroup('Initializing the repository')
|
||||
await git.init()
|
||||
await git.init(objectFormat)
|
||||
await git.remoteAdd('origin', repositoryUrl)
|
||||
core.endGroup()
|
||||
}
|
||||
@@ -159,7 +176,6 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
const fetchOptions: {
|
||||
filter?: string
|
||||
fetchDepth?: number
|
||||
fetchTags?: boolean
|
||||
showProgress?: boolean
|
||||
} = {}
|
||||
|
||||
@@ -182,12 +198,35 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
|
||||
refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
||||
await git.fetch(refSpec, fetchOptions)
|
||||
|
||||
// Verify the ref now matches. For branches, the targeted fetch above brings
|
||||
// in the specific commit. For tags (fetched by ref), this will fail if
|
||||
// the tag was moved after the workflow was triggered.
|
||||
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
|
||||
throw new Error(
|
||||
`The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
|
||||
`The ref may have been updated after the workflow was triggered.`
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fetchOptions.fetchDepth = settings.fetchDepth
|
||||
fetchOptions.fetchTags = settings.fetchTags
|
||||
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
||||
const refSpec = refHelper.getRefSpec(
|
||||
settings.ref,
|
||||
settings.commit,
|
||||
settings.fetchTags
|
||||
)
|
||||
await git.fetch(refSpec, fetchOptions)
|
||||
|
||||
// For tags, verify the ref still points to the expected commit.
|
||||
// Tags are fetched by ref (not commit), so if a tag was moved after the
|
||||
// workflow was triggered, we would silently check out the wrong commit.
|
||||
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
|
||||
throw new Error(
|
||||
`The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
|
||||
`The ref may have been updated after the workflow was triggered.`
|
||||
)
|
||||
}
|
||||
}
|
||||
core.endGroup()
|
||||
|
||||
|
||||
@@ -118,4 +118,10 @@ export interface IGitSourceSettings {
|
||||
* User override on the GitHub Server/Host URL that hosts the repository to be cloned
|
||||
*/
|
||||
githubServerUrl: string | undefined
|
||||
|
||||
/**
|
||||
* Opt-in to allow checking out fork pull request code from a workflow
|
||||
* triggered by pull_request_target or workflow_run.
|
||||
*/
|
||||
allowUnsafePrCheckout: boolean
|
||||
}
|
||||
|
||||
@@ -4,13 +4,18 @@ import * as fs from 'fs'
|
||||
import * as github from '@actions/github'
|
||||
import * as io from '@actions/io'
|
||||
import * as path from 'path'
|
||||
import * as retryHelper from './retry-helper'
|
||||
import * as retryHelper from './retry-helper.js'
|
||||
import * as toolCache from '@actions/tool-cache'
|
||||
import {v4 as uuid} from 'uuid'
|
||||
import {getServerApiUrl} from './url-helper'
|
||||
import {randomUUID} from 'crypto'
|
||||
import {getServerApiUrl} from './url-helper.js'
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
|
||||
export interface RepositoryObjectFormatResult {
|
||||
format: string
|
||||
succeeded: boolean
|
||||
}
|
||||
|
||||
export async function downloadRepository(
|
||||
authToken: string,
|
||||
owner: string,
|
||||
@@ -34,7 +39,7 @@ export async function downloadRepository(
|
||||
|
||||
// Write archive to disk
|
||||
core.info('Writing archive to disk')
|
||||
const uniqueId = uuid()
|
||||
const uniqueId = randomUUID()
|
||||
const archivePath = IS_WINDOWS
|
||||
? path.join(repositoryPath, `${uniqueId}.zip`)
|
||||
: path.join(repositoryPath, `${uniqueId}.tar.gz`)
|
||||
@@ -122,6 +127,53 @@ export async function getDefaultBranch(
|
||||
})
|
||||
}
|
||||
|
||||
export async function tryGetRepositoryObjectFormat(
|
||||
authToken: string,
|
||||
owner: string,
|
||||
repo: string,
|
||||
baseUrl?: string,
|
||||
commit?: string
|
||||
): Promise<RepositoryObjectFormatResult> {
|
||||
const commitFormat = getObjectFormat(commit)
|
||||
if (commitFormat) {
|
||||
return {format: commitFormat, succeeded: true}
|
||||
}
|
||||
|
||||
try {
|
||||
const octokit = github.getOctokit(authToken, {
|
||||
baseUrl: getServerApiUrl(baseUrl)
|
||||
})
|
||||
const response = await octokit.request(
|
||||
'GET /repos/{owner}/{repo}/hash-algorithm',
|
||||
{owner, repo}
|
||||
)
|
||||
const hashAlgorithm = response.data.hash_algorithm
|
||||
if (hashAlgorithm === 'sha256' || hashAlgorithm === 'sha1') {
|
||||
return {format: hashAlgorithm, succeeded: true}
|
||||
}
|
||||
|
||||
core.debug(
|
||||
'Unable to determine repository object format from hash-algorithm endpoint'
|
||||
)
|
||||
return {format: '', succeeded: false}
|
||||
} catch (err) {
|
||||
core.debug(
|
||||
`Unable to determine repository object format from hash-algorithm endpoint: ${(err as any)?.message ?? err}`
|
||||
)
|
||||
return {format: '', succeeded: false}
|
||||
}
|
||||
}
|
||||
|
||||
function getObjectFormat(sha?: string): string {
|
||||
if (/^[0-9a-fA-F]{64}$/.test(sha || '')) {
|
||||
return 'sha256'
|
||||
}
|
||||
if (/^[0-9a-fA-F]{40}$/.test(sha || '')) {
|
||||
return 'sha1'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
async function downloadArchive(
|
||||
authToken: string,
|
||||
owner: string,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fsHelper from './fs-helper'
|
||||
import * as fsHelper from './fs-helper.js'
|
||||
import * as github from '@actions/github'
|
||||
import * as path from 'path'
|
||||
import * as workflowContextHelper from './workflow-context-helper'
|
||||
import {IGitSourceSettings} from './git-source-settings'
|
||||
import * as unsafePrCheckoutHelper from './unsafe-pr-checkout-helper.js'
|
||||
import * as workflowContextHelper from './workflow-context-helper.js'
|
||||
import {IGitSourceSettings} from './git-source-settings.js'
|
||||
|
||||
export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
const result = {} as unknown as IGitSourceSettings
|
||||
@@ -71,7 +72,7 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
}
|
||||
}
|
||||
// SHA?
|
||||
else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) {
|
||||
else if (result.ref.match(/^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$/)) {
|
||||
result.commit = result.ref
|
||||
result.ref = ''
|
||||
}
|
||||
@@ -161,5 +162,18 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
result.githubServerUrl = core.getInput('github-server-url')
|
||||
core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
|
||||
|
||||
// Allow unsafe PR checkout (opt-in for pull_request_target / workflow_run fork PRs)
|
||||
result.allowUnsafePrCheckout =
|
||||
(core.getInput('allow-unsafe-pr-checkout') || 'false').toUpperCase() ===
|
||||
'TRUE'
|
||||
core.debug(`allow unsafe PR checkout = ${result.allowUnsafePrCheckout}`)
|
||||
|
||||
unsafePrCheckoutHelper.assertSafePrCheckout({
|
||||
qualifiedRepository,
|
||||
ref: result.ref,
|
||||
commit: result.commit,
|
||||
allowUnsafePrCheckout: result.allowUnsafePrCheckout
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
18
src/main.ts
18
src/main.ts
@@ -1,9 +1,11 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as coreCommand from '@actions/core/lib/command'
|
||||
import * as gitSourceProvider from './git-source-provider'
|
||||
import * as inputHelper from './input-helper'
|
||||
import * as gitSourceProvider from './git-source-provider.js'
|
||||
import * as inputHelper from './input-helper.js'
|
||||
import * as path from 'path'
|
||||
import * as stateHelper from './state-helper'
|
||||
import * as stateHelper from './state-helper.js'
|
||||
import {fileURLToPath} from 'url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
@@ -11,10 +13,8 @@ async function run(): Promise<void> {
|
||||
|
||||
try {
|
||||
// Register problem matcher
|
||||
coreCommand.issueCommand(
|
||||
'add-matcher',
|
||||
{},
|
||||
path.join(__dirname, 'problem-matcher.json')
|
||||
core.info(
|
||||
`::add-matcher::${path.join(__dirname, 'problem-matcher.json')}`
|
||||
)
|
||||
|
||||
// Get sources
|
||||
@@ -22,7 +22,7 @@ async function run(): Promise<void> {
|
||||
core.setOutput('ref', sourceSettings.ref)
|
||||
} finally {
|
||||
// Unregister problem matcher
|
||||
coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '')
|
||||
core.info('::remove-matcher owner=checkout-git::')
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(`${(error as any)?.message ?? error}`)
|
||||
|
||||
@@ -2,6 +2,9 @@ import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import * as yaml from 'js-yaml'
|
||||
import {fileURLToPath} from 'url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
//
|
||||
// SUMMARY
|
||||
@@ -120,7 +123,7 @@ function updateUsage(
|
||||
}
|
||||
|
||||
updateUsage(
|
||||
'actions/checkout@v5',
|
||||
'actions/checkout@v7',
|
||||
path.join(__dirname, '..', '..', 'action.yml'),
|
||||
path.join(__dirname, '..', '..', 'README.md')
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {IGitCommandManager} from './git-command-manager'
|
||||
import {IGitCommandManager} from './git-command-manager.js'
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import {getServerApiUrl, isGhes} from './url-helper'
|
||||
import {getServerApiUrl, isGhes} from './url-helper.js'
|
||||
|
||||
export const tagsRefSpec = '+refs/tags/*:refs/tags/*'
|
||||
|
||||
@@ -76,55 +76,75 @@ export function getRefSpecForAllHistory(ref: string, commit: string): string[] {
|
||||
return result
|
||||
}
|
||||
|
||||
export function getRefSpec(ref: string, commit: string): string[] {
|
||||
export function getRefSpec(
|
||||
ref: string,
|
||||
commit: string,
|
||||
fetchTags?: boolean
|
||||
): string[] {
|
||||
if (!ref && !commit) {
|
||||
throw new Error('Args ref and commit cannot both be empty')
|
||||
}
|
||||
|
||||
const upperRef = (ref || '').toUpperCase()
|
||||
const result: string[] = []
|
||||
|
||||
// When fetchTags is true, always include the tags refspec
|
||||
if (fetchTags) {
|
||||
result.push(tagsRefSpec)
|
||||
}
|
||||
|
||||
// SHA
|
||||
if (commit) {
|
||||
// refs/heads
|
||||
if (upperRef.startsWith('REFS/HEADS/')) {
|
||||
const branch = ref.substring('refs/heads/'.length)
|
||||
return [`+${commit}:refs/remotes/origin/${branch}`]
|
||||
result.push(`+${commit}:refs/remotes/origin/${branch}`)
|
||||
}
|
||||
// refs/pull/
|
||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||
const branch = ref.substring('refs/pull/'.length)
|
||||
return [`+${commit}:refs/remotes/pull/${branch}`]
|
||||
result.push(`+${commit}:refs/remotes/pull/${branch}`)
|
||||
}
|
||||
// refs/tags/
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
return [`+${commit}:${ref}`]
|
||||
if (!fetchTags) {
|
||||
result.push(`+${ref}:${ref}`)
|
||||
}
|
||||
}
|
||||
// Otherwise no destination ref
|
||||
else {
|
||||
return [commit]
|
||||
result.push(commit)
|
||||
}
|
||||
}
|
||||
// Unqualified ref, check for a matching branch or tag
|
||||
else if (!upperRef.startsWith('REFS/')) {
|
||||
return [
|
||||
`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`,
|
||||
`+refs/tags/${ref}*:refs/tags/${ref}*`
|
||||
]
|
||||
result.push(`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`)
|
||||
if (!fetchTags) {
|
||||
result.push(`+refs/tags/${ref}*:refs/tags/${ref}*`)
|
||||
}
|
||||
}
|
||||
// refs/heads/
|
||||
else if (upperRef.startsWith('REFS/HEADS/')) {
|
||||
const branch = ref.substring('refs/heads/'.length)
|
||||
return [`+${ref}:refs/remotes/origin/${branch}`]
|
||||
result.push(`+${ref}:refs/remotes/origin/${branch}`)
|
||||
}
|
||||
// refs/pull/
|
||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||
const branch = ref.substring('refs/pull/'.length)
|
||||
return [`+${ref}:refs/remotes/pull/${branch}`]
|
||||
result.push(`+${ref}:refs/remotes/pull/${branch}`)
|
||||
}
|
||||
// refs/tags/
|
||||
else {
|
||||
return [`+${ref}:${ref}`]
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
if (!fetchTags) {
|
||||
result.push(`+${ref}:${ref}`)
|
||||
}
|
||||
}
|
||||
// Other refs
|
||||
else {
|
||||
result.push(`+${ref}:${ref}`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,8 +190,10 @@ export async function testRef(
|
||||
// refs/tags/
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
const tagName = ref.substring('refs/tags/'.length)
|
||||
// Use ^{commit} to dereference annotated tags to their underlying commit
|
||||
return (
|
||||
(await git.tagExists(tagName)) && commit === (await git.revParse(ref))
|
||||
(await git.tagExists(tagName)) &&
|
||||
commit === (await git.revParse(`${ref}^{commit}`))
|
||||
)
|
||||
}
|
||||
// Unexpected
|
||||
@@ -236,7 +258,9 @@ export async function checkCommitInfo(
|
||||
}
|
||||
|
||||
// Extract details from message
|
||||
const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/)
|
||||
const match = commitInfo.match(
|
||||
/Merge ([0-9a-f]{40}|[0-9a-f]{64}) into ([0-9a-f]{40}|[0-9a-f]{64})/
|
||||
)
|
||||
if (!match) {
|
||||
core.debug('Unexpected message format')
|
||||
return
|
||||
@@ -268,7 +292,7 @@ export async function checkCommitInfo(
|
||||
}
|
||||
}
|
||||
|
||||
function fromPayload(path: string): any {
|
||||
export function fromPayload(path: string): any {
|
||||
return select(github.context.payload, path)
|
||||
}
|
||||
|
||||
|
||||
88
src/unsafe-pr-checkout-helper.ts
Normal file
88
src/unsafe-pr-checkout-helper.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import * as github from '@actions/github'
|
||||
import {fromPayload} from './ref-helper.js'
|
||||
|
||||
const PR_REF_PATTERN = /^refs\/pull\/[0-9]+\/(?:head|merge)$/
|
||||
|
||||
export interface IUnsafePrCheckoutInput {
|
||||
qualifiedRepository: string
|
||||
ref: string
|
||||
commit: string | undefined
|
||||
allowUnsafePrCheckout: boolean
|
||||
}
|
||||
|
||||
export function assertSafePrCheckout(input: IUnsafePrCheckoutInput): void {
|
||||
if (input.allowUnsafePrCheckout) {
|
||||
return
|
||||
}
|
||||
|
||||
const eventName = github.context.eventName
|
||||
if (eventName !== 'pull_request_target' && eventName !== 'workflow_run') {
|
||||
return
|
||||
}
|
||||
|
||||
const baseRepoId = fromPayload('repository.id')
|
||||
if (typeof baseRepoId !== 'number') {
|
||||
return
|
||||
}
|
||||
|
||||
let prHeadRepoId: unknown
|
||||
let prHeadRepoFullName: unknown
|
||||
const prShas: string[] = []
|
||||
|
||||
if (eventName === 'pull_request_target') {
|
||||
prHeadRepoId = fromPayload('pull_request.head.repo.id')
|
||||
prHeadRepoFullName = fromPayload('pull_request.head.repo.full_name')
|
||||
pushIfSha(prShas, fromPayload('pull_request.head.sha'))
|
||||
pushIfSha(prShas, fromPayload('pull_request.merge_commit_sha'))
|
||||
} else {
|
||||
const wrEvent = fromPayload('workflow_run.event')
|
||||
if (typeof wrEvent !== 'string' || !wrEvent.startsWith('pull_request')) {
|
||||
return
|
||||
}
|
||||
prHeadRepoId = fromPayload('workflow_run.head_repository.id')
|
||||
prHeadRepoFullName = fromPayload('workflow_run.head_repository.full_name')
|
||||
pushIfSha(prShas, fromPayload('workflow_run.head_commit.id'))
|
||||
// For `pull_request_target`-triggered workflow_run, `head_sha` is the base
|
||||
// default branch SHA (not the PR head)
|
||||
if (wrEvent !== 'pull_request_target') {
|
||||
pushIfSha(prShas, fromPayload('workflow_run.head_sha'))
|
||||
}
|
||||
}
|
||||
|
||||
// (A) Fork PR?
|
||||
if (typeof prHeadRepoId !== 'number' || prHeadRepoId === baseRepoId) {
|
||||
return
|
||||
}
|
||||
|
||||
// (B) We cannot check for all fork PR refs so check to see
|
||||
// if the resolved input points to the fork PR sha we have in the payload
|
||||
const repositoryMatchesPrHead =
|
||||
typeof prHeadRepoFullName === 'string' &&
|
||||
input.qualifiedRepository.toLowerCase() === prHeadRepoFullName.toLowerCase()
|
||||
const refMatchesPullPattern = PR_REF_PATTERN.test(input.ref)
|
||||
const commitMatchesPrHeadSha =
|
||||
!!input.commit && prShas.includes(input.commit.toLowerCase())
|
||||
|
||||
if (
|
||||
!repositoryMatchesPrHead &&
|
||||
!refMatchesPullPattern &&
|
||||
!commitMatchesPrHeadSha
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Refusing to check out fork pull request code from a '${eventName}' workflow. ` +
|
||||
`This workflow runs with the base repository's GITHUB_TOKEN, secrets, default-branch ` +
|
||||
`cache scope, and runner access. Fetching and executing a fork's code in that trusted ` +
|
||||
`context commonly leads to "pwn request" vulnerabilities. To opt in, review the risks ` +
|
||||
`at https://gh.io/securely-using-pull_request_target and set 'allow-unsafe-pr-checkout: true' ` +
|
||||
`on the actions/checkout step.`
|
||||
)
|
||||
}
|
||||
|
||||
function pushIfSha(target: string[], value: unknown): void {
|
||||
if (typeof value === 'string' && value.length > 0) {
|
||||
target.push(value.toLowerCase())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as assert from 'assert'
|
||||
import {URL} from 'url'
|
||||
import {IGitSourceSettings} from './git-source-settings'
|
||||
import {IGitSourceSettings} from './git-source-settings.js'
|
||||
|
||||
export function getFetchUrl(settings: IGitSourceSettings): string {
|
||||
assert.ok(
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
"es6"
|
||||
],
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": ["__test__", "lib", "node_modules"]
|
||||
"exclude": ["__test__", "lib", "node_modules", "jest.config.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user