mirror of
https://github.com/actions/checkout.git
synced 2026-06-11 16:43:48 +08:00
Compare commits
9 Commits
ed69f3bbdd
...
takost/tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b8f79e013 | ||
|
|
d651e32721 | ||
|
|
8f0d2fbedc | ||
|
|
418530e26c | ||
|
|
0cf6e008d0 | ||
|
|
9a43814785 | ||
|
|
317b5f36ae | ||
|
|
efb66ddacf | ||
|
|
3e9f24a519 |
20
.github/dependabot.yml
vendored
20
.github/dependabot.yml
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
minor-npm-dependencies:
|
||||
# NPM: Only group minor and patch updates (we want to carefully review major updates)
|
||||
update-types: [minor, patch]
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
minor-actions-dependencies:
|
||||
# GitHub Actions: Only group minor and patch updates (we want to carefully review major updates)
|
||||
update-types: [minor, patch]
|
||||
10
.github/workflows/check-dist.yml
vendored
10
.github/workflows/check-dist.yml
vendored
@@ -22,12 +22,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set Node.js 24.x
|
||||
uses: actions/setup-node@v4
|
||||
- name: Set Node.js 20.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 24.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# If dist/ was different than expected, upload the expected version as an artifact
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
|
||||
with:
|
||||
name: dist
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -39,10 +39,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -55,4 +55,4 @@ jobs:
|
||||
- run: rm -rf dist # We want code scanning to analyze lib instead (individual .js files)
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
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@v6
|
||||
- uses: actions/checkout@v3
|
||||
- run: npm ci
|
||||
- run: npm run licensed-check
|
||||
20
.github/workflows/publish-immutable-actions.yml
vendored
20
.github/workflows/publish-immutable-actions.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: 'Publish Immutable Action Version'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checking out
|
||||
uses: actions/checkout@v6
|
||||
- name: Publish
|
||||
id: publish
|
||||
uses: actions/publish-immutable-action@0.0.3
|
||||
103
.github/workflows/test.yml
vendored
103
.github/workflows/test.yml
vendored
@@ -7,19 +7,14 @@ on:
|
||||
- main
|
||||
- releases/*
|
||||
|
||||
|
||||
# Note that when you see patterns like "ref: test-data/v2/basic" within this workflow,
|
||||
# these refer to "test-data" branches on this actions/checkout repo.
|
||||
# (For example, test-data/v2/basic -> https://github.com/actions/checkout/tree/test-data/v2/basic)
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 24.x
|
||||
- uses: actions/checkout@v6
|
||||
node-version: 20.x
|
||||
- uses: actions/checkout@v3
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run format-check
|
||||
@@ -37,7 +32,7 @@ jobs:
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Basic checkout
|
||||
- name: Checkout basic
|
||||
@@ -87,17 +82,6 @@ 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: ./
|
||||
@@ -111,16 +95,6 @@ jobs:
|
||||
- name: Verify sparse checkout
|
||||
run: __test__/verify-sparse-checkout.sh
|
||||
|
||||
# Disabled sparse checkout in existing checkout
|
||||
- name: Disabled sparse checkout
|
||||
uses: ./
|
||||
with:
|
||||
path: sparse-checkout
|
||||
|
||||
- name: Verify disabled sparse checkout
|
||||
shell: bash
|
||||
run: set -x && ls -l sparse-checkout/src/git-command-manager.ts
|
||||
|
||||
# Sparse checkout (non-cone mode)
|
||||
- name: Sparse checkout (non-cone mode)
|
||||
uses: ./
|
||||
@@ -176,22 +150,6 @@ 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'
|
||||
@@ -217,7 +175,7 @@ jobs:
|
||||
test-proxy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/actions/test-ubuntu-git:main.20240221.114913.703z
|
||||
image: alpine/git:latest
|
||||
options: --dns 127.0.0.1
|
||||
services:
|
||||
squid-proxy:
|
||||
@@ -229,7 +187,7 @@ jobs:
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
@@ -261,7 +219,7 @@ jobs:
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
@@ -284,20 +242,20 @@ jobs:
|
||||
path: basic
|
||||
- name: Verify basic
|
||||
run: __test__/verify-basic.sh --archive
|
||||
|
||||
|
||||
test-git-container:
|
||||
runs-on: ubuntu-latest
|
||||
container: bitnami/git:latest
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: localClone
|
||||
path: v3
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
uses: ./localClone
|
||||
uses: ./v3
|
||||
with:
|
||||
ref: test-data/v2/basic
|
||||
- name: Verify basic
|
||||
@@ -318,40 +276,7 @@ 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 v6
|
||||
uses: actions/checkout@v6
|
||||
- name: Fix Checkout v3
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: localClone
|
||||
|
||||
test-output:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: actions-checkout
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
id: checkout
|
||||
uses: ./actions-checkout
|
||||
with:
|
||||
path: cloned-using-local-action
|
||||
ref: test-data/v2/basic
|
||||
|
||||
# Verify output
|
||||
- name: Verify output
|
||||
run: |
|
||||
echo "Commit: ${{ steps.checkout.outputs.commit }}"
|
||||
echo "Ref: ${{ steps.checkout.outputs.ref }}"
|
||||
|
||||
if [ "${{ steps.checkout.outputs.ref }}" != "test-data/v2/basic" ]; then
|
||||
echo "Expected ref to be test-data/v2/basic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${{ steps.checkout.outputs.commit }}" != "82f71901cf8c021332310dcc8cdba84c4193ff5d" ]; then
|
||||
echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d"
|
||||
exit 1
|
||||
fi
|
||||
path: v3
|
||||
10
.github/workflows/update-main-version.yml
vendored
10
.github/workflows/update-main-version.yml
vendored
@@ -11,7 +11,6 @@ on:
|
||||
type: choice
|
||||
description: The major version to update
|
||||
options:
|
||||
- v5
|
||||
- v4
|
||||
- v3
|
||||
- v2
|
||||
@@ -20,16 +19,13 @@ jobs:
|
||||
tag:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# 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@v6
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Git config
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config user.name github-actions
|
||||
git config user.email github-actions@github.com
|
||||
- name: Tag new target
|
||||
run: git tag -f ${{ github.event.inputs.major_version }} ${{ github.event.inputs.target }}
|
||||
- name: Push new tag
|
||||
|
||||
59
.github/workflows/update-test-ubuntu-git.yml
vendored
59
.github/workflows/update-test-ubuntu-git.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Publish test-ubuntu-git Container
|
||||
|
||||
on:
|
||||
# Use an on demand workflow trigger.
|
||||
# (Forked copies of actions/checkout won't have permission to update GHCR.io/actions,
|
||||
# so avoid trigger events that run automatically.)
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
description: 'Publish to ghcr.io? (main branch only)'
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: actions/test-ubuntu-git
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
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.
|
||||
- name: Log in to the ghcr.io container registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Format Timestamp
|
||||
id: timestamp
|
||||
# Use `date` with a custom format to achieve the key=value format GITHUB_OUTPUT expects.
|
||||
run: date -u "+now=%Y%m%d.%H%M%S.%3NZ" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Issue Image Publish Warning
|
||||
if: ${{ inputs.publish && github.ref_name != 'main' }}
|
||||
run: echo "::warning::test-ubuntu-git images can only be published from the actions/checkout 'main' branch. Workflow will continue with push/publish disabled."
|
||||
|
||||
# Use `docker/build-push-action` to build (and optionally publish) the image.
|
||||
- name: Build Docker Image (with optional Push)
|
||||
uses: docker/build-push-action@v6.5.0
|
||||
with:
|
||||
context: .
|
||||
file: images/test-ubuntu-git.Dockerfile
|
||||
# For now, attempts to push to ghcr.io must target the `main` branch.
|
||||
# In the future, consider also allowing attempts from `releases/*` branches.
|
||||
push: ${{ inputs.publish && github.ref_name == 'main' }}
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}.${{ steps.timestamp.outputs.now }}
|
||||
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: 6.0.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: 2.1.0
|
||||
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-3.0.2.dep.yml
generated
Normal file
32
.licenses/npm/@actions/http-client-3.0.2.dep.yml
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "@actions/http-client"
|
||||
version: 3.0.2
|
||||
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/@fastify/busboy.dep.yml
generated
2
.licenses/npm/@fastify/busboy.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@fastify/busboy"
|
||||
version: 2.1.1
|
||||
version: 2.0.0
|
||||
type: npm
|
||||
summary: A streaming parser for HTML form data for node.js
|
||||
homepage:
|
||||
|
||||
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: 3.0.4
|
||||
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: 4.2.4
|
||||
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: 7.0.6
|
||||
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: 5.0.6
|
||||
type: npm
|
||||
summary: GitHub GraphQL API client for browsers and Node
|
||||
homepage:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@octokit/openapi-types"
|
||||
version: 20.0.0
|
||||
version: 18.1.1
|
||||
type: npm
|
||||
summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com
|
||||
homepage:
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/plugin-paginate-rest"
|
||||
version: 9.2.2
|
||||
version: 6.1.2
|
||||
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: 7.2.3
|
||||
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: 3.0.3
|
||||
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: 6.2.8
|
||||
type: npm
|
||||
summary: Send parameterized requests to GitHub's APIs with sensible defaults in browsers
|
||||
and Node
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
---
|
||||
name: "@octokit/openapi-types"
|
||||
version: 22.1.0
|
||||
name: "@octokit/tsconfig"
|
||||
version: 1.0.2
|
||||
type: npm
|
||||
summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com
|
||||
summary: TypeScript configuration for Octokit packages
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |-
|
||||
Copyright 2020 Gregor Martynus
|
||||
text: |
|
||||
MIT License Copyright (c) 2020 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 shall be included in all copies or substantial portions of the Software.
|
||||
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
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@octokit/types"
|
||||
version: 12.6.0
|
||||
version: 10.0.0
|
||||
type: npm
|
||||
summary: Shared TypeScript definitions for Octokit projects
|
||||
homepage:
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@octokit/types"
|
||||
version: 13.4.1
|
||||
version: 9.3.2
|
||||
type: npm
|
||||
summary: Shared TypeScript definitions for Octokit projects
|
||||
homepage:
|
||||
40
.licenses/npm/is-plain-object.dep.yml
generated
Normal file
40
.licenses/npm/is-plain-object.dep.yml
generated
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: is-plain-object
|
||||
version: 5.0.0
|
||||
type: npm
|
||||
summary: Returns true if an object was created by the `Object` constructor, or Object.create(null).
|
||||
homepage: https://github.com/jonschlinkert/is-plain-object
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2017, Jon Schlinkert.
|
||||
|
||||
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: |-
|
||||
Copyright © 2019, [Jon Schlinkert](https://github.com/jonschlinkert).
|
||||
Released under the [MIT License](LICENSE).
|
||||
|
||||
***
|
||||
|
||||
_This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.8.0, on April 28, 2019._
|
||||
notices: []
|
||||
56
.licenses/npm/node-fetch.dep.yml
generated
Normal file
56
.licenses/npm/node-fetch.dep.yml
generated
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: node-fetch
|
||||
version: 2.7.0
|
||||
type: npm
|
||||
summary: A light-weight module that brings window.fetch to node.js
|
||||
homepage: https://github.com/bitinn/node-fetch
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |+
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 David Frank
|
||||
|
||||
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
|
||||
|
||||
[npm-image]: https://flat.badgen.net/npm/v/node-fetch
|
||||
[npm-url]: https://www.npmjs.com/package/node-fetch
|
||||
[travis-image]: https://flat.badgen.net/travis/bitinn/node-fetch
|
||||
[travis-url]: https://travis-ci.org/bitinn/node-fetch
|
||||
[codecov-image]: https://flat.badgen.net/codecov/c/github/bitinn/node-fetch/master
|
||||
[codecov-url]: https://codecov.io/gh/bitinn/node-fetch
|
||||
[install-size-image]: https://flat.badgen.net/packagephobia/install/node-fetch
|
||||
[install-size-url]: https://packagephobia.now.sh/result?p=node-fetch
|
||||
[discord-image]: https://img.shields.io/discord/619915844268326952?color=%237289DA&label=Discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/Zxbndcm
|
||||
[opencollective-image]: https://opencollective.com/node-fetch/backers.svg
|
||||
[opencollective-url]: https://opencollective.com/node-fetch
|
||||
[whatwg-fetch]: https://fetch.spec.whatwg.org/
|
||||
[response-init]: https://fetch.spec.whatwg.org/#responseinit
|
||||
[node-readable]: https://nodejs.org/api/stream.html#stream_readable_streams
|
||||
[mdn-headers]: https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
||||
[LIMITS.md]: https://github.com/bitinn/node-fetch/blob/master/LIMITS.md
|
||||
[ERROR-HANDLING.md]: https://github.com/bitinn/node-fetch/blob/master/ERROR-HANDLING.md
|
||||
[UPGRADE-GUIDE.md]: https://github.com/bitinn/node-fetch/blob/master/UPGRADE-GUIDE.md
|
||||
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: 6.3.0
|
||||
type: npm
|
||||
summary: The semantic version parser used by npm.
|
||||
homepage:
|
||||
|
||||
30
.licenses/npm/tr46.dep.yml
generated
Normal file
30
.licenses/npm/tr46.dep.yml
generated
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: tr46
|
||||
version: 0.0.3
|
||||
type: npm
|
||||
summary: An implementation of the Unicode TR46 spec
|
||||
homepage: https://github.com/Sebmaster/tr46.js#readme
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: Auto-generated MIT license text
|
||||
text: |
|
||||
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/undici.dep.yml
generated
2
.licenses/npm/undici.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: undici
|
||||
version: 5.29.0
|
||||
version: 5.25.4
|
||||
type: npm
|
||||
summary: An HTTP/1.1 client, written from scratch for Node.js
|
||||
homepage: https://undici.nodejs.org
|
||||
|
||||
2
.licenses/npm/universal-user-agent.dep.yml
generated
2
.licenses/npm/universal-user-agent.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: universal-user-agent
|
||||
version: 6.0.1
|
||||
version: 6.0.0
|
||||
type: npm
|
||||
summary: Get a user agent string in both browser and node
|
||||
homepage:
|
||||
|
||||
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: []
|
||||
23
.licenses/npm/webidl-conversions.dep.yml
generated
Normal file
23
.licenses/npm/webidl-conversions.dep.yml
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: webidl-conversions
|
||||
version: 3.0.1
|
||||
type: npm
|
||||
summary: Implements the WebIDL algorithms for converting to and from JavaScript values
|
||||
homepage: https://github.com/jsdom/webidl-conversions#readme
|
||||
license: bsd-2-clause
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |
|
||||
# The BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2014, Domenic Denicola
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
notices: []
|
||||
32
.licenses/npm/whatwg-url.dep.yml
generated
Normal file
32
.licenses/npm/whatwg-url.dep.yml
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: whatwg-url
|
||||
version: 5.0.0
|
||||
type: npm
|
||||
summary: An implementation of the WHATWG URL Standard's URL API and parsing machinery
|
||||
homepage: https://github.com/jsdom/whatwg-url#readme
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.txt
|
||||
text: |
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015–2016 Sebastian Mayr
|
||||
|
||||
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: []
|
||||
74
CHANGELOG.md
74
CHANGELOG.md
@@ -1,79 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
* docs: update README.md by @motss in https://github.com/actions/checkout/pull/1971
|
||||
* Add internal repos for checking out multiple repositories by @mouismail in https://github.com/actions/checkout/pull/1977
|
||||
* Documentation update - add recommended permissions to Readme by @benwells in https://github.com/actions/checkout/pull/2043
|
||||
* Adjust positioning of user email note and permissions heading by @joshmgross in https://github.com/actions/checkout/pull/2044
|
||||
* Update README.md by @nebuk89 in https://github.com/actions/checkout/pull/2194
|
||||
* Update CODEOWNERS for actions by @TingluoHuang in https://github.com/actions/checkout/pull/2224
|
||||
* Update package dependencies by @salmanmkc in https://github.com/actions/checkout/pull/2236
|
||||
|
||||
## v4.2.2
|
||||
* `url-helper.ts` now leverages well-known environment variables by @jww3 in https://github.com/actions/checkout/pull/1941
|
||||
* Expand unit test coverage for `isGhes` by @jww3 in https://github.com/actions/checkout/pull/1946
|
||||
|
||||
## v4.2.1
|
||||
* Check out other refs/* by commit if provided, fall back to ref by @orhantoy in https://github.com/actions/checkout/pull/1924
|
||||
|
||||
## v4.2.0
|
||||
|
||||
* Add Ref and Commit outputs by @lucacome in https://github.com/actions/checkout/pull/1180
|
||||
* Dependency updates by @dependabot- https://github.com/actions/checkout/pull/1777, https://github.com/actions/checkout/pull/1872
|
||||
|
||||
## v4.1.7
|
||||
* Bump the minor-npm-dependencies group across 1 directory with 4 updates by @dependabot in https://github.com/actions/checkout/pull/1739
|
||||
* Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/actions/checkout/pull/1697
|
||||
* Check out other refs/* by commit by @orhantoy in https://github.com/actions/checkout/pull/1774
|
||||
* Pin actions/checkout's own workflows to a known, good, stable version. by @jww3 in https://github.com/actions/checkout/pull/1776
|
||||
|
||||
## v4.1.6
|
||||
* Check platform to set archive extension appropriately by @cory-miller in https://github.com/actions/checkout/pull/1732
|
||||
|
||||
## v4.1.5
|
||||
* Update NPM dependencies by @cory-miller in https://github.com/actions/checkout/pull/1703
|
||||
* Bump github/codeql-action from 2 to 3 by @dependabot in https://github.com/actions/checkout/pull/1694
|
||||
* Bump actions/setup-node from 1 to 4 by @dependabot in https://github.com/actions/checkout/pull/1696
|
||||
* Bump actions/upload-artifact from 2 to 4 by @dependabot in https://github.com/actions/checkout/pull/1695
|
||||
* README: Suggest `user.email` to be `41898282+github-actions[bot]@users.noreply.github.com` by @cory-miller in https://github.com/actions/checkout/pull/1707
|
||||
|
||||
## v4.1.4
|
||||
- Disable `extensions.worktreeConfig` when disabling `sparse-checkout` by @jww3 in https://github.com/actions/checkout/pull/1692
|
||||
- Add dependabot config by @cory-miller in https://github.com/actions/checkout/pull/1688
|
||||
- Bump the minor-actions-dependencies group with 2 updates by @dependabot in https://github.com/actions/checkout/pull/1693
|
||||
- Bump word-wrap from 1.2.3 to 1.2.5 by @dependabot in https://github.com/actions/checkout/pull/1643
|
||||
|
||||
## v4.1.3
|
||||
- Check git version before attempting to disable `sparse-checkout` by @jww3 in https://github.com/actions/checkout/pull/1656
|
||||
- Add SSH user parameter by @cory-miller in https://github.com/actions/checkout/pull/1685
|
||||
- Update `actions/checkout` version in `update-main-version.yml` by @jww3 in https://github.com/actions/checkout/pull/1650
|
||||
|
||||
## v4.1.2
|
||||
- Fix: Disable sparse checkout whenever `sparse-checkout` option is not present @dscho in https://github.com/actions/checkout/pull/1598
|
||||
|
||||
## v4.1.1
|
||||
- Correct link to GitHub Docs by @peterbe in https://github.com/actions/checkout/pull/1511
|
||||
- Link to release page from what's new section by @cory-miller in https://github.com/actions/checkout/pull/1514
|
||||
|
||||
## v4.1.0
|
||||
- [Add support for partial checkout filters](https://github.com/actions/checkout/pull/1396)
|
||||
|
||||
|
||||
163
README.md
163
README.md
@@ -1,58 +1,26 @@
|
||||
[](https://github.com/actions/checkout/actions/workflows/test.yml)
|
||||
|
||||
# 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.
|
||||
|
||||
Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. Set `fetch-depth: 0` to fetch all history for all branches and tags. Refer [here](https://docs.github.com/actions/using-workflows/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events.
|
||||
Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. Set `fetch-depth: 0` to fetch all history for all branches and tags. Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events.
|
||||
|
||||
The auth token is persisted in the local git config. This enables your scripts to run authenticated git commands. The token is removed during post-job cleanup. Set `persist-credentials: false` to opt-out.
|
||||
|
||||
When Git 2.18 or higher is not in your PATH, falls back to the REST API to download the files.
|
||||
|
||||
### Note
|
||||
|
||||
Thank you for your interest in this GitHub action, however, right now we are not taking contributions.
|
||||
|
||||
We continue to focus our resources on strategic areas that help our customers be successful while making developers' lives easier. While GitHub Actions remains a key part of this vision, we are allocating resources towards other areas of Actions and are not taking contributions to this repository at this time. The GitHub public roadmap is the best place to follow along for any updates on features we’re working on and what stage they’re in.
|
||||
|
||||
We are taking the following steps to better direct requests related to GitHub Actions, including:
|
||||
|
||||
1. We will be directing questions and support requests to our [Community Discussions area](https://github.com/orgs/community/discussions/categories/actions)
|
||||
|
||||
2. High Priority bugs can be reported through Community Discussions or you can report these to our support team https://support.github.com/contact/bug-report.
|
||||
|
||||
3. Security Issues should be handled as per our [security.md](security.md)
|
||||
|
||||
We will still provide security updates for this project and fix major breaking changes during this time.
|
||||
|
||||
You are welcome to still raise bugs in this repo.
|
||||
|
||||
# What's new
|
||||
|
||||
Please refer to the [release page](https://github.com/actions/checkout/releases/latest) for the latest release notes.
|
||||
- Updated default runtime to node20
|
||||
- This requires a minimum Actions Runner version of [v2.308.0](https://github.com/actions/runner/releases/tag/v2.308.0).
|
||||
- Added support for fetching without the `--progress` option
|
||||
|
||||
# Usage
|
||||
|
||||
<!-- start usage -->
|
||||
```yaml
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Repository name with owner. For example, actions/checkout
|
||||
# Default: ${{ github.repository }}
|
||||
@@ -96,11 +64,6 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
# 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: ''
|
||||
@@ -160,44 +123,28 @@ 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: ''
|
||||
|
||||
# Path to a local directory used as a reference cache for Git clones. Over time,
|
||||
# this directory will contain bare clones of the checked-out repositories (and
|
||||
# their submodules). Using this significantly reduces network bandwidth and speeds
|
||||
# up clones.
|
||||
reference-cache: ''
|
||||
```
|
||||
<!-- end usage -->
|
||||
|
||||
# Scenarios
|
||||
|
||||
- [Checkout V5](#checkout-v5)
|
||||
- [What's new](#whats-new)
|
||||
- [Checkout V4](#checkout-v4)
|
||||
- [Note](#note)
|
||||
- [What's new](#whats-new-1)
|
||||
- [Usage](#usage)
|
||||
- [Scenarios](#scenarios)
|
||||
- [Fetch only the root files](#fetch-only-the-root-files)
|
||||
- [Fetch only the root files and `.github` and `src` folder](#fetch-only-the-root-files-and-github-and-src-folder)
|
||||
- [Fetch only a single file](#fetch-only-a-single-file)
|
||||
- [Fetch all history for all tags and branches](#fetch-all-history-for-all-tags-and-branches)
|
||||
- [Checkout a different branch](#checkout-a-different-branch)
|
||||
- [Checkout HEAD^](#checkout-head)
|
||||
- [Checkout multiple repos (side by side)](#checkout-multiple-repos-side-by-side)
|
||||
- [Checkout multiple repos (nested)](#checkout-multiple-repos-nested)
|
||||
- [Checkout multiple repos (private)](#checkout-multiple-repos-private)
|
||||
- [Checkout pull request HEAD commit instead of merge commit](#checkout-pull-request-head-commit-instead-of-merge-commit)
|
||||
- [Checkout pull request on closed event](#checkout-pull-request-on-closed-event)
|
||||
- [Push a commit using the built-in token](#push-a-commit-using-the-built-in-token)
|
||||
- [Push a commit to a PR using the built-in token](#push-a-commit-to-a-pr-using-the-built-in-token)
|
||||
- [Recommended permissions](#recommended-permissions)
|
||||
- [License](#license)
|
||||
- [Fetch only the root files](#Fetch-only-the-root-files)
|
||||
- [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder)
|
||||
- [Fetch only a single file](#Fetch-only-a-single-file)
|
||||
- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches)
|
||||
- [Checkout a different branch](#Checkout-a-different-branch)
|
||||
- [Checkout HEAD^](#Checkout-HEAD)
|
||||
- [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side)
|
||||
- [Checkout multiple repos (nested)](#Checkout-multiple-repos-nested)
|
||||
- [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
- [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit)
|
||||
- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
|
||||
- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token)
|
||||
|
||||
## Fetch only the root files
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .
|
||||
```
|
||||
@@ -205,7 +152,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@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github
|
||||
@@ -215,7 +162,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Fetch only a single file
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
README.md
|
||||
@@ -225,7 +172,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@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
```
|
||||
@@ -233,7 +180,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Checkout a different branch
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: my-branch
|
||||
```
|
||||
@@ -241,7 +188,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Checkout HEAD^
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- run: git checkout HEAD^
|
||||
@@ -251,42 +198,42 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: main
|
||||
|
||||
- name: Checkout tools repo
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: my-org/my-tools
|
||||
path: my-tools
|
||||
```
|
||||
> - If your secondary repository is private or internal you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
> - If your secondary repository is private you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
|
||||
## Checkout multiple repos (nested)
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout tools repo
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: my-org/my-tools
|
||||
path: my-tools
|
||||
```
|
||||
> - If your secondary repository is private or internal you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
> - If your secondary repository is private you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
|
||||
## Checkout multiple repos (private)
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: main
|
||||
|
||||
- name: Checkout private tools
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: my-org/my-private-tools
|
||||
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
|
||||
@@ -299,7 +246,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@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
```
|
||||
@@ -315,7 +262,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
```
|
||||
|
||||
## Push a commit using the built-in token
|
||||
@@ -326,51 +273,15 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
date > generated.txt
|
||||
# Note: the following account information will not work on GHES
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config user.name github-actions
|
||||
git config user.email github-actions@github.com
|
||||
git add .
|
||||
git commit -m "generated"
|
||||
git push
|
||||
```
|
||||
*NOTE:* The user email is `{user.id}+{user.login}@users.noreply.github.com`. See users API: https://api.github.com/users/github-actions%5Bbot%5D
|
||||
|
||||
## Push a commit to a PR using the built-in token
|
||||
|
||||
In a pull request trigger, `ref` is required as GitHub Actions checks out in detached HEAD mode, meaning it doesn’t check out your branch by default.
|
||||
|
||||
```yaml
|
||||
on: pull_request
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- run: |
|
||||
date > generated.txt
|
||||
# Note: the following account information will not work on GHES
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git commit -m "generated"
|
||||
git push
|
||||
```
|
||||
|
||||
*NOTE:* The user email is `{user.id}+{user.login}@users.noreply.github.com`. See users API: https://api.github.com/users/github-actions%5Bbot%5D
|
||||
|
||||
# Recommended permissions
|
||||
|
||||
When using the `checkout` action in your GitHub Actions workflow, it is recommended to set the following `GITHUB_TOKEN` permissions to ensure proper functionality, unless alternative auth is provided via the `token` or `ssh-key` inputs:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read
|
||||
```
|
||||
|
||||
# License
|
||||
|
||||
|
||||
@@ -86,29 +86,16 @@ describe('git-auth-helper tests', () => {
|
||||
// Act
|
||||
await authHelper.configureAuth()
|
||||
|
||||
// Assert config - check that .git/config contains includeIf entries
|
||||
const localConfigContent = (
|
||||
// Assert config
|
||||
const configContent = (
|
||||
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(
|
||||
credentialsContent.indexOf(
|
||||
configContent.indexOf(
|
||||
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||
)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
@@ -133,7 +120,7 @@ describe('git-auth-helper tests', () => {
|
||||
'inject https://github.com as github server url'
|
||||
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
|
||||
await testAuthHeader(
|
||||
configureAuth_AcceptsGitHubServerUrlSetToGHEC,
|
||||
configureAuth_AcceptsGitHubServerUrl,
|
||||
'https://github.com'
|
||||
)
|
||||
})
|
||||
@@ -154,17 +141,12 @@ describe('git-auth-helper tests', () => {
|
||||
// Act
|
||||
await authHelper.configureAuth()
|
||||
|
||||
// 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)
|
||||
// Assert config
|
||||
const configContent = (
|
||||
await fs.promises.readFile(localGitConfigPath)
|
||||
).toString()
|
||||
expect(
|
||||
credentialsContent.indexOf(
|
||||
configContent.indexOf(
|
||||
`http.https://github.com/.extraheader AUTHORIZATION`
|
||||
)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
@@ -269,16 +251,13 @@ describe('git-auth-helper tests', () => {
|
||||
expectedSshCommand
|
||||
)
|
||||
|
||||
// Assert git config
|
||||
// Asserty git config
|
||||
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter(x => x)
|
||||
// Should have includeIf entries pointing to credentials file
|
||||
expect(gitConfigLines.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0)
|
||||
).toBeTruthy()
|
||||
expect(gitConfigLines).toHaveLength(1)
|
||||
expect(gitConfigLines[0]).toMatch(/^http\./)
|
||||
})
|
||||
|
||||
const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
|
||||
@@ -440,20 +419,8 @@ 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(
|
||||
credentialsContent.indexOf(
|
||||
configContent.indexOf(
|
||||
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||
)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
@@ -496,20 +463,8 @@ 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(
|
||||
credentialsContent.indexOf(
|
||||
configContent.indexOf(
|
||||
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||
)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
@@ -595,15 +550,15 @@ describe('git-auth-helper tests', () => {
|
||||
await authHelper.configureSubmoduleAuth()
|
||||
|
||||
// Assert
|
||||
// Should configure insteadOf (2 calls for two values)
|
||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
|
||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
|
||||
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
||||
/unset-all.*insteadOf/
|
||||
)
|
||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(
|
||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
|
||||
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
|
||||
/url.*insteadOf.*git@github.com:/
|
||||
)
|
||||
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
|
||||
expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
|
||||
/url.*insteadOf.*org-123456@github.com:/
|
||||
)
|
||||
}
|
||||
@@ -634,12 +589,12 @@ describe('git-auth-helper tests', () => {
|
||||
await authHelper.configureSubmoduleAuth()
|
||||
|
||||
// Assert
|
||||
// Should configure sshCommand (1 call)
|
||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
|
||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
|
||||
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
||||
/unset-all.*insteadOf/
|
||||
)
|
||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/)
|
||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
|
||||
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -705,201 +660,19 @@ describe('git-auth-helper tests', () => {
|
||||
await setup(removeAuth_removesToken)
|
||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
await authHelper.configureAuth()
|
||||
|
||||
// Verify includeIf entries exist in local config
|
||||
let localConfigContent = (
|
||||
let gitConfigContent = (
|
||||
await fs.promises.readFile(localGitConfigPath)
|
||||
).toString()
|
||||
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)
|
||||
expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
|
||||
|
||||
// Act
|
||||
await authHelper.removeAuth()
|
||||
|
||||
// Assert all includeIf entries removed from local git config
|
||||
localConfigContent = (
|
||||
// Assert git config
|
||||
gitConfigContent = (
|
||||
await fs.promises.readFile(localGitConfigPath)
|
||||
).toString()
|
||||
expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0)
|
||||
expect(
|
||||
localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
|
||||
).toBeLessThan(0)
|
||||
expect(
|
||||
localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
|
||||
).toBeLessThan(0)
|
||||
expect(localConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
|
||||
expect(localConfigContent.indexOf(containerCredentialsPath)).toBeLessThan(0)
|
||||
|
||||
// Assert credentials config file deleted
|
||||
credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||
)
|
||||
expect(credentialsFiles.length).toBe(0)
|
||||
|
||||
// Verify credentials file no longer exists on disk
|
||||
try {
|
||||
await fs.promises.stat(credentialsFilePath)
|
||||
throw new Error('Credentials file should have been deleted')
|
||||
} catch (err) {
|
||||
if ((err as any)?.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const removeAuth_removesTokenFromSubmodules =
|
||||
'removeAuth removes token from submodules'
|
||||
it(removeAuth_removesTokenFromSubmodules, async () => {
|
||||
// Arrange
|
||||
await setup(removeAuth_removesTokenFromSubmodules)
|
||||
|
||||
// Create fake submodule config paths
|
||||
const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
|
||||
const submodule2Dir = path.join(workspace, '.git', 'modules', 'submodule-2')
|
||||
const submodule1ConfigPath = path.join(submodule1Dir, 'config')
|
||||
const submodule2ConfigPath = path.join(submodule2Dir, 'config')
|
||||
|
||||
await fs.promises.mkdir(submodule1Dir, {recursive: true})
|
||||
await fs.promises.mkdir(submodule2Dir, {recursive: true})
|
||||
await fs.promises.writeFile(submodule1ConfigPath, '')
|
||||
await fs.promises.writeFile(submodule2ConfigPath, '')
|
||||
|
||||
// Mock getSubmoduleConfigPaths to return our fake submodules (for both configure and remove)
|
||||
const mockGetSubmoduleConfigPaths =
|
||||
git.getSubmoduleConfigPaths as jest.Mock<any, any>
|
||||
mockGetSubmoduleConfigPaths.mockResolvedValue([
|
||||
submodule1ConfigPath,
|
||||
submodule2ConfigPath
|
||||
])
|
||||
|
||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
await authHelper.configureAuth()
|
||||
await authHelper.configureSubmoduleAuth()
|
||||
|
||||
// Verify credentials file exists
|
||||
let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||
)
|
||||
expect(credentialsFiles.length).toBe(1)
|
||||
const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
|
||||
|
||||
// Verify submodule 1 config has includeIf entries
|
||||
let submodule1Content = (
|
||||
await fs.promises.readFile(submodule1ConfigPath)
|
||||
).toString()
|
||||
const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
|
||||
expect(
|
||||
submodule1Content.indexOf(`includeIf.gitdir:${submodule1GitDir}.path`)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
expect(
|
||||
submodule1Content.indexOf(credentialsFilePath)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Verify submodule 2 config has includeIf entries
|
||||
let submodule2Content = (
|
||||
await fs.promises.readFile(submodule2ConfigPath)
|
||||
).toString()
|
||||
const submodule2GitDir = submodule2Dir.replace(/\\/g, '/')
|
||||
expect(
|
||||
submodule2Content.indexOf(`includeIf.gitdir:${submodule2GitDir}.path`)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
expect(
|
||||
submodule2Content.indexOf(credentialsFilePath)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Verify both host and container paths are in each submodule config
|
||||
const containerCredentialsPath = path.posix.join(
|
||||
'/github/runner_temp',
|
||||
path.basename(credentialsFilePath)
|
||||
)
|
||||
expect(
|
||||
submodule1Content.indexOf(containerCredentialsPath)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
expect(
|
||||
submodule2Content.indexOf(containerCredentialsPath)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Act - ensure mock persists for removeAuth
|
||||
mockGetSubmoduleConfigPaths.mockResolvedValue([
|
||||
submodule1ConfigPath,
|
||||
submodule2ConfigPath
|
||||
])
|
||||
await authHelper.removeAuth()
|
||||
|
||||
// Assert submodule 1 includeIf entries removed
|
||||
submodule1Content = (
|
||||
await fs.promises.readFile(submodule1ConfigPath)
|
||||
).toString()
|
||||
expect(submodule1Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
|
||||
expect(submodule1Content.indexOf(credentialsFilePath)).toBeLessThan(0)
|
||||
expect(submodule1Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
|
||||
|
||||
// Assert submodule 2 includeIf entries removed
|
||||
submodule2Content = (
|
||||
await fs.promises.readFile(submodule2ConfigPath)
|
||||
).toString()
|
||||
expect(submodule2Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
|
||||
expect(submodule2Content.indexOf(credentialsFilePath)).toBeLessThan(0)
|
||||
expect(submodule2Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
|
||||
|
||||
// Assert credentials config file deleted
|
||||
credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||
)
|
||||
expect(credentialsFiles.length).toBe(0)
|
||||
|
||||
// Verify credentials file no longer exists on disk
|
||||
try {
|
||||
await fs.promises.stat(credentialsFilePath)
|
||||
throw new Error('Credentials file should have been deleted')
|
||||
} catch (err) {
|
||||
if ((err as any)?.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
|
||||
})
|
||||
|
||||
const removeGlobalConfig_removesOverride =
|
||||
@@ -928,52 +701,6 @@ 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> {
|
||||
@@ -988,7 +715,6 @@ 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')
|
||||
@@ -1001,26 +727,15 @@ async function setup(testName: string): Promise<void> {
|
||||
branchDelete: jest.fn(),
|
||||
branchExists: jest.fn(),
|
||||
branchList: jest.fn(),
|
||||
disableSparseCheckout: jest.fn(),
|
||||
sparseCheckout: jest.fn(),
|
||||
sparseCheckoutNonConeMode: jest.fn(),
|
||||
checkout: jest.fn(),
|
||||
checkoutDetach: jest.fn(),
|
||||
config: jest.fn(
|
||||
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})
|
||||
async (key: string, value: string, globalConfig?: boolean) => {
|
||||
const configPath = globalConfig
|
||||
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||
: localGitConfigPath
|
||||
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
|
||||
}
|
||||
),
|
||||
@@ -1040,17 +755,14 @@ 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(),
|
||||
lfsFetch: jest.fn(),
|
||||
lfsInstall: jest.fn(),
|
||||
log1: jest.fn(),
|
||||
referenceAdd: jest.fn(),
|
||||
remoteAdd: jest.fn(),
|
||||
removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
|
||||
execGit: jest.fn(),
|
||||
revParse: jest.fn(),
|
||||
setEnvironmentVariable: jest.fn((name: string, value: string) => {
|
||||
git.env[name] = value
|
||||
@@ -1081,74 +793,9 @@ 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()
|
||||
tryReset: jest.fn()
|
||||
}
|
||||
|
||||
settings = {
|
||||
@@ -1159,7 +806,6 @@ async function setup(testName: string): Promise<void> {
|
||||
sparseCheckout: [],
|
||||
sparseCheckoutConeMode: true,
|
||||
fetchDepth: 1,
|
||||
fetchDepthExplicit: false,
|
||||
fetchTags: false,
|
||||
showProgress: true,
|
||||
lfs: false,
|
||||
@@ -1173,17 +819,14 @@ async function setup(testName: string): Promise<void> {
|
||||
sshKey: sshPath ? 'some ssh private key' : '',
|
||||
sshKnownHosts: '',
|
||||
sshStrict: true,
|
||||
sshUser: '',
|
||||
workflowOrganizationId: 123456,
|
||||
setSafeDirectory: true,
|
||||
githubServerUrl: githubServerUrl,
|
||||
referenceCache: ''
|
||||
githubServerUrl: githubServerUrl
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -1197,7 +840,6 @@ 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,109 +0,0 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import * as io from '@actions/io'
|
||||
import { GitCacheHelper } from '../src/git-cache-helper'
|
||||
import { IGitCommandManager } from '../src/git-command-manager'
|
||||
|
||||
describe('GitCacheHelper', () => {
|
||||
let cacheHelper: GitCacheHelper
|
||||
let mockGit: jest.Mocked<IGitCommandManager>
|
||||
|
||||
const cacheDir = path.join(__dirname, 'test-cache')
|
||||
|
||||
beforeEach(async () => {
|
||||
cacheHelper = new GitCacheHelper(cacheDir)
|
||||
mockGit = {
|
||||
execGit: jest.fn().mockImplementation(async (args) => {
|
||||
// If git clone is called, simulate creating the destination dir
|
||||
if (args && args.includes('clone')) {
|
||||
const dest = args.find((a: string) => a.includes('.tmp.'));
|
||||
if (dest) {
|
||||
await io.mkdirP(dest);
|
||||
} else {
|
||||
console.log('No .tmp. found in args:', args);
|
||||
}
|
||||
}
|
||||
return { exitCode: 0, stdout: '', stderr: '' };
|
||||
}),
|
||||
gitEnv: {}
|
||||
} as any
|
||||
|
||||
await io.mkdirP(cacheDir)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await io.rmRF(cacheDir)
|
||||
})
|
||||
|
||||
it('generates a consistent, short, and safe cache directory name', () => {
|
||||
const url1 = 'https://github.com/mwyraz/forgejo-actions-checkout.git'
|
||||
const name1 = (cacheHelper as any).generateCacheDirName(url1)
|
||||
|
||||
// Check structure: safe string + hash
|
||||
expect(name1).toMatch(/^https___github_com_mwyraz_forgejo_actions_checkout_git_[0-9a-f]{8}\.git$/)
|
||||
|
||||
// Same URL should produce the same directory name
|
||||
const url1_duplicate = 'https://github.com/mwyraz/forgejo-actions-checkout.git'
|
||||
expect((cacheHelper as any).generateCacheDirName(url1_duplicate)).toBe(name1)
|
||||
|
||||
// Different URL should produce a different directory name
|
||||
const url2 = 'https://github.com/mwyraz/forgejo-actions-checkout-other.git'
|
||||
expect((cacheHelper as any).generateCacheDirName(url2)).not.toBe(name1)
|
||||
|
||||
// SSH URL
|
||||
const url3 = 'git@github.com:auth/repo.git'
|
||||
const name3 = (cacheHelper as any).generateCacheDirName(url3)
|
||||
expect(name3).toMatch(/^git_github_com_auth_repo_git_[0-9a-f]{8}\.git$/)
|
||||
|
||||
// Unclean URLs
|
||||
const url4 = 'https://github.com/foo/bar.git?v=1'
|
||||
const name4 = (cacheHelper as any).generateCacheDirName(url4)
|
||||
expect(name4).toMatch(/^https___github_com_foo_bar_git_v_1_[0-9a-f]{8}\.git$/)
|
||||
})
|
||||
|
||||
it('sets up a cache directory if it does not exist', async () => {
|
||||
const repositoryUrl = 'https://github.com/mwyraz/test-repo.git'
|
||||
const resultPath = await cacheHelper.setupCache(mockGit, repositoryUrl)
|
||||
|
||||
const expectedName = (cacheHelper as any).generateCacheDirName(repositoryUrl)
|
||||
expect(resultPath).toBe(path.join(cacheDir, expectedName))
|
||||
|
||||
// It should have executed git clone --bare
|
||||
expect(mockGit.execGit).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
'-C',
|
||||
cacheDir,
|
||||
'clone',
|
||||
'--bare',
|
||||
repositoryUrl,
|
||||
expect.stringContaining(`${expectedName}.tmp`) // should use tmp dir
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('fetches updates if the cache directory already exists', async () => {
|
||||
const repositoryUrl = 'https://github.com/mwyraz/existing-repo.git'
|
||||
const expectedName = (cacheHelper as any).generateCacheDirName(repositoryUrl)
|
||||
const fixedPath = path.join(cacheDir, expectedName)
|
||||
|
||||
// Fake existing directory
|
||||
await io.mkdirP(path.join(fixedPath, 'objects'))
|
||||
|
||||
const resultPath = await cacheHelper.setupCache(mockGit, repositoryUrl)
|
||||
expect(resultPath).toBe(fixedPath)
|
||||
|
||||
// It should have executed git fetch
|
||||
expect(mockGit.execGit).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
'-C',
|
||||
fixedPath,
|
||||
'fetch',
|
||||
'--force',
|
||||
'--prune',
|
||||
'--tags',
|
||||
'origin',
|
||||
'+refs/heads/*:refs/heads/*'
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -108,7 +108,7 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when fetchDepth is 0', async () => {
|
||||
it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is true', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
@@ -122,7 +122,45 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
const refSpec = ['refspec1', 'refspec2']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
fetchDepth: 0
|
||||
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
|
||||
}
|
||||
|
||||
await git.fetch(refSpec, options)
|
||||
@@ -145,45 +183,7 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when fetchDepth is 0 and refSpec includes tags', 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', '+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 () => {
|
||||
it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is false', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
|
||||
const workingDirectory = 'test'
|
||||
@@ -197,7 +197,8 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
const refSpec = ['refspec1', 'refspec2']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
fetchDepth: 1
|
||||
fetchDepth: 1,
|
||||
fetchTags: false
|
||||
}
|
||||
|
||||
await git.fetch(refSpec, options)
|
||||
@@ -221,7 +222,7 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when fetchDepth is 1 and refSpec includes tags', async () => {
|
||||
it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is true', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
|
||||
const workingDirectory = 'test'
|
||||
@@ -232,10 +233,11 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
|
||||
const refSpec = ['refspec1', 'refspec2']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
fetchDepth: 1
|
||||
fetchDepth: 1,
|
||||
fetchTags: true
|
||||
}
|
||||
|
||||
await git.fetch(refSpec, options)
|
||||
@@ -246,15 +248,13 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
'-c',
|
||||
'protocol.version=2',
|
||||
'fetch',
|
||||
'--no-tags',
|
||||
'--prune',
|
||||
'--no-recurse-submodules',
|
||||
'--filter=filterValue',
|
||||
'--depth=1',
|
||||
'origin',
|
||||
'refspec1',
|
||||
'refspec2',
|
||||
'+refs/tags/*:refs/tags/*'
|
||||
'refspec2'
|
||||
],
|
||||
expect.any(Object)
|
||||
)
|
||||
@@ -338,7 +338,7 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when showProgress is true and refSpec includes tags', async () => {
|
||||
it('should call execGit with the correct arguments when fetchTags is true and showProgress is true', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
|
||||
const workingDirectory = 'test'
|
||||
@@ -349,9 +349,10 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
|
||||
const refSpec = ['refspec1', 'refspec2']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
fetchTags: true,
|
||||
showProgress: true
|
||||
}
|
||||
|
||||
@@ -363,134 +364,15 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
'-c',
|
||||
'protocol.version=2',
|
||||
'fetch',
|
||||
'--no-tags',
|
||||
'--prune',
|
||||
'--no-recurse-submodules',
|
||||
'--progress',
|
||||
'--filter=filterValue',
|
||||
'origin',
|
||||
'refspec1',
|
||||
'refspec2',
|
||||
'+refs/tags/*:refs/tags/*'
|
||||
'refspec2'
|
||||
],
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('git user-agent with orchestration ID', () => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
||||
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
// 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, args, options) => {
|
||||
if (args.includes('version')) {
|
||||
options.listeners.stdout(Buffer.from('2.18'))
|
||||
}
|
||||
// Capture env on any command
|
||||
capturedEnv = options.env
|
||||
return 0
|
||||
})
|
||||
jest.spyOn(exec, 'exec').mockImplementation(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, args, options) => {
|
||||
if (args.includes('version')) {
|
||||
options.listeners.stdout(Buffer.from('2.18'))
|
||||
}
|
||||
// Capture env on any command
|
||||
capturedEnv = options.env
|
||||
return 0
|
||||
})
|
||||
jest.spyOn(exec, 'exec').mockImplementation(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, args, options) => {
|
||||
if (args.includes('version')) {
|
||||
options.listeners.stdout(Buffer.from('2.18'))
|
||||
}
|
||||
// Capture env on any command
|
||||
capturedEnv = options.env
|
||||
return 0
|
||||
})
|
||||
jest.spyOn(exec, 'exec').mockImplementation(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)'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -462,7 +462,6 @@ async function setup(testName: string): Promise<void> {
|
||||
branchList: jest.fn(async () => {
|
||||
return []
|
||||
}),
|
||||
disableSparseCheckout: jest.fn(),
|
||||
sparseCheckout: jest.fn(),
|
||||
sparseCheckoutNonConeMode: jest.fn(),
|
||||
checkout: jest.fn(),
|
||||
@@ -471,17 +470,14 @@ 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(),
|
||||
lfsFetch: jest.fn(),
|
||||
lfsInstall: jest.fn(),
|
||||
log1: jest.fn(),
|
||||
referenceAdd: jest.fn(),
|
||||
remoteAdd: jest.fn(),
|
||||
removeEnvironmentVariable: jest.fn(),
|
||||
execGit: jest.fn(),
|
||||
revParse: jest.fn(),
|
||||
setEnvironmentVariable: jest.fn(),
|
||||
shaExists: jest.fn(),
|
||||
@@ -496,18 +492,14 @@ 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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import * as core from '@actions/core'
|
||||
import {adjustFetchDepthForCache} from '../src/git-source-provider'
|
||||
|
||||
// Mock @actions/core
|
||||
jest.mock('@actions/core')
|
||||
|
||||
describe('adjustFetchDepthForCache', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('does nothing when referenceCache is not set', () => {
|
||||
const settings = {
|
||||
referenceCache: '',
|
||||
fetchDepth: 1,
|
||||
fetchDepthExplicit: false
|
||||
}
|
||||
adjustFetchDepthForCache(settings)
|
||||
expect(settings.fetchDepth).toBe(1)
|
||||
expect(core.warning).not.toHaveBeenCalled()
|
||||
expect(core.info).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('overrides fetchDepth to 0 when referenceCache is set and fetchDepth is default', () => {
|
||||
const settings = {
|
||||
referenceCache: '/cache/git-reference-cache',
|
||||
fetchDepth: 1,
|
||||
fetchDepthExplicit: false
|
||||
}
|
||||
adjustFetchDepthForCache(settings)
|
||||
expect(settings.fetchDepth).toBe(0)
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Overriding fetch-depth from 1 to 0')
|
||||
)
|
||||
expect(core.warning).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('warns but keeps fetchDepth when referenceCache is set and fetchDepth is explicit', () => {
|
||||
const settings = {
|
||||
referenceCache: '/cache/git-reference-cache',
|
||||
fetchDepth: 1,
|
||||
fetchDepthExplicit: true
|
||||
}
|
||||
adjustFetchDepthForCache(settings)
|
||||
expect(settings.fetchDepth).toBe(1)
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
expect.stringContaining("'fetch-depth: 1' is set with reference-cache enabled")
|
||||
)
|
||||
expect(core.info).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does nothing when referenceCache is set and fetchDepth is already 0 (explicit)', () => {
|
||||
const settings = {
|
||||
referenceCache: '/cache/git-reference-cache',
|
||||
fetchDepth: 0,
|
||||
fetchDepthExplicit: true
|
||||
}
|
||||
adjustFetchDepthForCache(settings)
|
||||
expect(settings.fetchDepth).toBe(0)
|
||||
expect(core.warning).not.toHaveBeenCalled()
|
||||
expect(core.info).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does nothing when referenceCache is set and fetchDepth is already 0 (default)', () => {
|
||||
const settings = {
|
||||
referenceCache: '/cache/git-reference-cache',
|
||||
fetchDepth: 0,
|
||||
fetchDepthExplicit: false
|
||||
}
|
||||
adjustFetchDepthForCache(settings)
|
||||
expect(settings.fetchDepth).toBe(0)
|
||||
expect(core.warning).not.toHaveBeenCalled()
|
||||
expect(core.info).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('warns with correct depth value when explicit fetchDepth is > 1', () => {
|
||||
const settings = {
|
||||
referenceCache: '/cache/git-reference-cache',
|
||||
fetchDepth: 42,
|
||||
fetchDepthExplicit: true
|
||||
}
|
||||
adjustFetchDepthForCache(settings)
|
||||
expect(settings.fetchDepth).toBe(42)
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
expect.stringContaining("'fetch-depth: 42' is set with reference-cache enabled")
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,4 @@
|
||||
import {GitVersion} from '../src/git-version'
|
||||
import {MinimumGitSparseCheckoutVersion} from '../src/git-command-manager'
|
||||
import {GitVersion} from '../lib/git-version'
|
||||
|
||||
describe('git-version tests', () => {
|
||||
it('basics', async () => {
|
||||
@@ -43,44 +42,4 @@ describe('git-version tests', () => {
|
||||
expect(version.checkMinimum(new GitVersion('5.1'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('5.1.2'))).toBeFalsy()
|
||||
})
|
||||
|
||||
it('sparse checkout', async () => {
|
||||
const minSparseVer = MinimumGitSparseCheckoutVersion
|
||||
expect(new GitVersion('1.0').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('1.99').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.0').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.24').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.24.0').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.24.9').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.25').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.25.0').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.25.1').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.25.9').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.26').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.26.0').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.26.1').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.26.9').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.27').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.27.0').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.27.1').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
expect(new GitVersion('2.27.9').checkMinimum(minSparseVer)).toBeFalsy()
|
||||
// /---------------------------------------
|
||||
// ^^^ before / after vvv
|
||||
// --------------------------/
|
||||
expect(new GitVersion('2.28').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('2.28.0').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('2.28.1').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('2.28.9').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('2.29').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('2.29.0').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('2.29.1').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('2.29.9').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('2.99').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('3.0').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('3.99').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('4.0').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('4.99').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('5.0').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
expect(new GitVersion('5.99').checkMinimum(minSparseVer)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -91,7 +91,6 @@ describe('input-helper tests', () => {
|
||||
expect(settings.repositoryOwner).toBe('some-owner')
|
||||
expect(settings.repositoryPath).toBe(gitHubWorkspace)
|
||||
expect(settings.setSafeDirectory).toBe(true)
|
||||
expect(settings.referenceCache || '').toBe('')
|
||||
})
|
||||
|
||||
it('qualifies ref', async () => {
|
||||
|
||||
@@ -67,26 +67,6 @@ describe('ref-helper tests', () => {
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
'refs/gh/queue/main/pr-123',
|
||||
commit
|
||||
)
|
||||
expect(checkoutInfo.ref).toBe(commit)
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/ without commit', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
'refs/non-standard-ref',
|
||||
''
|
||||
)
|
||||
expect(checkoutInfo.ref).toBe('refs/non-standard-ref')
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo unqualified branch only', async () => {
|
||||
git.branchExists = jest.fn(async (remote: boolean, pattern: string) => {
|
||||
return true
|
||||
@@ -152,22 +132,7 @@ 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(`+refs/tags/my-tag:refs/tags/my-tag`)
|
||||
})
|
||||
|
||||
it('getRefSpec sha + refs/tags/ with fetchTags', async () => {
|
||||
// When fetchTags is true, only include tags wildcard (specific tag is redundant)
|
||||
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 () => {
|
||||
// When fetchTags is true, include both the branch refspec and tags wildcard
|
||||
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`)
|
||||
expect(refSpec[0]).toBe(`+${commit}:refs/tags/my-tag`)
|
||||
})
|
||||
|
||||
it('getRefSpec sha only', async () => {
|
||||
@@ -183,14 +148,6 @@ describe('ref-helper tests', () => {
|
||||
expect(refSpec[1]).toBe('+refs/tags/my-ref*:refs/tags/my-ref*')
|
||||
})
|
||||
|
||||
it('getRefSpec unqualified ref only with fetchTags', async () => {
|
||||
// When fetchTags is true, skip specific tag pattern since wildcard covers all
|
||||
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)
|
||||
@@ -210,21 +167,4 @@ 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 () => {
|
||||
// When fetchTags is true, only include tags wildcard (specific tag is redundant)
|
||||
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 () => {
|
||||
// When fetchTags is true, include both the branch refspec and tags wildcard
|
||||
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'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import * as urlHelper from '../src/url-helper'
|
||||
|
||||
describe('getServerUrl tests', () => {
|
||||
it('basics', async () => {
|
||||
// Note that URL::toString will append a trailing / when passed just a domain name ...
|
||||
expect(urlHelper.getServerUrl().toString()).toBe('https://github.com/')
|
||||
expect(urlHelper.getServerUrl(' ').toString()).toBe('https://github.com/')
|
||||
expect(urlHelper.getServerUrl(' ').toString()).toBe('https://github.com/')
|
||||
expect(urlHelper.getServerUrl('http://contoso.com').toString()).toBe(
|
||||
'http://contoso.com/'
|
||||
)
|
||||
expect(urlHelper.getServerUrl('https://contoso.com').toString()).toBe(
|
||||
'https://contoso.com/'
|
||||
)
|
||||
expect(urlHelper.getServerUrl('https://contoso.com/').toString()).toBe(
|
||||
'https://contoso.com/'
|
||||
)
|
||||
|
||||
// ... but can't make that same assumption when passed an URL that includes some deeper path.
|
||||
expect(urlHelper.getServerUrl('https://contoso.com/a/b').toString()).toBe(
|
||||
'https://contoso.com/a/b'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isGhes tests', () => {
|
||||
const pristineEnv = process.env
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
process.env = {...pristineEnv}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
process.env = pristineEnv
|
||||
})
|
||||
|
||||
it('basics', async () => {
|
||||
delete process.env['GITHUB_SERVER_URL']
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://github.com')).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://contoso.ghe.com')).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://test.github.localhost')).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://src.onpremise.fabrikam.com')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable is not defined', async () => {
|
||||
delete process.env['GITHUB_SERVER_URL']
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable is set to github.com', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://github.com'
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://contoso.ghe.com'
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://mock-github.localhost'
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns true when the GITHUB_SERVER_URL environment variable is set to some other URL', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://src.onpremise.fabrikam.com'
|
||||
expect(urlHelper.isGhes()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getServerApiUrl tests', () => {
|
||||
it('basics', async () => {
|
||||
expect(urlHelper.getServerApiUrl()).toBe('https://api.github.com')
|
||||
expect(urlHelper.getServerApiUrl('https://github.com')).toBe(
|
||||
'https://api.github.com'
|
||||
)
|
||||
expect(urlHelper.getServerApiUrl('https://GitHub.com')).toBe(
|
||||
'https://api.github.com'
|
||||
)
|
||||
expect(urlHelper.getServerApiUrl('https://contoso.ghe.com')).toBe(
|
||||
'https://api.contoso.ghe.com'
|
||||
)
|
||||
expect(urlHelper.getServerApiUrl('https://fabrikam.GHE.COM')).toBe(
|
||||
'https://api.fabrikam.ghe.com'
|
||||
)
|
||||
expect(
|
||||
urlHelper.getServerApiUrl('https://src.onpremise.fabrikam.com')
|
||||
).toBe('https://src.onpremise.fabrikam.com/api/v3')
|
||||
})
|
||||
})
|
||||
@@ -18,20 +18,6 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify that sparse-checkout is disabled.
|
||||
SPARSE_CHECKOUT_ENABLED=$(git -C ./basic config --local --get-all core.sparseCheckout)
|
||||
if [ "$SPARSE_CHECKOUT_ENABLED" != "" ]; then
|
||||
echo "Expected sparse-checkout to be disabled (discovered: $SPARSE_CHECKOUT_ENABLED)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify git configuration shows worktreeConfig is effectively disabled
|
||||
WORKTREE_CONFIG_ENABLED=$(git -C ./basic config --local --get-all extensions.worktreeConfig)
|
||||
if [[ "$WORKTREE_CONFIG_ENABLED" != "" ]]; then
|
||||
echo "Expected extensions.worktreeConfig (boolean) to be disabled in git config. This could be an artifact of sparse checkout functionality."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify auth token
|
||||
cd basic
|
||||
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/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 --includes --name-only --get-regexp http.+extraheader && git fetch
|
||||
git config --local --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 --includes --name-only --get-regexp http.+extraheader && git fetch
|
||||
git config --local --name-only --get-regexp http.+extraheader && git fetch
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "Failed to validate persisted credential"
|
||||
popd
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/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!"
|
||||
17
action.yml
17
action.yml
@@ -45,10 +45,6 @@ inputs:
|
||||
and `CheckHostIP=no` to the SSH command line. Use the input `ssh-known-hosts` to
|
||||
configure additional hosts.
|
||||
default: true
|
||||
ssh-user:
|
||||
description: >
|
||||
The user to use when connecting to the remote SSH host. By default 'git' is used.
|
||||
default: git
|
||||
persist-credentials:
|
||||
description: 'Whether to configure the token or SSH key with the local git config'
|
||||
default: true
|
||||
@@ -98,18 +94,7 @@ 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
|
||||
reference-cache:
|
||||
description: >
|
||||
Path to a local directory used as a reference cache for Git clones. Over time,
|
||||
this directory will contain bare clones of the checked-out repositories (and their submodules).
|
||||
Using this significantly reduces network bandwidth and speeds up clones.
|
||||
required: false
|
||||
outputs:
|
||||
ref:
|
||||
description: 'The branch, tag or SHA that was checked out'
|
||||
commit:
|
||||
description: 'The commit SHA that was checked out'
|
||||
runs:
|
||||
using: node24
|
||||
using: node20
|
||||
main: dist/index.js
|
||||
post: dist/index.js
|
||||
|
||||
BIN
actions-github-6.0.2.tgz
Normal file
BIN
actions-github-6.0.2.tgz
Normal file
Binary file not shown.
BIN
actions-http-client-3.0.3.tgz
Normal file
BIN
actions-http-client-3.0.3.tgz
Normal file
Binary file not shown.
@@ -1,37 +0,0 @@
|
||||
# Reference Cache für schnelle Checkouts
|
||||
|
||||
## Zusammenfassung
|
||||
Einführung eines lokal verwalteten Git-Referenz-Caches für Haupt-Repositories und Submodule, um Netzwerk-Traffic und Checkout-Zeiten auf persistenten Runnern (z.B. Self-Hosted) massiv zu reduzieren.
|
||||
|
||||
## Implementierungsplan
|
||||
|
||||
1. **Inputs:**
|
||||
- In `action.yml` einen neuen Input `reference-cache` (Pfad zum Cache-Verzeichnis) hinzufügen. Default ist leer.
|
||||
- In `src/git-source-settings.ts` und `src/input-helper.ts` den Input auslesen und bereitstellen (`settings.referenceCache`).
|
||||
|
||||
2. **Cache Manager (`src/git-cache-helper.ts`):**
|
||||
- Eine neue Klasse/Helper-Logik, die das Erstellen (`git clone --bare`) und Aktualisieren (`git fetch --force`) von Bare Cache-Repos übernimmt.
|
||||
- **Namenskonvention Cache-Verzeichnis:** Damit Admin-Lesbarkeit und Kollisionsfreiheit gewährleistet sind, wird das Cache-Verzeichnis aus der Repository-URL gebildet:
|
||||
- Alle Sonderzeichen in der URL durch `_` ersetzen.
|
||||
- Ein kurzer Hash (z. B. erste 8 Zeichen des SHA256) der echten URL zur Eindeutigkeit anhängen.
|
||||
- Beispiel: `<reference-cache>/https___github_com_actions_checkout_8f9b1c2a.git`
|
||||
|
||||
3. **Haupt-Repo Checkout (`src/git-source-provider.ts`):**
|
||||
- Vor dem Setup des Checkouts prüfen, ob `reference-cache` gesetzt ist.
|
||||
- Wenn ja: den Cache-Ordner für die Haupt-URL aktualisieren/anlegen.
|
||||
- Nach dem initialen `git.init()` den Pfad in `.git/objects/info/alternates` schreiben, der auf das `objects`-Verzeichnis des Cache-Ordners zeigt.
|
||||
|
||||
4. **Submodule Checkouts (Iterativ statt monolithisch):**
|
||||
- Der aktuelle Befehl `git submodule update --recursive` funktioniert nicht out-of-the-box mit `reference`, wenn jedes Submodul seinen individuellen Referenz-Cache benötigt.
|
||||
- Wenn `reference-cache` aktiv ist und Submodule initialisiert werden sollen:
|
||||
- Lese `.gitmodules` aus (alle Sub-URLs ermitteln).
|
||||
- Für jedes Submodul den Cache (genauso wie in Step 2) anlegen oder aktualisieren.
|
||||
- Submodul einzeln auschecken per `git submodule update --init --reference <cache-pfad/.git> <pfad>`.
|
||||
- Bei der Einstellung `recursive`: In jedes Submodul-Verzeichnis wechseln und den Vorgang für `.gitmodules` rekursiv auf Skript-Ebene durchführen (anstatt Git's `--recursive` Flag einfach weiterzugeben).
|
||||
|
||||
## Akzeptanzkriterien
|
||||
1. **Neue Option konfigurierbar**: Der Input `reference-cache` kann übergeben werden, der Code reagiert darauf.
|
||||
2. **Ordnerstruktur korrekt**: Der Cache-Ordner für das Hauptrepo und Submodule erhält Namen nach der "URL_Sonderzeichen_Ersetzt+SHA_Cut"-Logik.
|
||||
3. **Bandbreite gespart / Alternates genutzt**: Beim Hauptcheckout wird eine `.git/objects/info/alternates`-Datei mit Pfad zum lokalen Cache erzeugt. Danach ausgeführte `git fetch`-Befehle sind signifikant schneller bzw. laden deutlich weniger Bytes herunter.
|
||||
4. **Submodule erhalten Caches**: Auch tiefe (rekursive) Submodule profitieren für deren jeweilige Remote-URL vom Cache, da pro Submodul ein passender `--reference` Punkt dynamisch berechnet und übergeben wird.
|
||||
5. **Kein --dissociate**: Aus Performance-Gründen bleibt der Arbeitsordner an den Cache gebunden (`git repack` ist zeitaufwändig). Fällt der Cache weg, muss der Workspace erst einmal neu erzeugt werden (was bei Action Runnern die Norm ist, falls es nicht ohnehin "single-use" Runner sind).
|
||||
21168
dist/index.js
vendored
21168
dist/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,12 +0,0 @@
|
||||
# Defines the test-ubuntu-git Container Image.
|
||||
# Consumed by actions/checkout CI/CD validation workflows.
|
||||
|
||||
FROM ubuntu:latest
|
||||
|
||||
RUN apt update
|
||||
RUN apt install -y git
|
||||
|
||||
LABEL org.opencontainers.image.title="Ubuntu + git (validation image)"
|
||||
LABEL org.opencontainers.image.description="Ubuntu image with git pre-installed. Intended primarily for testing `actions/checkout` during CI/CD workflows."
|
||||
LABEL org.opencontainers.image.documentation="https://github.com/actions/checkout/tree/main/images/test-ubuntu-git.md"
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
||||
@@ -1,15 +0,0 @@
|
||||
# `test-ubuntu-git` Container Image
|
||||
|
||||
[](https://github.com/actions/checkout/actions/workflows/update-test-ubuntu-git.yml)
|
||||
|
||||
## Purpose
|
||||
|
||||
`test-ubuntu-git` is a container image hosted on the GitHub Container Registry, `ghcr.io`.
|
||||
|
||||
It is intended primarily for testing the [`actions/checkout` repository](https://github.com/actions/checkout) as part of `actions/checkout`'s CI/CD workflows.
|
||||
|
||||
The composition of `test-ubuntu-git` is intentionally minimal. It is comprised of [git](https://git-scm.com/) installed on top of a [base-level ubuntu image](https://hub.docker.com/_/ubuntu/tags).
|
||||
|
||||
# License
|
||||
|
||||
`test-ubuntu-git` is released under the [MIT License](/LICENSE).
|
||||
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
fakeTimers: {},
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
|
||||
8970
package-lock.json
generated
8970
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "checkout",
|
||||
"version": "5.0.0",
|
||||
"version": "4.1.0",
|
||||
"description": "checkout action",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
@@ -30,28 +30,26 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
"@actions/github": "file:actions-github-6.0.2.tgz",
|
||||
"@actions/io": "^1.1.3",
|
||||
"@actions/tool-cache": "^2.0.1",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^24.1.0",
|
||||
"@types/proper-lockfile": "^4.1.4",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
||||
"@typescript-eslint/parser": "^7.9.0",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-github": "^4.10.2",
|
||||
"eslint-plugin-jest": "^28.8.2",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/node": "^20.8.2",
|
||||
"@types/uuid": "^3.4.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@vercel/ncc": "^0.38.0",
|
||||
"eslint": "^8.50.0",
|
||||
"eslint-plugin-github": "^4.10.1",
|
||||
"eslint-plugin-jest": "^27.4.2",
|
||||
"jest": "^29.7.0",
|
||||
"jest-circus": "^29.7.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "^3.3.3",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.5.4"
|
||||
"js-yaml": "^3.13.1",
|
||||
"prettier": "^3.0.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 {default as uuid} from 'uuid/v4'
|
||||
import {IGitCommandManager} from './git-command-manager'
|
||||
import {IGitSourceSettings} from './git-source-settings'
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface IGitAuthHelper {
|
||||
configureSubmoduleAuth(): Promise<void>
|
||||
configureTempGlobalConfig(): Promise<string>
|
||||
removeAuth(): Promise<void>
|
||||
removeGlobalAuth(): Promise<void>
|
||||
removeGlobalConfig(): Promise<void>
|
||||
}
|
||||
|
||||
@@ -44,7 +43,6 @@ class GitAuthHelper {
|
||||
private sshKeyPath = ''
|
||||
private sshKnownHostsPath = ''
|
||||
private temporaryHomePath = ''
|
||||
private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP
|
||||
|
||||
constructor(
|
||||
gitCommandManager: IGitCommandManager,
|
||||
@@ -128,21 +126,16 @@ class GitAuthHelper {
|
||||
|
||||
async configureGlobalAuth(): Promise<void> {
|
||||
// 'configureTempGlobalConfig' noops if already set, just returns the path
|
||||
await this.configureTempGlobalConfig()
|
||||
const newGitConfigPath = await this.configureTempGlobalConfig()
|
||||
try {
|
||||
// Configure the token
|
||||
await this.configureToken(true)
|
||||
await this.configureToken(newGitConfigPath, 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, // globalConfig?
|
||||
true // add?
|
||||
)
|
||||
await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -157,60 +150,24 @@ class GitAuthHelper {
|
||||
|
||||
async configureSubmoduleAuth(): Promise<void> {
|
||||
// Remove possible previous HTTPS instead of SSH
|
||||
await this.removeSubmoduleGitConfig(this.insteadOfKey)
|
||||
await this.removeGitConfig(this.insteadOfKey, true)
|
||||
|
||||
if (this.settings.persistCredentials) {
|
||||
// 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(
|
||||
// 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"`,
|
||||
this.settings.nestedSubmodules
|
||||
)
|
||||
|
||||
// For each submodule, configure includeIf entries pointing to the shared credentials file.
|
||||
// Configure both host and container paths to support Docker container actions.
|
||||
// Replace the placeholder
|
||||
const configPaths: string[] =
|
||||
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
|
||||
for (const configPath of configPaths) {
|
||||
// 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
|
||||
)
|
||||
core.debug(`Replacing token placeholder in '${configPath}'`)
|
||||
await this.replaceTokenPlaceholder(configPath)
|
||||
}
|
||||
|
||||
if (this.settings.sshKey) {
|
||||
@@ -236,12 +193,6 @@ class GitAuthHelper {
|
||||
await this.removeToken()
|
||||
}
|
||||
|
||||
async removeGlobalAuth(): Promise<void> {
|
||||
core.debug('Removing global auth entries')
|
||||
await this.git.tryConfigUnset('include.path', true)
|
||||
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
||||
}
|
||||
|
||||
async removeGlobalConfig(): Promise<void> {
|
||||
if (this.temporaryHomePath?.length > 0) {
|
||||
core.debug(`Unsetting HOME override`)
|
||||
@@ -250,10 +201,6 @@ 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
|
||||
@@ -325,127 +272,57 @@ class GitAuthHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
private async configureToken(
|
||||
configPath?: string,
|
||||
globalConfig?: boolean
|
||||
): Promise<void> {
|
||||
// Validate args
|
||||
assert.ok(
|
||||
(configPath && globalConfig) || (!configPath && !globalConfig),
|
||||
'Unexpected configureToken parameter combinations'
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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
|
||||
await this.git.config(
|
||||
this.tokenConfigKey,
|
||||
this.tokenPlaceholderConfigValue,
|
||||
false, // globalConfig?
|
||||
false, // add?
|
||||
credentialsConfigPath
|
||||
globalConfig
|
||||
)
|
||||
|
||||
// Replace the placeholder in the credentials config file
|
||||
let content = (await fs.promises.readFile(credentialsConfigPath)).toString()
|
||||
// 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()
|
||||
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
|
||||
if (
|
||||
placeholderIndex < 0 ||
|
||||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
|
||||
) {
|
||||
throw new Error(
|
||||
`Unable to replace auth placeholder in ${credentialsConfigPath}`
|
||||
)
|
||||
throw new Error(`Unable to replace auth placeholder in ${configPath}`)
|
||||
}
|
||||
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
|
||||
content = content.replace(
|
||||
this.tokenPlaceholderConfigValue,
|
||||
this.tokenConfigValue
|
||||
)
|
||||
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
|
||||
)
|
||||
}
|
||||
await fs.promises.writeFile(configPath, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates the path to the credentials config file in RUNNER_TEMP.
|
||||
* @returns The absolute path to the credentials config file
|
||||
*/
|
||||
private getCredentialsConfigPath(): string {
|
||||
if (this.credentialsConfigPath) {
|
||||
return this.credentialsConfigPath
|
||||
}
|
||||
|
||||
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
||||
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||
|
||||
// Create a unique filename for this checkout instance
|
||||
const configFileName = `git-credentials-${uuid()}.config`
|
||||
this.credentialsConfigPath = path.join(runnerTemp, configFileName)
|
||||
|
||||
core.debug(`Credentials config path: ${this.credentialsConfigPath}`)
|
||||
return this.credentialsConfigPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes SSH authentication configuration by cleaning up SSH keys,
|
||||
* known hosts files, and SSH command configurations.
|
||||
*/
|
||||
private async removeSsh(): Promise<void> {
|
||||
// SSH key
|
||||
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
|
||||
if (keyPath) {
|
||||
try {
|
||||
core.info(`Removing SSH key '${keyPath}'`)
|
||||
await io.rmRF(keyPath)
|
||||
} catch (err) {
|
||||
core.debug(`${(err as any)?.message ?? err}`)
|
||||
@@ -458,149 +335,40 @@ class GitAuthHelper {
|
||||
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
|
||||
if (knownHostsPath) {
|
||||
try {
|
||||
core.info(`Removing SSH known hosts '${knownHostsPath}'`)
|
||||
await io.rmRF(knownHostsPath)
|
||||
} catch (err) {
|
||||
core.debug(`${(err as any)?.message ?? err}`)
|
||||
core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`)
|
||||
} catch {
|
||||
// Intentionally empty
|
||||
}
|
||||
}
|
||||
|
||||
// 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> {
|
||||
// Remove HTTP extra header
|
||||
core.info('Removing HTTP extra header')
|
||||
// HTTP extra header
|
||||
await this.removeGitConfig(this.tokenConfigKey)
|
||||
await this.removeSubmoduleGitConfig(this.tokenConfigKey)
|
||||
}
|
||||
|
||||
// Collect credentials config paths that need to be removed
|
||||
const credentialsPaths = new Set<string>()
|
||||
|
||||
// Remove includeIf entries that point to git-credentials-*.config files
|
||||
core.info('Removing includeIf entries pointing to credentials config files')
|
||||
const mainCredentialsPaths = await this.removeIncludeIfCredentials()
|
||||
mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
|
||||
|
||||
// Remove submodule includeIf entries that point to git-credentials-*.config files
|
||||
const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true)
|
||||
for (const configPath of submoduleConfigPaths) {
|
||||
const submoduleCredentialsPaths =
|
||||
await this.removeIncludeIfCredentials(configPath)
|
||||
submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
|
||||
}
|
||||
|
||||
// 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`
|
||||
)
|
||||
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`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,98 +0,0 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import * as crypto from 'crypto'
|
||||
import * as lockfile from 'proper-lockfile'
|
||||
import {IGitCommandManager} from './git-command-manager'
|
||||
|
||||
export class GitCacheHelper {
|
||||
constructor(private referenceCache: string) {}
|
||||
|
||||
/**
|
||||
* Prepares the reference cache for a given repository URL.
|
||||
* If the cache does not exist, it performs a bare clone.
|
||||
* If it exists, it performs a fetch to update it.
|
||||
* Returns the absolute path to the bare cache repository.
|
||||
*/
|
||||
async setupCache(git: IGitCommandManager, repositoryUrl: string): Promise<string> {
|
||||
const cacheDirName = this.generateCacheDirName(repositoryUrl)
|
||||
const cachePath = path.join(this.referenceCache, cacheDirName)
|
||||
|
||||
// Ensure the base cache directory exists before we try to lock inside it
|
||||
if (!fs.existsSync(this.referenceCache)) {
|
||||
await fs.promises.mkdir(this.referenceCache, { recursive: true })
|
||||
}
|
||||
|
||||
// We use a dedicated lock dir specifically for this repository's cache
|
||||
// since we cannot place a lock *inside* a repository that might not exist yet
|
||||
const lockfilePath = `${cachePath}.lock`
|
||||
|
||||
// Ensure the file we are locking exists
|
||||
if (!fs.existsSync(lockfilePath)) {
|
||||
await fs.promises.writeFile(lockfilePath, '')
|
||||
}
|
||||
|
||||
core.debug(`Acquiring lock for ${repositoryUrl} at ${lockfilePath}`)
|
||||
|
||||
let releaseLock: () => Promise<void>
|
||||
try {
|
||||
// proper-lockfile creates a ".lock" directory next to the target file.
|
||||
// We configure it to wait up to 10 minutes (600,000 ms) for another process to finish.
|
||||
// E.g. cloning a very large monorepo might take minutes.
|
||||
releaseLock = await lockfile.lock(lockfilePath, {
|
||||
retries: {
|
||||
retries: 60, // try 60 times
|
||||
factor: 1, // linear backoff
|
||||
minTimeout: 10000, // wait 10 seconds between tries
|
||||
maxTimeout: 10000, // (total max wait time: 600s = 10m)
|
||||
randomize: true
|
||||
}
|
||||
})
|
||||
core.debug(`Lock acquired.`)
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to acquire lock for repository cache ${repositoryUrl}: ${err}`)
|
||||
}
|
||||
|
||||
try {
|
||||
if (fs.existsSync(path.join(cachePath, 'objects'))) {
|
||||
core.info(`Reference cache for ${repositoryUrl} exists. Updating...`)
|
||||
const args = ['-C', cachePath, 'fetch', '--force', '--prune', '--tags', 'origin', '+refs/heads/*:refs/heads/*']
|
||||
await git.execGit(args)
|
||||
} else {
|
||||
core.info(`Reference cache for ${repositoryUrl} does not exist. Cloning --bare...`)
|
||||
|
||||
// Use a temporary clone pattern to prevent corrupted repos if process is killed mid-clone
|
||||
const tmpPath = `${cachePath}.tmp.${crypto.randomUUID()}`
|
||||
try {
|
||||
const args = ['-C', this.referenceCache, 'clone', '--bare', repositoryUrl, tmpPath]
|
||||
await git.execGit(args)
|
||||
|
||||
if (fs.existsSync(cachePath)) {
|
||||
// In rare cases where it somehow exists but objects/ didn't, clean it up
|
||||
await fs.promises.rm(cachePath, { recursive: true, force: true })
|
||||
}
|
||||
await fs.promises.rename(tmpPath, cachePath)
|
||||
} catch (cloneErr) {
|
||||
// Cleanup partial clone if an error occurred
|
||||
await fs.promises.rm(tmpPath, { recursive: true, force: true }).catch(() => {})
|
||||
throw cloneErr
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await releaseLock()
|
||||
}
|
||||
|
||||
return cachePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a directory name for the cache based on the URL.
|
||||
* Replaces non-alphanumeric characters with underscores
|
||||
* and appends a short SHA256 hash of the original URL.
|
||||
*/
|
||||
generateCacheDirName(url: string): string {
|
||||
const cleanUrl = url.replace(/[^a-zA-Z0-9]/g, '_')
|
||||
const hash = crypto.createHash('sha256').update(url).digest('hex').substring(0, 8)
|
||||
return `${cleanUrl}_${hash}.git`
|
||||
}
|
||||
}
|
||||
@@ -11,20 +11,12 @@ import {GitVersion} from './git-version'
|
||||
|
||||
// Auth header not supported before 2.9
|
||||
// Wire protocol v2 not supported before 2.18
|
||||
// sparse-checkout not [well-]supported before 2.28 (see https://github.com/actions/checkout/issues/1386)
|
||||
export const MinimumGitVersion = new GitVersion('2.18')
|
||||
export const MinimumGitSparseCheckoutVersion = new GitVersion('2.28')
|
||||
|
||||
export class GitOutput {
|
||||
stdout = ''
|
||||
exitCode = 0
|
||||
}
|
||||
|
||||
export interface IGitCommandManager {
|
||||
branchDelete(remote: boolean, branch: string): Promise<void>
|
||||
branchExists(remote: boolean, pattern: string): Promise<boolean>
|
||||
branchList(remote: boolean): Promise<string[]>
|
||||
disableSparseCheckout(): Promise<void>
|
||||
sparseCheckout(sparseCheckout: string[]): Promise<void>
|
||||
sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void>
|
||||
checkout(ref: string, startPoint: string): Promise<void>
|
||||
@@ -33,8 +25,7 @@ export interface IGitCommandManager {
|
||||
configKey: string,
|
||||
configValue: string,
|
||||
globalConfig?: boolean,
|
||||
add?: boolean,
|
||||
configFile?: string
|
||||
add?: boolean
|
||||
): Promise<void>
|
||||
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
|
||||
fetch(
|
||||
@@ -42,18 +33,17 @@ 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>
|
||||
isDetached(): Promise<boolean>
|
||||
lfsFetch(ref: string): Promise<void>
|
||||
lfsInstall(): Promise<void>
|
||||
log1(format?: string): Promise<string>
|
||||
referenceAdd(referenceObjects: string): Promise<void>
|
||||
remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
|
||||
removeEnvironmentVariable(name: string): void
|
||||
revParse(ref: string): Promise<string>
|
||||
@@ -66,32 +56,9 @@ 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>
|
||||
execGit(
|
||||
args: string[],
|
||||
allowAllExitCodes?: boolean,
|
||||
silent?: boolean,
|
||||
customListeners?: any
|
||||
): Promise<GitOutput>
|
||||
}
|
||||
|
||||
export async function createCommandManager(
|
||||
@@ -115,7 +82,6 @@ class GitCommandManager {
|
||||
private lfs = false
|
||||
private doSparseCheckout = false
|
||||
private workingDirectory = ''
|
||||
private gitVersion: GitVersion = new GitVersion()
|
||||
|
||||
// Private constructor; use createCommandManager()
|
||||
private constructor() {}
|
||||
@@ -205,12 +171,6 @@ class GitCommandManager {
|
||||
return result
|
||||
}
|
||||
|
||||
async disableSparseCheckout(): Promise<void> {
|
||||
await this.execGit(['sparse-checkout', 'disable'])
|
||||
// Disabling 'sparse-checkout` leaves behind an undesirable side-effect in config (even in a pristine environment).
|
||||
await this.tryConfigUnset('extensions.worktreeConfig', false)
|
||||
}
|
||||
|
||||
async sparseCheckout(sparseCheckout: string[]): Promise<void> {
|
||||
await this.execGit(['sparse-checkout', 'set', ...sparseCheckout])
|
||||
}
|
||||
@@ -252,15 +212,9 @@ class GitCommandManager {
|
||||
configKey: string,
|
||||
configValue: string,
|
||||
globalConfig?: boolean,
|
||||
add?: boolean,
|
||||
configFile?: string
|
||||
add?: boolean
|
||||
): Promise<void> {
|
||||
const args: string[] = ['config']
|
||||
if (configFile) {
|
||||
args.push('--file', configFile)
|
||||
} else {
|
||||
args.push(globalConfig ? '--global' : '--local')
|
||||
}
|
||||
const args: string[] = ['config', globalConfig ? '--global' : '--local']
|
||||
if (add) {
|
||||
args.push('--add')
|
||||
}
|
||||
@@ -291,13 +245,14 @@ class GitCommandManager {
|
||||
options: {
|
||||
filter?: string
|
||||
fetchDepth?: number
|
||||
fetchTags?: boolean
|
||||
showProgress?: boolean
|
||||
}
|
||||
): Promise<void> {
|
||||
const args = ['-c', 'protocol.version=2', 'fetch']
|
||||
// Always use --no-tags for explicit control over tag fetching
|
||||
// Tags are fetched explicitly via refspec when needed
|
||||
args.push('--no-tags')
|
||||
if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
|
||||
args.push('--no-tags')
|
||||
}
|
||||
|
||||
args.push('--prune', '--no-recurse-submodules')
|
||||
if (options.showProgress) {
|
||||
@@ -357,21 +312,6 @@ 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
|
||||
}
|
||||
@@ -413,32 +353,6 @@ class GitCommandManager {
|
||||
await this.execGit(['remote', 'add', remoteName, remoteUrl])
|
||||
}
|
||||
|
||||
async referenceAdd(referenceObjects: string): Promise<void> {
|
||||
const alternatesPath = path.join(
|
||||
this.workingDirectory,
|
||||
'.git',
|
||||
'objects',
|
||||
'info',
|
||||
'alternates'
|
||||
)
|
||||
core.info(`Configuring git alternate to reference objects at ${referenceObjects}`)
|
||||
const infoDir = path.dirname(alternatesPath)
|
||||
if (!fs.existsSync(infoDir)) {
|
||||
await fs.promises.mkdir(infoDir, { recursive: true })
|
||||
}
|
||||
|
||||
let existing = ''
|
||||
if (fs.existsSync(alternatesPath)) {
|
||||
existing = (await fs.promises.readFile(alternatesPath, 'utf8')).trim()
|
||||
}
|
||||
|
||||
const lines = existing ? existing.split('\n') : []
|
||||
if (!lines.includes(referenceObjects)) {
|
||||
lines.push(referenceObjects)
|
||||
await fs.promises.writeFile(alternatesPath, lines.join('\n') + '\n')
|
||||
}
|
||||
}
|
||||
|
||||
removeEnvironmentVariable(name: string): void {
|
||||
delete this.gitEnv[name]
|
||||
}
|
||||
@@ -530,24 +444,6 @@ 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'],
|
||||
@@ -574,65 +470,11 @@ 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
|
||||
}
|
||||
|
||||
async version(): Promise<GitVersion> {
|
||||
return this.gitVersion
|
||||
}
|
||||
|
||||
static async createCommandManager(
|
||||
workingDirectory: string,
|
||||
lfs: boolean,
|
||||
@@ -647,7 +489,7 @@ class GitCommandManager {
|
||||
return result
|
||||
}
|
||||
|
||||
async execGit(
|
||||
private async execGit(
|
||||
args: string[],
|
||||
allowAllExitCodes = false,
|
||||
silent = false,
|
||||
@@ -709,23 +551,23 @@ class GitCommandManager {
|
||||
|
||||
// Git version
|
||||
core.debug('Getting git version')
|
||||
this.gitVersion = new GitVersion()
|
||||
let gitVersion = new GitVersion()
|
||||
let gitOutput = await this.execGit(['version'])
|
||||
let stdout = gitOutput.stdout.trim()
|
||||
if (!stdout.includes('\n')) {
|
||||
const match = stdout.match(/\d+\.\d+(\.\d+)?/)
|
||||
if (match) {
|
||||
this.gitVersion = new GitVersion(match[0])
|
||||
gitVersion = new GitVersion(match[0])
|
||||
}
|
||||
}
|
||||
if (!this.gitVersion.isValid()) {
|
||||
if (!gitVersion.isValid()) {
|
||||
throw new Error('Unable to determine git version')
|
||||
}
|
||||
|
||||
// Minimum git version
|
||||
if (!this.gitVersion.checkMinimum(MinimumGitVersion)) {
|
||||
if (!gitVersion.checkMinimum(MinimumGitVersion)) {
|
||||
throw new Error(
|
||||
`Minimum required git version is ${MinimumGitVersion}. Your git ('${this.gitPath}') is ${this.gitVersion}`
|
||||
`Minimum required git version is ${MinimumGitVersion}. Your git ('${this.gitPath}') is ${gitVersion}`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -759,28 +601,22 @@ class GitCommandManager {
|
||||
|
||||
this.doSparseCheckout = doSparseCheckout
|
||||
if (this.doSparseCheckout) {
|
||||
if (!this.gitVersion.checkMinimum(MinimumGitSparseCheckoutVersion)) {
|
||||
// The `git sparse-checkout` command was introduced in Git v2.25.0
|
||||
const minimumGitSparseCheckoutVersion = new GitVersion('2.25')
|
||||
if (!gitVersion.checkMinimum(minimumGitSparseCheckoutVersion)) {
|
||||
throw new Error(
|
||||
`Minimum Git version required for sparse checkout is ${MinimumGitSparseCheckoutVersion}. Your git ('${this.gitPath}') is ${this.gitVersion}`
|
||||
`Minimum Git version required for sparse checkout is ${minimumGitSparseCheckoutVersion}. Your git ('${this.gitPath}') is ${gitVersion}`
|
||||
)
|
||||
}
|
||||
}
|
||||
// Set the user agent
|
||||
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}`
|
||||
}
|
||||
}
|
||||
|
||||
const gitHttpUserAgent = `git/${gitVersion} (github-actions-checkout)`
|
||||
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
|
||||
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
|
||||
}
|
||||
}
|
||||
|
||||
class GitOutput {
|
||||
stdout = ''
|
||||
exitCode = 0
|
||||
}
|
||||
|
||||
@@ -9,161 +9,8 @@ import * as path from 'path'
|
||||
import * as refHelper from './ref-helper'
|
||||
import * as stateHelper from './state-helper'
|
||||
import * as urlHelper from './url-helper'
|
||||
import {
|
||||
MinimumGitSparseCheckoutVersion,
|
||||
IGitCommandManager
|
||||
} from './git-command-manager'
|
||||
import {IGitCommandManager} from './git-command-manager'
|
||||
import {IGitSourceSettings} from './git-source-settings'
|
||||
import {GitCacheHelper} from './git-cache-helper'
|
||||
import * as fs from 'fs'
|
||||
|
||||
interface SubmoduleInfo {
|
||||
name: string
|
||||
path: string
|
||||
url: string
|
||||
}
|
||||
|
||||
async function iterativeSubmoduleUpdate(
|
||||
git: IGitCommandManager,
|
||||
cacheHelper: GitCacheHelper,
|
||||
repositoryPath: string,
|
||||
fetchDepth: number,
|
||||
nestedSubmodules: boolean
|
||||
): Promise<void> {
|
||||
const gitmodulesPath = path.join(repositoryPath, '.gitmodules')
|
||||
if (!fs.existsSync(gitmodulesPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const submodules = new Map<string, SubmoduleInfo>()
|
||||
|
||||
// Get all submodule config keys
|
||||
try {
|
||||
const output = await git.execGit([
|
||||
'-C', repositoryPath,
|
||||
'config', '--file', gitmodulesPath, '--get-regexp', 'submodule\\..*'
|
||||
], true, true)
|
||||
|
||||
const lines = output.stdout.split('\n').filter(l => l.trim().length > 0)
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^submodule\.(.+?)\.(path|url)\s+(.*)$/)
|
||||
if (match) {
|
||||
const [, name, key, value] = match
|
||||
if (!submodules.has(name)) {
|
||||
submodules.set(name, { name, path: '', url: '' })
|
||||
}
|
||||
const info = submodules.get(name)!
|
||||
if (key === 'path') info.path = value
|
||||
if (key === 'url') info.url = value
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
core.warning(`Failed to read .gitmodules: ${err}`)
|
||||
return
|
||||
}
|
||||
|
||||
for (const info of submodules.values()) {
|
||||
if (!info.path || !info.url) continue
|
||||
|
||||
core.info(`Processing submodule ${info.name} at ${info.path}`)
|
||||
|
||||
// Resolve relative URLs or valid URLs
|
||||
let subUrl = info.url
|
||||
if (subUrl.startsWith('../') || subUrl.startsWith('./')) {
|
||||
// In checkout action, relative URLs are handled automatically by git.
|
||||
// But for our bare cache clone, we need an absolute URL.
|
||||
let originUrl = ''
|
||||
try {
|
||||
const originOut = await git.execGit(['-C', repositoryPath, 'remote', 'get-url', 'origin'], true, true)
|
||||
if (originOut.exitCode === 0) {
|
||||
originUrl = originOut.stdout.trim()
|
||||
}
|
||||
|
||||
if (originUrl) {
|
||||
try {
|
||||
if (originUrl.match(/^https?:\/\//)) {
|
||||
// Using Node's URL class to resolve relative paths for HTTP(s)
|
||||
const parsedOrigin = new URL(originUrl.replace(/\.git$/, ''))
|
||||
const resolvedUrl = new URL(subUrl, parsedOrigin.href + '/')
|
||||
subUrl = resolvedUrl.href
|
||||
} else {
|
||||
// Fallback for SSH URLs which new URL() cannot parse (e.g. git@github.com:org/repo)
|
||||
let originParts = originUrl.replace(/\.git$/, '').split('/')
|
||||
originParts.pop() // remove current repo
|
||||
|
||||
// Handle multiple ../
|
||||
let subTarget = subUrl
|
||||
while (subTarget.startsWith('../')) {
|
||||
if (originParts.length === 0) break // Can't go higher
|
||||
originParts.pop()
|
||||
subTarget = subTarget.substring(3)
|
||||
}
|
||||
if (subTarget.startsWith('./')) {
|
||||
subTarget = subTarget.substring(2)
|
||||
}
|
||||
|
||||
if (originParts.length > 0) {
|
||||
subUrl = originParts.join('/') + '/' + subTarget
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fallback does not work
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (!subUrl || subUrl.startsWith('../') || subUrl.startsWith('./')) {
|
||||
core.warning(`Could not resolve absolute URL for submodule ${info.name}. Falling back to standard clone.`)
|
||||
await invokeStandardSubmoduleUpdate(git, repositoryPath, fetchDepth, info.path)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
// Prepare cache
|
||||
const cachePath = await cacheHelper.setupCache(git, subUrl)
|
||||
|
||||
// Submodule update for this specific one
|
||||
const args = ['-C', repositoryPath, '-c', 'protocol.version=2', 'submodule', 'update', '--init', '--force']
|
||||
if (fetchDepth > 0) {
|
||||
args.push(`--depth=${fetchDepth}`)
|
||||
}
|
||||
args.push('--reference', cachePath)
|
||||
args.push(info.path)
|
||||
|
||||
const output = await git.execGit(args, true)
|
||||
if (output.exitCode !== 0) {
|
||||
throw new Error(`Submodule update failed with exit code ${output.exitCode}`)
|
||||
}
|
||||
} catch (err) {
|
||||
core.warning(`Reference cache failed for submodule ${info.name} (${err}). Falling back to standard clone...`)
|
||||
await invokeStandardSubmoduleUpdate(git, repositoryPath, fetchDepth, info.path)
|
||||
}
|
||||
|
||||
// Recursive update inside the submodule
|
||||
if (nestedSubmodules) {
|
||||
const subRepoPath = path.join(repositoryPath, info.path)
|
||||
await iterativeSubmoduleUpdate(
|
||||
git,
|
||||
cacheHelper,
|
||||
subRepoPath,
|
||||
fetchDepth,
|
||||
nestedSubmodules
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function invokeStandardSubmoduleUpdate(git: IGitCommandManager, repositoryPath: string, fetchDepth: number, submodulePath: string) {
|
||||
const args = ['-C', repositoryPath, '-c', 'protocol.version=2', 'submodule', 'update', '--init', '--force']
|
||||
if (fetchDepth > 0) {
|
||||
args.push(`--depth=${fetchDepth}`)
|
||||
}
|
||||
args.push(submodulePath)
|
||||
await git.execGit(args)
|
||||
}
|
||||
|
||||
export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
// Repository URL
|
||||
@@ -255,19 +102,6 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
// Save state for POST action
|
||||
stateHelper.setRepositoryPath(settings.repositoryPath)
|
||||
|
||||
// If we didn't initialize it above, do it now
|
||||
if (!authHelper) {
|
||||
authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
}
|
||||
|
||||
// Check if we need global auth setup early for reference cache
|
||||
// Global auth does not require a local .git directory
|
||||
if (settings.referenceCache) {
|
||||
core.startGroup('Setting up global auth for reference cache')
|
||||
await authHelper.configureGlobalAuth()
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
// Initialize the repository
|
||||
if (
|
||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||
@@ -276,35 +110,8 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
await git.init()
|
||||
await git.remoteAdd('origin', repositoryUrl)
|
||||
core.endGroup()
|
||||
|
||||
// Setup reference cache if requested
|
||||
if (settings.referenceCache) {
|
||||
core.startGroup('Setting up reference repository cache')
|
||||
const cacheHelper = new GitCacheHelper(settings.referenceCache)
|
||||
const cachePath = await cacheHelper.setupCache(git, repositoryUrl)
|
||||
const cacheObjects = path.join(cachePath, 'objects')
|
||||
if (fsHelper.directoryExistsSync(cacheObjects, false)) {
|
||||
await git.referenceAdd(cacheObjects)
|
||||
} else {
|
||||
core.warning(`Reference repository cache objects directory ${cacheObjects} does not exist`)
|
||||
}
|
||||
core.endGroup()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove global auth if it was set for reference cache,
|
||||
// to avoid duplicate AUTHORIZATION headers during fetch
|
||||
if (settings.referenceCache) {
|
||||
core.startGroup('Removing global auth after reference cache setup')
|
||||
await authHelper.removeGlobalAuth()
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
// Configure auth (must happen after git init so .git exists)
|
||||
core.startGroup('Setting up auth')
|
||||
await authHelper.configureAuth()
|
||||
core.endGroup()
|
||||
|
||||
// Disable automatic garbage collection
|
||||
core.startGroup('Disabling automatic garbage collection')
|
||||
if (!(await git.tryDisableAutomaticGarbageCollection())) {
|
||||
@@ -314,6 +121,15 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
}
|
||||
core.endGroup()
|
||||
|
||||
// If we didn't initialize it above, do it now
|
||||
if (!authHelper) {
|
||||
authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
}
|
||||
// Configure auth
|
||||
core.startGroup('Setting up auth')
|
||||
await authHelper.configureAuth()
|
||||
core.endGroup()
|
||||
|
||||
// Determine the default branch
|
||||
if (!settings.ref && !settings.commit) {
|
||||
core.startGroup('Determining the default branch')
|
||||
@@ -335,15 +151,12 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
await git.lfsInstall()
|
||||
}
|
||||
|
||||
// When using reference cache, fetch-depth > 0 is counterproductive:
|
||||
// objects are served from the local cache, so shallow negotiation only adds latency.
|
||||
adjustFetchDepthForCache(settings)
|
||||
|
||||
// Fetch
|
||||
core.startGroup('Fetching the repository')
|
||||
const fetchOptions: {
|
||||
filter?: string
|
||||
fetchDepth?: number
|
||||
fetchTags?: boolean
|
||||
showProgress?: boolean
|
||||
} = {}
|
||||
|
||||
@@ -366,35 +179,12 @@ 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
|
||||
const refSpec = refHelper.getRefSpec(
|
||||
settings.ref,
|
||||
settings.commit,
|
||||
settings.fetchTags
|
||||
)
|
||||
fetchOptions.fetchTags = settings.fetchTags
|
||||
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
||||
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()
|
||||
|
||||
@@ -418,13 +208,7 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
}
|
||||
|
||||
// Sparse checkout
|
||||
if (!settings.sparseCheckout) {
|
||||
let gitVersion = await git.version()
|
||||
// no need to disable sparse-checkout if the installed git runtime doesn't even support it.
|
||||
if (gitVersion.checkMinimum(MinimumGitSparseCheckoutVersion)) {
|
||||
await git.disableSparseCheckout()
|
||||
}
|
||||
} else {
|
||||
if (settings.sparseCheckout) {
|
||||
core.startGroup('Setting up sparse checkout')
|
||||
if (settings.sparseCheckoutConeMode) {
|
||||
await git.sparseCheckout(settings.sparseCheckout)
|
||||
@@ -449,21 +233,7 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
// Checkout submodules
|
||||
core.startGroup('Fetching submodules')
|
||||
await git.submoduleSync(settings.nestedSubmodules)
|
||||
|
||||
if (settings.referenceCache) {
|
||||
core.info('Iterative submodule update using reference cache')
|
||||
const cacheHelper = new GitCacheHelper(settings.referenceCache)
|
||||
await iterativeSubmoduleUpdate(
|
||||
git,
|
||||
cacheHelper,
|
||||
settings.repositoryPath,
|
||||
settings.fetchDepth,
|
||||
settings.nestedSubmodules
|
||||
)
|
||||
} else {
|
||||
await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules)
|
||||
}
|
||||
|
||||
await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules)
|
||||
await git.submoduleForeach(
|
||||
'git config --local gc.auto 0',
|
||||
settings.nestedSubmodules
|
||||
@@ -482,8 +252,7 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
const commitInfo = await git.log1()
|
||||
|
||||
// Log commit sha
|
||||
const commitSHA = await git.log1('--format=%H')
|
||||
core.setOutput('commit', commitSHA.trim())
|
||||
await git.log1("--format='%H'")
|
||||
|
||||
// Check for incorrect pull request merge commit
|
||||
await refHelper.checkCommitInfo(
|
||||
@@ -572,30 +341,3 @@ async function getGitCommandManager(
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts fetchDepth when reference-cache is active.
|
||||
* Shallow fetches are counterproductive with a local cache because
|
||||
* objects are served from disk, making shallow negotiation pure overhead.
|
||||
*/
|
||||
export function adjustFetchDepthForCache(
|
||||
settings: Pick<
|
||||
IGitSourceSettings,
|
||||
'referenceCache' | 'fetchDepth' | 'fetchDepthExplicit'
|
||||
>
|
||||
): void {
|
||||
if (settings.referenceCache && settings.fetchDepth > 0) {
|
||||
if (settings.fetchDepthExplicit) {
|
||||
core.warning(
|
||||
`'fetch-depth: ${settings.fetchDepth}' is set with reference-cache enabled. ` +
|
||||
`This may slow down checkout because shallow negotiation bypasses the local cache. ` +
|
||||
`Consider using 'fetch-depth: 0' for best performance with reference-cache.`
|
||||
)
|
||||
} else {
|
||||
core.info(
|
||||
`Overriding fetch-depth from ${settings.fetchDepth} to 0 because reference-cache is enabled`
|
||||
)
|
||||
settings.fetchDepth = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,11 +49,6 @@ export interface IGitSourceSettings {
|
||||
*/
|
||||
fetchDepth: number
|
||||
|
||||
/**
|
||||
* Whether fetch-depth was explicitly set by the user
|
||||
*/
|
||||
fetchDepthExplicit: boolean
|
||||
|
||||
/**
|
||||
* Fetch tags, even if fetchDepth > 0 (default: false)
|
||||
*/
|
||||
@@ -64,11 +59,6 @@ export interface IGitSourceSettings {
|
||||
*/
|
||||
showProgress: boolean
|
||||
|
||||
/**
|
||||
* The path to a local directory used as a reference cache for Git clones
|
||||
*/
|
||||
referenceCache: string
|
||||
|
||||
/**
|
||||
* Indicates whether to fetch LFS objects
|
||||
*/
|
||||
@@ -104,11 +94,6 @@ export interface IGitSourceSettings {
|
||||
*/
|
||||
sshStrict: boolean
|
||||
|
||||
/**
|
||||
* The SSH user to login as
|
||||
*/
|
||||
sshUser: string
|
||||
|
||||
/**
|
||||
* Indicates whether to persist the credentials on disk to enable scripting authenticated git commands
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as io from '@actions/io'
|
||||
import * as path from 'path'
|
||||
import * as retryHelper from './retry-helper'
|
||||
import * as toolCache from '@actions/tool-cache'
|
||||
import {v4 as uuid} from 'uuid'
|
||||
import {default as uuid} from 'uuid/v4'
|
||||
import {getServerApiUrl} from './url-helper'
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
@@ -35,9 +35,7 @@ export async function downloadRepository(
|
||||
// Write archive to disk
|
||||
core.info('Writing archive to disk')
|
||||
const uniqueId = uuid()
|
||||
const archivePath = IS_WINDOWS
|
||||
? path.join(repositoryPath, `${uniqueId}.zip`)
|
||||
: path.join(repositoryPath, `${uniqueId}.tar.gz`)
|
||||
const archivePath = path.join(repositoryPath, `${uniqueId}.tar.gz`)
|
||||
await fs.promises.writeFile(archivePath, archiveData)
|
||||
archiveData = Buffer.from('') // Free memory
|
||||
|
||||
|
||||
@@ -102,9 +102,7 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
'TRUE'
|
||||
|
||||
// Fetch depth
|
||||
const fetchDepthInput = core.getInput('fetch-depth')
|
||||
result.fetchDepthExplicit = fetchDepthInput !== ''
|
||||
result.fetchDepth = Math.floor(Number(fetchDepthInput || '1'))
|
||||
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))
|
||||
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {
|
||||
result.fetchDepth = 0
|
||||
}
|
||||
@@ -145,7 +143,6 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
result.sshKnownHosts = core.getInput('ssh-known-hosts')
|
||||
result.sshStrict =
|
||||
(core.getInput('ssh-strict') || 'true').toUpperCase() === 'TRUE'
|
||||
result.sshUser = core.getInput('ssh-user')
|
||||
|
||||
// Persist credentials
|
||||
result.persistCredentials =
|
||||
@@ -163,9 +160,5 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
result.githubServerUrl = core.getInput('github-server-url')
|
||||
core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
|
||||
|
||||
// Reference Cache
|
||||
result.referenceCache = core.getInput('reference-cache')
|
||||
core.debug(`Reference Cache = ${result.referenceCache}`)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ async function run(): Promise<void> {
|
||||
|
||||
// Get sources
|
||||
await gitSourceProvider.getSource(sourceSettings)
|
||||
core.setOutput('ref', sourceSettings.ref)
|
||||
} finally {
|
||||
// Unregister problem matcher
|
||||
coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '')
|
||||
|
||||
@@ -20,7 +20,7 @@ function updateUsage(
|
||||
}
|
||||
|
||||
// Load the action.yml
|
||||
const actionYaml = yaml.load(fs.readFileSync(actionYamlPath).toString())
|
||||
const actionYaml = yaml.safeLoad(fs.readFileSync(actionYamlPath).toString())
|
||||
|
||||
// Load the README
|
||||
const originalReadme = fs.readFileSync(readmePath).toString()
|
||||
@@ -120,7 +120,7 @@ function updateUsage(
|
||||
}
|
||||
|
||||
updateUsage(
|
||||
'actions/checkout@v6',
|
||||
'actions/checkout@v4',
|
||||
path.join(__dirname, '..', '..', 'action.yml'),
|
||||
path.join(__dirname, '..', '..', 'README.md')
|
||||
)
|
||||
|
||||
@@ -42,12 +42,8 @@ export async function getCheckoutInfo(
|
||||
result.ref = `refs/remotes/pull/${branch}`
|
||||
}
|
||||
// refs/tags/
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
result.ref = ref
|
||||
}
|
||||
// refs/
|
||||
else if (upperRef.startsWith('REFS/')) {
|
||||
result.ref = commit ? commit : ref
|
||||
result.ref = ref
|
||||
}
|
||||
// Unqualified ref, check for a matching branch or tag
|
||||
else {
|
||||
@@ -76,75 +72,55 @@ export function getRefSpecForAllHistory(ref: string, commit: string): string[] {
|
||||
return result
|
||||
}
|
||||
|
||||
export function getRefSpec(
|
||||
ref: string,
|
||||
commit: string,
|
||||
fetchTags?: boolean
|
||||
): string[] {
|
||||
export function getRefSpec(ref: string, commit: string): 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)
|
||||
result.push(`+${commit}:refs/remotes/origin/${branch}`)
|
||||
return [`+${commit}:refs/remotes/origin/${branch}`]
|
||||
}
|
||||
// refs/pull/
|
||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||
const branch = ref.substring('refs/pull/'.length)
|
||||
result.push(`+${commit}:refs/remotes/pull/${branch}`)
|
||||
return [`+${commit}:refs/remotes/pull/${branch}`]
|
||||
}
|
||||
// refs/tags/
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
if (!fetchTags) {
|
||||
result.push(`+${ref}:${ref}`)
|
||||
}
|
||||
return [`+${commit}:${ref}`]
|
||||
}
|
||||
// Otherwise no destination ref
|
||||
else {
|
||||
result.push(commit)
|
||||
return [commit]
|
||||
}
|
||||
}
|
||||
// Unqualified ref, check for a matching branch or tag
|
||||
else if (!upperRef.startsWith('REFS/')) {
|
||||
result.push(`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`)
|
||||
if (!fetchTags) {
|
||||
result.push(`+refs/tags/${ref}*:refs/tags/${ref}*`)
|
||||
}
|
||||
return [
|
||||
`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`,
|
||||
`+refs/tags/${ref}*:refs/tags/${ref}*`
|
||||
]
|
||||
}
|
||||
// refs/heads/
|
||||
else if (upperRef.startsWith('REFS/HEADS/')) {
|
||||
const branch = ref.substring('refs/heads/'.length)
|
||||
result.push(`+${ref}:refs/remotes/origin/${branch}`)
|
||||
return [`+${ref}:refs/remotes/origin/${branch}`]
|
||||
}
|
||||
// refs/pull/
|
||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||
const branch = ref.substring('refs/pull/'.length)
|
||||
result.push(`+${ref}:refs/remotes/pull/${branch}`)
|
||||
return [`+${ref}:refs/remotes/pull/${branch}`]
|
||||
}
|
||||
// refs/tags/
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
if (!fetchTags) {
|
||||
result.push(`+${ref}:${ref}`)
|
||||
}
|
||||
}
|
||||
// Other refs
|
||||
else {
|
||||
result.push(`+${ref}:${ref}`)
|
||||
return [`+${ref}:${ref}`]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,10 +166,8 @@ 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}^{commit}`))
|
||||
(await git.tagExists(tagName)) && commit === (await git.revParse(ref))
|
||||
)
|
||||
}
|
||||
// Unexpected
|
||||
|
||||
@@ -12,8 +12,7 @@ export function getFetchUrl(settings: IGitSourceSettings): string {
|
||||
const encodedOwner = encodeURIComponent(settings.repositoryOwner)
|
||||
const encodedName = encodeURIComponent(settings.repositoryName)
|
||||
if (settings.sshKey) {
|
||||
const user = settings.sshUser.length > 0 ? settings.sshUser : 'git'
|
||||
return `${user}@${serviceUrl.hostname}:${encodedOwner}/${encodedName}.git`
|
||||
return `git@${serviceUrl.hostname}:${encodedOwner}/${encodedName}.git`
|
||||
}
|
||||
|
||||
// "origin" is SCHEME://HOSTNAME[:PORT]
|
||||
@@ -21,61 +20,26 @@ export function getFetchUrl(settings: IGitSourceSettings): string {
|
||||
}
|
||||
|
||||
export function getServerUrl(url?: string): URL {
|
||||
let resolvedUrl = process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
if (hasContent(url, WhitespaceMode.Trim)) {
|
||||
resolvedUrl = url!
|
||||
}
|
||||
|
||||
return new URL(resolvedUrl)
|
||||
let urlValue =
|
||||
url && url.trim().length > 0
|
||||
? url
|
||||
: process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
return new URL(urlValue)
|
||||
}
|
||||
|
||||
export function getServerApiUrl(url?: string): string {
|
||||
if (hasContent(url, WhitespaceMode.Trim)) {
|
||||
let serverUrl = getServerUrl(url)
|
||||
if (isGhes(url)) {
|
||||
serverUrl.pathname = 'api/v3'
|
||||
} else {
|
||||
serverUrl.hostname = 'api.' + serverUrl.hostname
|
||||
}
|
||||
let apiUrl = 'https://api.github.com'
|
||||
|
||||
return pruneSuffix(serverUrl.toString(), '/')
|
||||
if (isGhes(url)) {
|
||||
const serverUrl = getServerUrl(url)
|
||||
apiUrl = new URL(`${serverUrl.origin}/api/v3`).toString()
|
||||
}
|
||||
|
||||
return process.env['GITHUB_API_URL'] || 'https://api.github.com'
|
||||
return apiUrl
|
||||
}
|
||||
|
||||
export function isGhes(url?: string): boolean {
|
||||
const ghUrl = new URL(
|
||||
url || process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
)
|
||||
const ghUrl = getServerUrl(url)
|
||||
|
||||
const hostname = ghUrl.hostname.trimEnd().toUpperCase()
|
||||
const isGitHubHost = hostname === 'GITHUB.COM'
|
||||
const isGitHubEnterpriseCloudHost = hostname.endsWith('.GHE.COM')
|
||||
const isLocalHost = hostname.endsWith('.LOCALHOST')
|
||||
|
||||
return !isGitHubHost && !isGitHubEnterpriseCloudHost && !isLocalHost
|
||||
}
|
||||
|
||||
function pruneSuffix(text: string, suffix: string) {
|
||||
if (hasContent(suffix, WhitespaceMode.Preserve) && text?.endsWith(suffix)) {
|
||||
return text.substring(0, text.length - suffix.length)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
enum WhitespaceMode {
|
||||
Trim,
|
||||
Preserve
|
||||
}
|
||||
|
||||
function hasContent(
|
||||
text: string | undefined,
|
||||
whitespaceMode: WhitespaceMode
|
||||
): boolean {
|
||||
let refinedText = text ?? ''
|
||||
if (whitespaceMode == WhitespaceMode.Trim) {
|
||||
refinedText = refinedText.trim()
|
||||
}
|
||||
return refinedText.length > 0
|
||||
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user