mirror of
https://github.com/actions/checkout.git
synced 2026-08-02 07:24:23 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f548e57e54 | ||
|
|
3d3c42e5aa | ||
|
|
28802689a1 | ||
|
|
12cd2235ef | ||
|
|
62661c4e71 | ||
|
|
e8d4307400 | ||
|
|
631c942040 | ||
|
|
4f1f4aec02 | ||
|
|
ba097532fb | ||
|
|
b9e0990d21 | ||
|
|
e8cb398be4 | ||
|
|
5de26ee9b1 | ||
|
|
79102f2503 | ||
|
|
9c091bb21b | ||
|
|
1044a6dea9 | ||
|
|
f0282184c7 | ||
|
|
d914b262ff | ||
|
|
537c7ef99c | ||
|
|
130a169078 | ||
|
|
7d09575332 | ||
|
|
0f9f3aa320 | ||
|
|
f9e715a95f | ||
|
|
df4cb1c069 | ||
|
|
1cce3390c2 | ||
|
|
900f2210b1 | ||
|
|
0c366fd6a8 | ||
|
|
de0fac2e45 | ||
|
|
064fe7f331 | ||
|
|
8e8c483db8 | ||
|
|
033fa0dc0b | ||
|
|
c2d88d3ecc | ||
|
|
1af3b93b68 | ||
|
|
71cf2267d8 | ||
|
|
069c695914 | ||
|
|
ff7abcd0c3 | ||
|
|
08c6903cd8 | ||
|
|
9f265659d3 |
@ -1,3 +0,0 @@
|
|||||||
dist/
|
|
||||||
lib/
|
|
||||||
node_modules/
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": ["jest", "@typescript-eslint"],
|
|
||||||
"extends": ["plugin:github/recommended"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 9,
|
|
||||||
"sourceType": "module",
|
|
||||||
"project": "./tsconfig.json"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"eslint-comments/no-use": "off",
|
|
||||||
"import/no-namespace": "off",
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
|
||||||
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
|
||||||
"@typescript-eslint/no-require-imports": "error",
|
|
||||||
"@typescript-eslint/array-type": "error",
|
|
||||||
"@typescript-eslint/await-thenable": "error",
|
|
||||||
"camelcase": "off",
|
|
||||||
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
|
|
||||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
|
||||||
"@typescript-eslint/no-array-constructor": "error",
|
|
||||||
"@typescript-eslint/no-empty-interface": "error",
|
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
|
||||||
"@typescript-eslint/no-extraneous-class": "error",
|
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
|
||||||
"@typescript-eslint/no-for-in-array": "error",
|
|
||||||
"@typescript-eslint/no-inferrable-types": "error",
|
|
||||||
"@typescript-eslint/no-misused-new": "error",
|
|
||||||
"@typescript-eslint/no-namespace": "error",
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
|
||||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
|
||||||
"@typescript-eslint/no-useless-constructor": "error",
|
|
||||||
"@typescript-eslint/no-var-requires": "error",
|
|
||||||
"@typescript-eslint/prefer-for-of": "warn",
|
|
||||||
"@typescript-eslint/prefer-function-type": "warn",
|
|
||||||
"@typescript-eslint/prefer-includes": "error",
|
|
||||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
|
||||||
"@typescript-eslint/promise-function-async": "error",
|
|
||||||
"@typescript-eslint/require-array-sort-compare": "error",
|
|
||||||
"@typescript-eslint/restrict-plus-operands": "error",
|
|
||||||
"semi": "off",
|
|
||||||
"@typescript-eslint/semi": ["error", "never"],
|
|
||||||
"@typescript-eslint/type-annotation-spacing": "error",
|
|
||||||
"@typescript-eslint/unbound-method": "error"
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"node": true,
|
|
||||||
"es6": true,
|
|
||||||
"jest/globals": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
.github/workflows/check-dist.yml
vendored
10
.github/workflows/check-dist.yml
vendored
@ -22,12 +22,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.6
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set Node.js 20.x
|
- name: Set Node.js 24.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 24.x
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@ -44,7 +44,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# If dist/ was different than expected, upload the expected version as an artifact
|
# If dist/ was different than expected, upload the expected version as an artifact
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v7
|
||||||
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
|
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
|
|||||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -39,10 +39,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# 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)
|
- run: rm -rf dist # We want code scanning to analyze lib instead (individual .js files)
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
|
|||||||
2
.github/workflows/licensed.yml
vendored
2
.github/workflows/licensed.yml
vendored
@ -9,6 +9,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Check licenses
|
name: Check licenses
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.6
|
- uses: actions/checkout@v7
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run licensed-check
|
- run: npm run licensed-check
|
||||||
@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checking out
|
- name: Checking out
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
- name: Publish
|
- name: Publish
|
||||||
id: publish
|
id: publish
|
||||||
uses: actions/publish-immutable-action@0.0.3
|
uses: actions/publish-immutable-action@v0.0.4
|
||||||
|
|||||||
56
.github/workflows/test.yml
vendored
56
.github/workflows/test.yml
vendored
@ -16,10 +16,10 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 24.x
|
||||||
- uses: actions/checkout@v4.1.6
|
- uses: actions/checkout@v7
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run format-check
|
- run: npm run format-check
|
||||||
@ -37,7 +37,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Clone this repo
|
# Clone this repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
# Basic checkout
|
# Basic checkout
|
||||||
- name: Checkout basic
|
- name: Checkout basic
|
||||||
@ -87,6 +87,17 @@ jobs:
|
|||||||
- name: Verify fetch filter
|
- name: Verify fetch filter
|
||||||
run: __test__/verify-fetch-filter.sh
|
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
|
# Sparse checkout
|
||||||
- name: Sparse checkout
|
- name: Sparse checkout
|
||||||
uses: ./
|
uses: ./
|
||||||
@ -165,6 +176,22 @@ jobs:
|
|||||||
- name: Verify submodules recursive
|
- name: Verify submodules recursive
|
||||||
run: __test__/verify-submodules-recursive.sh
|
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
|
# Basic checkout using REST API
|
||||||
- name: Remove basic
|
- name: Remove basic
|
||||||
if: runner.os != 'windows'
|
if: runner.os != 'windows'
|
||||||
@ -202,7 +229,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Clone this repo
|
# Clone this repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
# Basic checkout using git
|
# Basic checkout using git
|
||||||
- name: Checkout basic
|
- name: Checkout basic
|
||||||
@ -234,7 +261,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Clone this repo
|
# Clone this repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
# Basic checkout using git
|
# Basic checkout using git
|
||||||
- name: Checkout basic
|
- name: Checkout basic
|
||||||
@ -264,7 +291,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Clone this repo
|
# Clone this repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
path: localClone
|
path: localClone
|
||||||
|
|
||||||
@ -291,8 +318,8 @@ jobs:
|
|||||||
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
|
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
|
||||||
|
|
||||||
# needed to make checkout post cleanup succeed
|
# needed to make checkout post cleanup succeed
|
||||||
- name: Fix Checkout v4
|
- name: Fix Checkout v6
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
path: localClone
|
path: localClone
|
||||||
|
|
||||||
@ -301,13 +328,16 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Clone this repo
|
# Clone this repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
path: actions-checkout
|
||||||
|
|
||||||
# Basic checkout using git
|
# Basic checkout using git
|
||||||
- name: Checkout basic
|
- name: Checkout basic
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: ./
|
uses: ./actions-checkout
|
||||||
with:
|
with:
|
||||||
|
path: cloned-using-local-action
|
||||||
ref: test-data/v2/basic
|
ref: test-data/v2/basic
|
||||||
|
|
||||||
# Verify output
|
# Verify output
|
||||||
@ -325,7 +355,3 @@ jobs:
|
|||||||
echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d"
|
echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# needed to make checkout post cleanup succeed
|
|
||||||
- name: Fix Checkout
|
|
||||||
uses: actions/checkout@v4.1.6
|
|
||||||
|
|||||||
3
.github/workflows/update-main-version.yml
vendored
3
.github/workflows/update-main-version.yml
vendored
@ -11,6 +11,7 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
description: The major version to update
|
description: The major version to update
|
||||||
options:
|
options:
|
||||||
|
- v5
|
||||||
- v4
|
- v4
|
||||||
- v3
|
- v3
|
||||||
- v2
|
- v2
|
||||||
@ -22,7 +23,7 @@ jobs:
|
|||||||
# Note this update workflow can also be used as a rollback tool.
|
# 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
|
# For that reason, it's best to pin `actions/checkout` to a known, stable version
|
||||||
# (typically, about two releases back).
|
# (typically, about two releases back).
|
||||||
- uses: actions/checkout@v4.1.6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Git config
|
- name: Git config
|
||||||
|
|||||||
6
.github/workflows/update-test-ubuntu-git.yml
vendored
6
.github/workflows/update-test-ubuntu-git.yml
vendored
@ -26,12 +26,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
# Use `docker/login-action` to log in to GHCR.io.
|
# Use `docker/login-action` to log in to GHCR.io.
|
||||||
# Once published, the packages are scoped to the account defined here.
|
# Once published, the packages are scoped to the account defined here.
|
||||||
- name: Log in to the ghcr.io container registry
|
- name: Log in to the ghcr.io container registry
|
||||||
uses: docker/login-action@v3.3.0
|
uses: docker/login-action@v4.4.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@ -48,7 +48,7 @@ jobs:
|
|||||||
|
|
||||||
# Use `docker/build-push-action` to build (and optionally publish) the image.
|
# Use `docker/build-push-action` to build (and optionally publish) the image.
|
||||||
- name: Build Docker Image (with optional Push)
|
- name: Build Docker Image (with optional Push)
|
||||||
uses: docker/build-push-action@v6.5.0
|
uses: docker/build-push-action@v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: images/test-ubuntu-git.Dockerfile
|
file: images/test-ubuntu-git.Dockerfile
|
||||||
|
|||||||
@ -12,3 +12,4 @@ allowed:
|
|||||||
|
|
||||||
reviewed:
|
reviewed:
|
||||||
npm:
|
npm:
|
||||||
|
- "@actions/http-client" # MIT
|
||||||
|
|||||||
BIN
.licenses/npm/@actions/core.dep.yml
generated
BIN
.licenses/npm/@actions/core.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/@actions/exec.dep.yml
generated
BIN
.licenses/npm/@actions/exec.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/@actions/github.dep.yml
generated
BIN
.licenses/npm/@actions/github.dep.yml
generated
Binary file not shown.
Binary file not shown.
BIN
.licenses/npm/@actions/http-client-4.0.1.dep.yml
generated
Normal file
BIN
.licenses/npm/@actions/http-client-4.0.1.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@actions/io.dep.yml
generated
BIN
.licenses/npm/@actions/io.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/@actions/tool-cache.dep.yml
generated
BIN
.licenses/npm/@actions/tool-cache.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/@fastify/busboy.dep.yml
generated
BIN
.licenses/npm/@fastify/busboy.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/@octokit/auth-token.dep.yml
generated
BIN
.licenses/npm/@octokit/auth-token.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/@octokit/core.dep.yml
generated
BIN
.licenses/npm/@octokit/core.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/@octokit/endpoint.dep.yml
generated
BIN
.licenses/npm/@octokit/endpoint.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/@octokit/graphql.dep.yml
generated
BIN
.licenses/npm/@octokit/graphql.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/@octokit/openapi-types-22.1.0.dep.yml
generated
BIN
.licenses/npm/@octokit/openapi-types-22.1.0.dep.yml
generated
Binary file not shown.
Binary file not shown.
BIN
.licenses/npm/@octokit/plugin-paginate-rest.dep.yml
generated
BIN
.licenses/npm/@octokit/plugin-paginate-rest.dep.yml
generated
Binary file not shown.
Binary file not shown.
BIN
.licenses/npm/@octokit/request-error.dep.yml
generated
BIN
.licenses/npm/@octokit/request-error.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/@octokit/request.dep.yml
generated
BIN
.licenses/npm/@octokit/request.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/@octokit/types-13.4.1.dep.yml
generated
BIN
.licenses/npm/@octokit/types-13.4.1.dep.yml
generated
Binary file not shown.
Binary file not shown.
BIN
.licenses/npm/before-after-hook.dep.yml
generated
BIN
.licenses/npm/before-after-hook.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/content-type.dep.yml
generated
Normal file
BIN
.licenses/npm/content-type.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/deprecation.dep.yml
generated
BIN
.licenses/npm/deprecation.dep.yml
generated
Binary file not shown.
Binary file not shown.
BIN
.licenses/npm/once.dep.yml
generated
BIN
.licenses/npm/once.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/semver.dep.yml
generated
BIN
.licenses/npm/semver.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/undici.dep.yml
generated
BIN
.licenses/npm/undici.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/universal-user-agent.dep.yml
generated
BIN
.licenses/npm/universal-user-agent.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/uuid-8.3.2.dep.yml
generated
BIN
.licenses/npm/uuid-8.3.2.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/uuid-9.0.1.dep.yml
generated
BIN
.licenses/npm/uuid-9.0.1.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/wrappy.dep.yml
generated
BIN
.licenses/npm/wrappy.dep.yml
generated
Binary file not shown.
35
CHANGELOG.md
35
CHANGELOG.md
@ -1,6 +1,39 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## V4.3.0
|
## v7.0.1
|
||||||
|
* Skip running unsafe pr check if input is default by @aiqiaoy in https://github.com/actions/checkout/pull/2518
|
||||||
|
* Trim only ascii whitespace for branch by @aiqiaoy in https://github.com/actions/checkout/pull/2521
|
||||||
|
* Escape values passed to --unset by @aiqiaoy in https://github.com/actions/checkout/pull/2530
|
||||||
|
* Various dependency updates
|
||||||
|
|
||||||
|
## v7.0.0
|
||||||
|
* Block checking out fork PR for pull_request_target and workflow_run by @aiqiaoy in https://github.com/actions/checkout/pull/2454
|
||||||
|
* Various dependency updates
|
||||||
|
|
||||||
|
## v6.0.3
|
||||||
|
* Fix checkout init for SHA-256 repositories by @yaananth in https://github.com/actions/checkout/pull/2439
|
||||||
|
* fix: expand merge commit SHA regex and add SHA-256 test cases by @yaananth in https://github.com/actions/checkout/pull/2414
|
||||||
|
|
||||||
|
## v6.0.2
|
||||||
|
* Fix tag handling: preserve annotations and explicit fetch-tags by @ericsciple in https://github.com/actions/checkout/pull/2356
|
||||||
|
|
||||||
|
## v6.0.1
|
||||||
|
* Add worktree support for persist-credentials includeIf by @ericsciple in https://github.com/actions/checkout/pull/2327
|
||||||
|
|
||||||
|
## v6.0.0
|
||||||
|
* Persist creds to a separate file by @ericsciple in https://github.com/actions/checkout/pull/2286
|
||||||
|
* Update README to include Node.js 24 support details and requirements by @salmanmkc in https://github.com/actions/checkout/pull/2248
|
||||||
|
|
||||||
|
## v5.0.1
|
||||||
|
* Port v6 cleanup to v5 by @ericsciple in https://github.com/actions/checkout/pull/2301
|
||||||
|
|
||||||
|
## v5.0.0
|
||||||
|
* Update actions checkout to use node 24 by @salmanmkc in https://github.com/actions/checkout/pull/2226
|
||||||
|
|
||||||
|
## v4.3.1
|
||||||
|
* Port v6 cleanup to v4 by @ericsciple in https://github.com/actions/checkout/pull/2305
|
||||||
|
|
||||||
|
## v4.3.0
|
||||||
* docs: update README.md by @motss in https://github.com/actions/checkout/pull/1971
|
* 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
|
* 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
|
* Documentation update - add recommended permissions to Readme by @benwells in https://github.com/actions/checkout/pull/2043
|
||||||
|
|||||||
95
README.md
95
README.md
@ -1,6 +1,30 @@
|
|||||||
[](https://github.com/actions/checkout/actions/workflows/test.yml)
|
[](https://github.com/actions/checkout/actions/workflows/test.yml)
|
||||||
|
|
||||||
# Checkout V4
|
# Checkout v7
|
||||||
|
|
||||||
|
## What's new
|
||||||
|
|
||||||
|
- Safer fork pull request handling: checkout now refuses to check out fork pull request code by default when the workflow is triggered by `pull_request_target` or `workflow_run`. These triggers run with the base repository's `GITHUB_TOKEN`, secrets, and runner access, where executing a fork's code commonly leads to "pwn request" vulnerabilities.
|
||||||
|
- To opt in after [reviewing the risks](https://gh.io/securely-using-pull_request_target), set the new `allow-unsafe-pr-checkout: true` input.
|
||||||
|
- Migrated `actions/checkout` to ESM to support new versions of the `@actions/*` packages.
|
||||||
|
- Updated direct and transitive dependencies, including security fixes for known vulnerabilities.
|
||||||
|
|
||||||
|
# Checkout v6
|
||||||
|
|
||||||
|
## What's new
|
||||||
|
|
||||||
|
- Improved credential security: `persist-credentials` now stores credentials in a separate file under `$RUNNER_TEMP` instead of directly in `.git/config`
|
||||||
|
- No workflow changes required — `git fetch`, `git push`, etc. continue to work automatically
|
||||||
|
- Running authenticated git commands from a [Docker container action](https://docs.github.com/actions/sharing-automations/creating-actions/creating-a-docker-container-action) requires Actions Runner [v2.329.0](https://github.com/actions/runner/releases/tag/v2.329.0) or later
|
||||||
|
|
||||||
|
# Checkout v5
|
||||||
|
|
||||||
|
## What's new
|
||||||
|
|
||||||
|
- Updated to the node24 runtime
|
||||||
|
- This requires a minimum Actions Runner version of [v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) to run.
|
||||||
|
|
||||||
|
# Checkout v4
|
||||||
|
|
||||||
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
|
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
|
||||||
|
|
||||||
@ -36,7 +60,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
|
|
||||||
<!-- start usage -->
|
<!-- start usage -->
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
# Repository name with owner. For example, actions/checkout
|
# Repository name with owner. For example, actions/checkout
|
||||||
# Default: ${{ github.repository }}
|
# Default: ${{ github.repository }}
|
||||||
@ -158,24 +182,33 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
|
|
||||||
# Scenarios
|
# Scenarios
|
||||||
|
|
||||||
- [Fetch only the root files](#Fetch-only-the-root-files)
|
- [Checkout V5](#checkout-v5)
|
||||||
- [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder)
|
- [What's new](#whats-new)
|
||||||
- [Fetch only a single file](#Fetch-only-a-single-file)
|
- [Checkout V4](#checkout-v4)
|
||||||
- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches)
|
- [Note](#note)
|
||||||
- [Checkout a different branch](#Checkout-a-different-branch)
|
- [What's new](#whats-new-1)
|
||||||
- [Checkout HEAD^](#Checkout-HEAD)
|
- [Usage](#usage)
|
||||||
- [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side)
|
- [Scenarios](#scenarios)
|
||||||
- [Checkout multiple repos (nested)](#Checkout-multiple-repos-nested)
|
- [Fetch only the root files](#fetch-only-the-root-files)
|
||||||
- [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
- [Fetch only the root files and `.github` and `src` folder](#fetch-only-the-root-files-and-github-and-src-folder)
|
||||||
- [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit)
|
- [Fetch only a single file](#fetch-only-a-single-file)
|
||||||
- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
|
- [Fetch all history for all tags and branches](#fetch-all-history-for-all-tags-and-branches)
|
||||||
- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token)
|
- [Checkout a different branch](#checkout-a-different-branch)
|
||||||
- [Push a commit to a PR using the built-in token](#Push-a-commit-to-a-PR-using-the-built-in-token)
|
- [Checkout HEAD^](#checkout-head)
|
||||||
|
- [Checkout multiple repos (side by side)](#checkout-multiple-repos-side-by-side)
|
||||||
|
- [Checkout multiple repos (nested)](#checkout-multiple-repos-nested)
|
||||||
|
- [Checkout multiple repos (private)](#checkout-multiple-repos-private)
|
||||||
|
- [Checkout pull request HEAD commit instead of merge commit](#checkout-pull-request-head-commit-instead-of-merge-commit)
|
||||||
|
- [Checkout pull request on closed event](#checkout-pull-request-on-closed-event)
|
||||||
|
- [Push a commit using the built-in token](#push-a-commit-using-the-built-in-token)
|
||||||
|
- [Push a commit to a PR using the built-in token](#push-a-commit-to-a-pr-using-the-built-in-token)
|
||||||
|
- [Recommended permissions](#recommended-permissions)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
## Fetch only the root files
|
## Fetch only the root files
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
sparse-checkout: .
|
sparse-checkout: .
|
||||||
```
|
```
|
||||||
@ -183,7 +216,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
## Fetch only the root files and `.github` and `src` folder
|
## Fetch only the root files and `.github` and `src` folder
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
.github
|
.github
|
||||||
@ -193,7 +226,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
## Fetch only a single file
|
## Fetch only a single file
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
README.md
|
README.md
|
||||||
@ -203,7 +236,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
## Fetch all history for all tags and branches
|
## Fetch all history for all tags and branches
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
```
|
```
|
||||||
@ -211,7 +244,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
## Checkout a different branch
|
## Checkout a different branch
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: my-branch
|
ref: my-branch
|
||||||
```
|
```
|
||||||
@ -219,7 +252,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
## Checkout HEAD^
|
## Checkout HEAD^
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- run: git checkout HEAD^
|
- run: git checkout HEAD^
|
||||||
@ -229,12 +262,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
path: main
|
path: main
|
||||||
|
|
||||||
- name: Checkout tools repo
|
- name: Checkout tools repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
repository: my-org/my-tools
|
repository: my-org/my-tools
|
||||||
path: my-tools
|
path: my-tools
|
||||||
@ -245,10 +278,10 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Checkout tools repo
|
- name: Checkout tools repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
repository: my-org/my-tools
|
repository: my-org/my-tools
|
||||||
path: my-tools
|
path: my-tools
|
||||||
@ -259,12 +292,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
path: main
|
path: main
|
||||||
|
|
||||||
- name: Checkout private tools
|
- name: Checkout private tools
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
repository: my-org/my-private-tools
|
repository: my-org/my-private-tools
|
||||||
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
|
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
|
||||||
@ -277,7 +310,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
## Checkout pull request HEAD commit instead of merge commit
|
## Checkout pull request HEAD commit instead of merge commit
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
```
|
```
|
||||||
@ -293,7 +326,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
```
|
```
|
||||||
|
|
||||||
## Push a commit using the built-in token
|
## Push a commit using the built-in token
|
||||||
@ -304,7 +337,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- run: |
|
- run: |
|
||||||
date > generated.txt
|
date > generated.txt
|
||||||
# Note: the following account information will not work on GHES
|
# Note: the following account information will not work on GHES
|
||||||
@ -326,7 +359,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.head_ref }}
|
ref: ${{ github.head_ref }}
|
||||||
- run: |
|
- run: |
|
||||||
|
|||||||
@ -1,12 +1,46 @@
|
|||||||
import * as core from '@actions/core'
|
import {
|
||||||
|
jest,
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
expect,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
afterEach,
|
||||||
|
afterAll
|
||||||
|
} from '@jest/globals'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as gitAuthHelper from '../lib/git-auth-helper'
|
|
||||||
import * as io from '@actions/io'
|
import * as io from '@actions/io'
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as stateHelper from '../lib/state-helper'
|
import {fileURLToPath} from 'url'
|
||||||
import {IGitCommandManager} from '../lib/git-command-manager'
|
|
||||||
import {IGitSourceSettings} from '../lib/git-source-settings'
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
// Mock @actions/core before loading git-auth-helper
|
||||||
|
jest.unstable_mockModule('@actions/core', () => ({
|
||||||
|
setSecret: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
warning: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
setFailed: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock state-helper
|
||||||
|
jest.unstable_mockModule('../src/state-helper.js', () => ({
|
||||||
|
setSshKeyPath: jest.fn(),
|
||||||
|
setSshKnownHostsPath: jest.fn(),
|
||||||
|
IsPost: false,
|
||||||
|
RepositoryPath: ''
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Dynamic imports after mocking
|
||||||
|
const core = await import('@actions/core')
|
||||||
|
const gitAuthHelper = await import('../src/git-auth-helper.js')
|
||||||
|
type IGitCommandManager =
|
||||||
|
import('../src/git-command-manager.js').IGitCommandManager
|
||||||
|
type IGitSourceSettings =
|
||||||
|
import('../src/git-source-settings.js').IGitSourceSettings
|
||||||
|
|
||||||
const isWindows = process.platform === 'win32'
|
const isWindows = process.platform === 'win32'
|
||||||
const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
|
const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
|
||||||
@ -32,25 +66,12 @@ describe('git-auth-helper tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Mock setSecret
|
jest.clearAllMocks()
|
||||||
jest.spyOn(core, 'setSecret').mockImplementation((secret: string) => {})
|
|
||||||
|
|
||||||
// Mock error/warning/info/debug
|
|
||||||
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
|
||||||
|
|
||||||
// Mock state helper
|
|
||||||
jest.spyOn(stateHelper, 'setSshKeyPath').mockImplementation(jest.fn())
|
|
||||||
jest
|
|
||||||
.spyOn(stateHelper, 'setSshKnownHostsPath')
|
|
||||||
.mockImplementation(jest.fn())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// Unregister mocks
|
// Unregister mocks
|
||||||
jest.restoreAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
// Restore HOME
|
// Restore HOME
|
||||||
if (originalHome) {
|
if (originalHome) {
|
||||||
@ -86,16 +107,29 @@ describe('git-auth-helper tests', () => {
|
|||||||
// Act
|
// Act
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
// Assert config
|
// Assert config - check that .git/config contains includeIf entries
|
||||||
const configContent = (
|
const localConfigContent = (
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
).toString()
|
).toString()
|
||||||
|
expect(
|
||||||
|
localConfigContent.indexOf('includeIf.gitdir:')
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
|
// Assert credentials config file contains the actual credentials
|
||||||
|
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBe(1)
|
||||||
|
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
const credentialsContent = (
|
||||||
|
await fs.promises.readFile(credentialsConfigPath)
|
||||||
|
).toString()
|
||||||
const basicCredential = Buffer.from(
|
const basicCredential = Buffer.from(
|
||||||
`x-access-token:${settings.authToken}`,
|
`x-access-token:${settings.authToken}`,
|
||||||
'utf8'
|
'utf8'
|
||||||
).toString('base64')
|
).toString('base64')
|
||||||
expect(
|
expect(
|
||||||
configContent.indexOf(
|
credentialsContent.indexOf(
|
||||||
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||||
)
|
)
|
||||||
).toBeGreaterThanOrEqual(0)
|
).toBeGreaterThanOrEqual(0)
|
||||||
@ -120,7 +154,7 @@ describe('git-auth-helper tests', () => {
|
|||||||
'inject https://github.com as github server url'
|
'inject https://github.com as github server url'
|
||||||
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
|
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
|
||||||
await testAuthHeader(
|
await testAuthHeader(
|
||||||
configureAuth_AcceptsGitHubServerUrl,
|
configureAuth_AcceptsGitHubServerUrlSetToGHEC,
|
||||||
'https://github.com'
|
'https://github.com'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -141,12 +175,17 @@ describe('git-auth-helper tests', () => {
|
|||||||
// Act
|
// Act
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
// Assert config
|
// Assert config - check credentials config file (not local .git/config)
|
||||||
const configContent = (
|
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBe(1)
|
||||||
|
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
const credentialsContent = (
|
||||||
|
await fs.promises.readFile(credentialsConfigPath)
|
||||||
).toString()
|
).toString()
|
||||||
expect(
|
expect(
|
||||||
configContent.indexOf(
|
credentialsContent.indexOf(
|
||||||
`http.https://github.com/.extraheader AUTHORIZATION`
|
`http.https://github.com/.extraheader AUTHORIZATION`
|
||||||
)
|
)
|
||||||
).toBeGreaterThanOrEqual(0)
|
).toBeGreaterThanOrEqual(0)
|
||||||
@ -211,7 +250,7 @@ describe('git-auth-helper tests', () => {
|
|||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
// Assert secret
|
// Assert secret
|
||||||
const setSecretSpy = core.setSecret as jest.Mock<any, any>
|
const setSecretSpy = core.setSecret as jest.Mock<any>
|
||||||
expect(setSecretSpy).toHaveBeenCalledTimes(1)
|
expect(setSecretSpy).toHaveBeenCalledTimes(1)
|
||||||
const expectedSecret = Buffer.from(
|
const expectedSecret = Buffer.from(
|
||||||
`x-access-token:${settings.authToken}`,
|
`x-access-token:${settings.authToken}`,
|
||||||
@ -251,13 +290,16 @@ describe('git-auth-helper tests', () => {
|
|||||||
expectedSshCommand
|
expectedSshCommand
|
||||||
)
|
)
|
||||||
|
|
||||||
// Asserty git config
|
// Assert git config
|
||||||
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
|
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
|
||||||
.toString()
|
.toString()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter(x => x)
|
.filter(x => x)
|
||||||
expect(gitConfigLines).toHaveLength(1)
|
// Should have includeIf entries pointing to credentials file
|
||||||
expect(gitConfigLines[0]).toMatch(/^http\./)
|
expect(gitConfigLines.length).toBeGreaterThan(0)
|
||||||
|
expect(
|
||||||
|
gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0)
|
||||||
|
).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
|
const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
|
||||||
@ -419,8 +461,20 @@ describe('git-auth-helper tests', () => {
|
|||||||
expect(
|
expect(
|
||||||
configContent.indexOf('value-from-global-config')
|
configContent.indexOf('value-from-global-config')
|
||||||
).toBeGreaterThanOrEqual(0)
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
// Global config should have include.path pointing to credentials file
|
||||||
|
expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
|
// Check credentials in the separate config file
|
||||||
|
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBeGreaterThan(0)
|
||||||
|
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
const credentialsContent = (
|
||||||
|
await fs.promises.readFile(credentialsConfigPath)
|
||||||
|
).toString()
|
||||||
expect(
|
expect(
|
||||||
configContent.indexOf(
|
credentialsContent.indexOf(
|
||||||
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||||
)
|
)
|
||||||
).toBeGreaterThanOrEqual(0)
|
).toBeGreaterThanOrEqual(0)
|
||||||
@ -463,8 +517,20 @@ describe('git-auth-helper tests', () => {
|
|||||||
const configContent = (
|
const configContent = (
|
||||||
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
|
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
|
||||||
).toString()
|
).toString()
|
||||||
|
// Global config should have include.path pointing to credentials file
|
||||||
|
expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
|
// Check credentials in the separate config file
|
||||||
|
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBeGreaterThan(0)
|
||||||
|
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
const credentialsContent = (
|
||||||
|
await fs.promises.readFile(credentialsConfigPath)
|
||||||
|
).toString()
|
||||||
expect(
|
expect(
|
||||||
configContent.indexOf(
|
credentialsContent.indexOf(
|
||||||
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||||
)
|
)
|
||||||
).toBeGreaterThanOrEqual(0)
|
).toBeGreaterThanOrEqual(0)
|
||||||
@ -484,7 +550,7 @@ describe('git-auth-helper tests', () => {
|
|||||||
settings.sshKey = ''
|
settings.sshKey = ''
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any>
|
||||||
mockSubmoduleForeach.mockClear() // reset calls
|
mockSubmoduleForeach.mockClear() // reset calls
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@ -517,7 +583,7 @@ describe('git-auth-helper tests', () => {
|
|||||||
settings.persistCredentials = false
|
settings.persistCredentials = false
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any>
|
||||||
mockSubmoduleForeach.mockClear() // reset calls
|
mockSubmoduleForeach.mockClear() // reset calls
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@ -543,22 +609,22 @@ describe('git-auth-helper tests', () => {
|
|||||||
settings.sshKey = ''
|
settings.sshKey = ''
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any>
|
||||||
mockSubmoduleForeach.mockClear() // reset calls
|
mockSubmoduleForeach.mockClear() // reset calls
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await authHelper.configureSubmoduleAuth()
|
await authHelper.configureSubmoduleAuth()
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
|
// Should configure insteadOf (2 calls for two values)
|
||||||
|
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
|
||||||
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
||||||
/unset-all.*insteadOf/
|
/unset-all.*insteadOf/
|
||||||
)
|
)
|
||||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
|
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(
|
||||||
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
|
|
||||||
/url.*insteadOf.*git@github.com:/
|
/url.*insteadOf.*git@github.com:/
|
||||||
)
|
)
|
||||||
expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
|
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
|
||||||
/url.*insteadOf.*org-123456@github.com:/
|
/url.*insteadOf.*org-123456@github.com:/
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -582,19 +648,19 @@ describe('git-auth-helper tests', () => {
|
|||||||
)
|
)
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any>
|
||||||
mockSubmoduleForeach.mockClear() // reset calls
|
mockSubmoduleForeach.mockClear() // reset calls
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await authHelper.configureSubmoduleAuth()
|
await authHelper.configureSubmoduleAuth()
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
|
// Should configure sshCommand (1 call)
|
||||||
|
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
|
||||||
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
||||||
/unset-all.*insteadOf/
|
/unset-all.*insteadOf/
|
||||||
)
|
)
|
||||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
|
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/)
|
||||||
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -660,112 +726,81 @@ describe('git-auth-helper tests', () => {
|
|||||||
await setup(removeAuth_removesToken)
|
await setup(removeAuth_removesToken)
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
let gitConfigContent = (
|
|
||||||
|
// Verify includeIf entries exist in local config
|
||||||
|
let localConfigContent = (
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
).toString()
|
).toString()
|
||||||
expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
|
expect(
|
||||||
|
localConfigContent.indexOf('includeIf.gitdir:')
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
// Act
|
// Verify both host and container includeIf entries are present
|
||||||
await authHelper.removeAuth()
|
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)
|
||||||
|
|
||||||
// Assert git config
|
// Verify credentials file exists
|
||||||
gitConfigContent = (
|
let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
|
)
|
||||||
|
expect(credentialsFiles.length).toBe(1)
|
||||||
|
const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
|
||||||
|
// Verify credentials file contains the auth token
|
||||||
|
let credentialsContent = (
|
||||||
|
await fs.promises.readFile(credentialsFilePath)
|
||||||
).toString()
|
).toString()
|
||||||
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
const removeAuth_removesV6StyleCredentials =
|
|
||||||
'removeAuth removes v6 style credentials'
|
|
||||||
it(removeAuth_removesV6StyleCredentials, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(removeAuth_removesV6StyleCredentials)
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
|
|
||||||
// Manually create v6-style credentials that would be left by v6
|
|
||||||
const credentialsFileName =
|
|
||||||
'git-credentials-12345678-1234-1234-1234-123456789abc.config'
|
|
||||||
const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
|
|
||||||
const basicCredential = Buffer.from(
|
const basicCredential = Buffer.from(
|
||||||
`x-access-token:${settings.authToken}`,
|
`x-access-token:${settings.authToken}`,
|
||||||
'utf8'
|
'utf8'
|
||||||
).toString('base64')
|
).toString('base64')
|
||||||
const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${basicCredential}\n`
|
|
||||||
await fs.promises.writeFile(credentialsFilePath, credentialsContent)
|
|
||||||
|
|
||||||
// Add includeIf entries to local git config (simulating v6 configuration)
|
|
||||||
const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
|
|
||||||
await fs.promises.appendFile(
|
|
||||||
localGitConfigPath,
|
|
||||||
`[includeIf "gitdir:${hostGitDir}/"]\n\tpath = ${credentialsFilePath}\n`
|
|
||||||
)
|
|
||||||
await fs.promises.appendFile(
|
|
||||||
localGitConfigPath,
|
|
||||||
`[includeIf "gitdir:/github/workspace/.git/"]\n\tpath = /github/runner_temp/${credentialsFileName}\n`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify v6 style config exists
|
|
||||||
let gitConfigContent = (
|
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
|
||||||
).toString()
|
|
||||||
expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
|
|
||||||
expect(
|
expect(
|
||||||
gitConfigContent.indexOf(credentialsFilePath)
|
credentialsContent.indexOf(
|
||||||
|
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||||
|
)
|
||||||
).toBeGreaterThanOrEqual(0)
|
).toBeGreaterThanOrEqual(0)
|
||||||
await fs.promises.stat(credentialsFilePath) // Verify file exists
|
|
||||||
|
|
||||||
// Mock the git methods to handle v6 cleanup
|
// Verify the includeIf entries point to the credentials file
|
||||||
const mockTryGetConfigKeys = git.tryGetConfigKeys as jest.Mock<any, any>
|
const containerCredentialsPath = path.posix.join(
|
||||||
mockTryGetConfigKeys.mockResolvedValue([
|
'/github/runner_temp',
|
||||||
`includeIf.gitdir:${hostGitDir}/.path`,
|
path.basename(credentialsFilePath)
|
||||||
'includeIf.gitdir:/github/workspace/.git/.path'
|
|
||||||
])
|
|
||||||
|
|
||||||
const mockTryGetConfigValues = git.tryGetConfigValues as jest.Mock<any, any>
|
|
||||||
mockTryGetConfigValues.mockImplementation(async (key: string) => {
|
|
||||||
if (key === `includeIf.gitdir:${hostGitDir}/.path`) {
|
|
||||||
return [credentialsFilePath]
|
|
||||||
}
|
|
||||||
if (key === 'includeIf.gitdir:/github/workspace/.git/.path') {
|
|
||||||
return [`/github/runner_temp/${credentialsFileName}`]
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
const mockTryConfigUnsetValue = git.tryConfigUnsetValue as jest.Mock<
|
|
||||||
any,
|
|
||||||
any
|
|
||||||
>
|
|
||||||
mockTryConfigUnsetValue.mockImplementation(
|
|
||||||
async (
|
|
||||||
key: string,
|
|
||||||
value: string,
|
|
||||||
globalConfig?: boolean,
|
|
||||||
configPath?: string
|
|
||||||
) => {
|
|
||||||
const targetPath = configPath || localGitConfigPath
|
|
||||||
let content = await fs.promises.readFile(targetPath, 'utf8')
|
|
||||||
// Remove the includeIf section
|
|
||||||
const lines = content
|
|
||||||
.split('\n')
|
|
||||||
.filter(line => !line.includes('includeIf') && !line.includes(value))
|
|
||||||
await fs.promises.writeFile(targetPath, lines.join('\n'))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
expect(
|
||||||
|
localConfigContent.indexOf(credentialsFilePath)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(
|
||||||
|
localConfigContent.indexOf(containerCredentialsPath)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await authHelper.removeAuth()
|
await authHelper.removeAuth()
|
||||||
|
|
||||||
// Assert includeIf entries removed from local git config
|
// Assert all includeIf entries removed from local git config
|
||||||
gitConfigContent = (
|
localConfigContent = (
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
).toString()
|
).toString()
|
||||||
expect(gitConfigContent.indexOf('includeIf')).toBeLessThan(0)
|
expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0)
|
||||||
expect(gitConfigContent.indexOf(credentialsFilePath)).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
|
// 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 {
|
try {
|
||||||
await fs.promises.stat(credentialsFilePath)
|
await fs.promises.stat(credentialsFilePath)
|
||||||
throw new Error('Credentials file should have been deleted')
|
throw new Error('Credentials file should have been deleted')
|
||||||
@ -776,113 +811,108 @@ describe('git-auth-helper tests', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const removeAuth_removesV6StyleCredentialsFromSubmodules =
|
const removeAuth_removesTokenFromSubmodules =
|
||||||
'removeAuth removes v6 style credentials from submodules'
|
'removeAuth removes token from submodules'
|
||||||
it(removeAuth_removesV6StyleCredentialsFromSubmodules, async () => {
|
it(removeAuth_removesTokenFromSubmodules, async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
await setup(removeAuth_removesV6StyleCredentialsFromSubmodules)
|
await setup(removeAuth_removesTokenFromSubmodules)
|
||||||
|
|
||||||
// Create fake submodule config paths
|
// Create fake submodule config paths
|
||||||
const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
|
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 submodule1ConfigPath = path.join(submodule1Dir, 'config')
|
||||||
|
const submodule2ConfigPath = path.join(submodule2Dir, 'config')
|
||||||
|
|
||||||
await fs.promises.mkdir(submodule1Dir, {recursive: true})
|
await fs.promises.mkdir(submodule1Dir, {recursive: true})
|
||||||
|
await fs.promises.mkdir(submodule2Dir, {recursive: true})
|
||||||
await fs.promises.writeFile(submodule1ConfigPath, '')
|
await fs.promises.writeFile(submodule1ConfigPath, '')
|
||||||
|
await fs.promises.writeFile(submodule2ConfigPath, '')
|
||||||
|
|
||||||
|
// Mock getSubmoduleConfigPaths to return our fake submodules (for both configure and remove)
|
||||||
|
const mockGetSubmoduleConfigPaths =
|
||||||
|
git.getSubmoduleConfigPaths as jest.Mock<any>
|
||||||
|
mockGetSubmoduleConfigPaths.mockResolvedValue([
|
||||||
|
submodule1ConfigPath,
|
||||||
|
submodule2ConfigPath
|
||||||
|
])
|
||||||
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
await authHelper.configureAuth()
|
await authHelper.configureAuth()
|
||||||
|
await authHelper.configureSubmoduleAuth()
|
||||||
|
|
||||||
// Create v6-style credentials file
|
// Verify credentials file exists
|
||||||
const credentialsFileName =
|
let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||||
'git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
|
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||||
const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
|
|
||||||
const basicCredential = Buffer.from(
|
|
||||||
`x-access-token:${settings.authToken}`,
|
|
||||||
'utf8'
|
|
||||||
).toString('base64')
|
|
||||||
const credentialsContent = `[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic ${basicCredential}\n`
|
|
||||||
await fs.promises.writeFile(credentialsFilePath, credentialsContent)
|
|
||||||
|
|
||||||
// Add includeIf entries to submodule config
|
|
||||||
const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
|
|
||||||
await fs.promises.appendFile(
|
|
||||||
submodule1ConfigPath,
|
|
||||||
`[includeIf "gitdir:${submodule1GitDir}/"]\n\tpath = ${credentialsFilePath}\n`
|
|
||||||
)
|
)
|
||||||
|
expect(credentialsFiles.length).toBe(1)
|
||||||
|
const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
|
||||||
|
|
||||||
// Verify submodule config has includeIf entry
|
// Verify submodule 1 config has includeIf entries
|
||||||
let submoduleConfigContent = (
|
let submodule1Content = (
|
||||||
await fs.promises.readFile(submodule1ConfigPath)
|
await fs.promises.readFile(submodule1ConfigPath)
|
||||||
).toString()
|
).toString()
|
||||||
expect(submoduleConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(
|
const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
|
||||||
0
|
|
||||||
)
|
|
||||||
expect(
|
expect(
|
||||||
submoduleConfigContent.indexOf(credentialsFilePath)
|
submodule1Content.indexOf(`includeIf.gitdir:${submodule1GitDir}.path`)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(
|
||||||
|
submodule1Content.indexOf(credentialsFilePath)
|
||||||
).toBeGreaterThanOrEqual(0)
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
// Mock getSubmoduleConfigPaths
|
// Verify submodule 2 config has includeIf entries
|
||||||
const mockGetSubmoduleConfigPaths =
|
let submodule2Content = (
|
||||||
git.getSubmoduleConfigPaths as jest.Mock<any, any>
|
await fs.promises.readFile(submodule2ConfigPath)
|
||||||
mockGetSubmoduleConfigPaths.mockResolvedValue([submodule1ConfigPath])
|
).toString()
|
||||||
|
const submodule2GitDir = submodule2Dir.replace(/\\/g, '/')
|
||||||
|
expect(
|
||||||
|
submodule2Content.indexOf(`includeIf.gitdir:${submodule2GitDir}.path`)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(
|
||||||
|
submodule2Content.indexOf(credentialsFilePath)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
// Mock tryGetConfigKeys for submodule
|
// Verify both host and container paths are in each submodule config
|
||||||
const mockTryGetConfigKeys = git.tryGetConfigKeys as jest.Mock<any, any>
|
const containerCredentialsPath = path.posix.join(
|
||||||
mockTryGetConfigKeys.mockImplementation(
|
'/github/runner_temp',
|
||||||
async (pattern: string, globalConfig?: boolean, configPath?: string) => {
|
path.basename(credentialsFilePath)
|
||||||
if (configPath === submodule1ConfigPath) {
|
|
||||||
return [`includeIf.gitdir:${submodule1GitDir}/.path`]
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
expect(
|
||||||
|
submodule1Content.indexOf(containerCredentialsPath)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(
|
||||||
|
submodule2Content.indexOf(containerCredentialsPath)
|
||||||
|
).toBeGreaterThanOrEqual(0)
|
||||||
|
|
||||||
// Mock tryGetConfigValues for submodule
|
// Act - ensure mock persists for removeAuth
|
||||||
const mockTryGetConfigValues = git.tryGetConfigValues as jest.Mock<any, any>
|
mockGetSubmoduleConfigPaths.mockResolvedValue([
|
||||||
mockTryGetConfigValues.mockImplementation(
|
submodule1ConfigPath,
|
||||||
async (key: string, globalConfig?: boolean, configPath?: string) => {
|
submodule2ConfigPath
|
||||||
if (
|
])
|
||||||
configPath === submodule1ConfigPath &&
|
|
||||||
key === `includeIf.gitdir:${submodule1GitDir}/.path`
|
|
||||||
) {
|
|
||||||
return [credentialsFilePath]
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Mock tryConfigUnsetValue for submodule
|
|
||||||
const mockTryConfigUnsetValue = git.tryConfigUnsetValue as jest.Mock<
|
|
||||||
any,
|
|
||||||
any
|
|
||||||
>
|
|
||||||
mockTryConfigUnsetValue.mockImplementation(
|
|
||||||
async (
|
|
||||||
key: string,
|
|
||||||
value: string,
|
|
||||||
globalConfig?: boolean,
|
|
||||||
configPath?: string
|
|
||||||
) => {
|
|
||||||
const targetPath = configPath || localGitConfigPath
|
|
||||||
let content = await fs.promises.readFile(targetPath, 'utf8')
|
|
||||||
const lines = content
|
|
||||||
.split('\n')
|
|
||||||
.filter(line => !line.includes('includeIf') && !line.includes(value))
|
|
||||||
await fs.promises.writeFile(targetPath, lines.join('\n'))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.removeAuth()
|
await authHelper.removeAuth()
|
||||||
|
|
||||||
// Assert submodule includeIf entries removed
|
// Assert submodule 1 includeIf entries removed
|
||||||
submoduleConfigContent = (
|
submodule1Content = (
|
||||||
await fs.promises.readFile(submodule1ConfigPath)
|
await fs.promises.readFile(submodule1ConfigPath)
|
||||||
).toString()
|
).toString()
|
||||||
expect(submoduleConfigContent.indexOf('includeIf')).toBeLessThan(0)
|
expect(submodule1Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
|
||||||
expect(submoduleConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
|
expect(submodule1Content.indexOf(credentialsFilePath)).toBeLessThan(0)
|
||||||
|
expect(submodule1Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
|
||||||
|
|
||||||
// Assert credentials file deleted
|
// 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 {
|
try {
|
||||||
await fs.promises.stat(credentialsFilePath)
|
await fs.promises.stat(credentialsFilePath)
|
||||||
throw new Error('Credentials file should have been deleted')
|
throw new Error('Credentials file should have been deleted')
|
||||||
@ -893,65 +923,6 @@ describe('git-auth-helper tests', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const removeAuth_skipsV6CleanupWhenEnvVarSet =
|
|
||||||
'removeAuth skips v6 cleanup when ACTIONS_CHECKOUT_SKIP_V6_CLEANUP is set'
|
|
||||||
it(removeAuth_skipsV6CleanupWhenEnvVarSet, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(removeAuth_skipsV6CleanupWhenEnvVarSet)
|
|
||||||
|
|
||||||
// Set the skip environment variable
|
|
||||||
process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'] = '1'
|
|
||||||
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
|
|
||||||
// Create v6-style credentials file in RUNNER_TEMP
|
|
||||||
const credentialsFileName = 'git-credentials-test-uuid-1234-5678.config'
|
|
||||||
const credentialsFilePath = path.join(runnerTemp, credentialsFileName)
|
|
||||||
const credentialsContent =
|
|
||||||
'[http "https://github.com/"]\n\textraheader = AUTHORIZATION: basic token\n'
|
|
||||||
await fs.promises.writeFile(credentialsFilePath, credentialsContent)
|
|
||||||
|
|
||||||
// Add includeIf section to local git config (separate from http.* config)
|
|
||||||
const includeIfSection = `\n[includeIf "gitdir:/some/path/.git/"]\n\tpath = ${credentialsFilePath}\n`
|
|
||||||
await fs.promises.appendFile(localGitConfigPath, includeIfSection)
|
|
||||||
|
|
||||||
// Verify v6 style config exists
|
|
||||||
let gitConfigContent = (
|
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
|
||||||
).toString()
|
|
||||||
expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
|
|
||||||
await fs.promises.stat(credentialsFilePath) // Verify file exists
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.removeAuth()
|
|
||||||
|
|
||||||
// Assert v5 cleanup still happened (http.* removed)
|
|
||||||
gitConfigContent = (
|
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
|
||||||
).toString()
|
|
||||||
expect(
|
|
||||||
gitConfigContent.indexOf('http.https://github.com/.extraheader')
|
|
||||||
).toBeLessThan(0)
|
|
||||||
|
|
||||||
// Assert v6 cleanup was skipped - includeIf should still be present
|
|
||||||
expect(gitConfigContent.indexOf('includeIf')).toBeGreaterThanOrEqual(0)
|
|
||||||
expect(
|
|
||||||
gitConfigContent.indexOf(credentialsFilePath)
|
|
||||||
).toBeGreaterThanOrEqual(0)
|
|
||||||
|
|
||||||
// Assert credentials file still exists (wasn't deleted)
|
|
||||||
await fs.promises.stat(credentialsFilePath) // File should still exist
|
|
||||||
|
|
||||||
// Assert debug message was logged
|
|
||||||
expect(core.debug).toHaveBeenCalledWith(
|
|
||||||
'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
delete process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP']
|
|
||||||
})
|
|
||||||
|
|
||||||
const removeGlobalConfig_removesOverride =
|
const removeGlobalConfig_removesOverride =
|
||||||
'removeGlobalConfig removes override'
|
'removeGlobalConfig removes override'
|
||||||
it(removeGlobalConfig_removesOverride, async () => {
|
it(removeGlobalConfig_removesOverride, async () => {
|
||||||
@ -978,6 +949,52 @@ describe('git-auth-helper tests', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const testCredentialsConfigPath_matchesCredentialsConfigPaths =
|
||||||
|
'testCredentialsConfigPath matches credentials config paths'
|
||||||
|
it(testCredentialsConfigPath_matchesCredentialsConfigPaths, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(testCredentialsConfigPath_matchesCredentialsConfigPaths)
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
|
||||||
|
// Get a real credentials config path
|
||||||
|
const credentialsConfigPath = await (
|
||||||
|
authHelper as any
|
||||||
|
).getCredentialsConfigPath()
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(
|
||||||
|
(authHelper as any).testCredentialsConfigPath(credentialsConfigPath)
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
(authHelper as any).testCredentialsConfigPath(
|
||||||
|
'/some/path/git-credentials-12345678-abcd-1234-5678-123456789012.config'
|
||||||
|
)
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
(authHelper as any).testCredentialsConfigPath(
|
||||||
|
'/some/path/git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
|
||||||
|
)
|
||||||
|
).toBe(true)
|
||||||
|
|
||||||
|
// Test invalid paths
|
||||||
|
expect(
|
||||||
|
(authHelper as any).testCredentialsConfigPath(
|
||||||
|
'/some/path/other-config.config'
|
||||||
|
)
|
||||||
|
).toBe(false)
|
||||||
|
expect(
|
||||||
|
(authHelper as any).testCredentialsConfigPath(
|
||||||
|
'/some/path/git-credentials-invalid.config'
|
||||||
|
)
|
||||||
|
).toBe(false)
|
||||||
|
expect(
|
||||||
|
(authHelper as any).testCredentialsConfigPath(
|
||||||
|
'/some/path/git-credentials-.config'
|
||||||
|
)
|
||||||
|
).toBe(false)
|
||||||
|
expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
async function setup(testName: string): Promise<void> {
|
async function setup(testName: string): Promise<void> {
|
||||||
@ -992,6 +1009,7 @@ async function setup(testName: string): Promise<void> {
|
|||||||
await fs.promises.mkdir(tempHomedir, {recursive: true})
|
await fs.promises.mkdir(tempHomedir, {recursive: true})
|
||||||
process.env['RUNNER_TEMP'] = runnerTemp
|
process.env['RUNNER_TEMP'] = runnerTemp
|
||||||
process.env['HOME'] = tempHomedir
|
process.env['HOME'] = tempHomedir
|
||||||
|
process.env['GITHUB_WORKSPACE'] = workspace
|
||||||
|
|
||||||
// Create git config
|
// Create git config
|
||||||
globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
|
globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
|
||||||
@ -1010,10 +1028,20 @@ async function setup(testName: string): Promise<void> {
|
|||||||
checkout: jest.fn(),
|
checkout: jest.fn(),
|
||||||
checkoutDetach: jest.fn(),
|
checkoutDetach: jest.fn(),
|
||||||
config: jest.fn(
|
config: jest.fn(
|
||||||
async (key: string, value: string, globalConfig?: boolean) => {
|
async (
|
||||||
const configPath = globalConfig
|
key: string,
|
||||||
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
value: string,
|
||||||
: localGitConfigPath
|
globalConfig?: boolean,
|
||||||
|
add?: boolean,
|
||||||
|
configFile?: string
|
||||||
|
) => {
|
||||||
|
const configPath =
|
||||||
|
configFile ||
|
||||||
|
(globalConfig
|
||||||
|
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||||
|
: localGitConfigPath)
|
||||||
|
// Ensure directory exists
|
||||||
|
await fs.promises.mkdir(path.dirname(configPath), {recursive: true})
|
||||||
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
|
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@ -1033,6 +1061,7 @@ async function setup(testName: string): Promise<void> {
|
|||||||
env: {},
|
env: {},
|
||||||
fetch: jest.fn(),
|
fetch: jest.fn(),
|
||||||
getDefaultBranch: jest.fn(),
|
getDefaultBranch: jest.fn(),
|
||||||
|
getSubmoduleConfigPaths: jest.fn(async () => []),
|
||||||
getWorkingDirectory: jest.fn(() => workspace),
|
getWorkingDirectory: jest.fn(() => workspace),
|
||||||
init: jest.fn(),
|
init: jest.fn(),
|
||||||
isDetached: jest.fn(),
|
isDetached: jest.fn(),
|
||||||
@ -1071,23 +1100,75 @@ async function setup(testName: string): Promise<void> {
|
|||||||
return true
|
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(),
|
tryDisableAutomaticGarbageCollection: jest.fn(),
|
||||||
tryGetFetchUrl: jest.fn(),
|
tryGetFetchUrl: jest.fn(),
|
||||||
getSubmoduleConfigPaths: jest.fn(async () => {
|
tryGetConfigValues: jest.fn(
|
||||||
return []
|
async (
|
||||||
}),
|
key: string,
|
||||||
tryConfigUnsetValue: jest.fn(async () => {
|
globalConfig?: boolean,
|
||||||
return true
|
configPath?: string
|
||||||
}),
|
): Promise<string[]> => {
|
||||||
tryGetConfigValues: jest.fn(async () => {
|
const targetConfigPath =
|
||||||
return []
|
configPath ||
|
||||||
}),
|
(globalConfig
|
||||||
tryGetConfigKeys: jest.fn(async () => {
|
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||||
return []
|
: 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(),
|
tryReset: jest.fn(),
|
||||||
version: jest.fn()
|
version: jest.fn()
|
||||||
}
|
} as unknown as IGitCommandManager & {env: {[key: string]: string}}
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
authToken: 'some auth token',
|
authToken: 'some auth token',
|
||||||
@ -1120,6 +1201,7 @@ async function setup(testName: string): Promise<void> {
|
|||||||
|
|
||||||
async function getActualSshKeyPath(): Promise<string> {
|
async function getActualSshKeyPath(): Promise<string> {
|
||||||
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
||||||
|
.filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
|
||||||
.sort()
|
.sort()
|
||||||
.map(x => path.join(runnerTemp, x))
|
.map(x => path.join(runnerTemp, x))
|
||||||
if (actualTempFiles.length === 0) {
|
if (actualTempFiles.length === 0) {
|
||||||
@ -1133,6 +1215,7 @@ async function getActualSshKeyPath(): Promise<string> {
|
|||||||
|
|
||||||
async function getActualSshKnownHostsPath(): Promise<string> {
|
async function getActualSshKnownHostsPath(): Promise<string> {
|
||||||
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
||||||
|
.filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
|
||||||
.sort()
|
.sort()
|
||||||
.map(x => path.join(runnerTemp, x))
|
.map(x => path.join(runnerTemp, x))
|
||||||
if (actualTempFiles.length === 0) {
|
if (actualTempFiles.length === 0) {
|
||||||
|
|||||||
@ -1,26 +1,51 @@
|
|||||||
import * as exec from '@actions/exec'
|
import {
|
||||||
import * as fshelper from '../lib/fs-helper'
|
jest,
|
||||||
import * as commandManager from '../lib/git-command-manager'
|
describe,
|
||||||
|
it,
|
||||||
|
expect,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
afterEach,
|
||||||
|
afterAll
|
||||||
|
} from '@jest/globals'
|
||||||
|
|
||||||
let git: commandManager.IGitCommandManager
|
// Mock @actions/exec
|
||||||
let mockExec = jest.fn()
|
const mockExec = jest.fn()
|
||||||
|
jest.unstable_mockModule('@actions/exec', () => ({
|
||||||
|
exec: mockExec
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock fs-helper
|
||||||
|
const mockFileExistsSync = jest.fn()
|
||||||
|
const mockDirectoryExistsSync = jest.fn()
|
||||||
|
jest.unstable_mockModule('../src/fs-helper.js', () => ({
|
||||||
|
fileExistsSync: mockFileExistsSync,
|
||||||
|
directoryExistsSync: mockDirectoryExistsSync
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Dynamic imports after mocking
|
||||||
|
const commandManager = await import('../src/git-command-manager.js')
|
||||||
|
type IGitCommandManager =
|
||||||
|
import('../src/git-command-manager.js').IGitCommandManager
|
||||||
|
|
||||||
|
let git: IGitCommandManager
|
||||||
|
|
||||||
describe('git-auth-helper tests', () => {
|
describe('git-auth-helper tests', () => {
|
||||||
beforeAll(async () => {})
|
beforeAll(async () => {})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
mockFileExistsSync.mockReset()
|
||||||
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
|
mockDirectoryExistsSync.mockReset()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {})
|
afterAll(() => {})
|
||||||
|
|
||||||
it('branch list matches', async () => {
|
it('branch list matches', async () => {
|
||||||
mockExec.mockImplementation((path, args, options) => {
|
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||||
console.log(args, options.listeners.stdout)
|
console.log(args, options.listeners.stdout)
|
||||||
|
|
||||||
if (args.includes('version')) {
|
if (args.includes('version')) {
|
||||||
@ -36,7 +61,7 @@ describe('git-auth-helper tests', () => {
|
|||||||
|
|
||||||
return 1
|
return 1
|
||||||
})
|
})
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
// exec.exec is already mockExec
|
||||||
const workingDirectory = 'test'
|
const workingDirectory = 'test'
|
||||||
const lfs = false
|
const lfs = false
|
||||||
const doSparseCheckout = false
|
const doSparseCheckout = false
|
||||||
@ -53,7 +78,7 @@ describe('git-auth-helper tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('ambiguous ref name output is captured', async () => {
|
it('ambiguous ref name output is captured', async () => {
|
||||||
mockExec.mockImplementation((path, args, options) => {
|
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||||
console.log(args, options.listeners.stdout)
|
console.log(args, options.listeners.stdout)
|
||||||
|
|
||||||
if (args.includes('version')) {
|
if (args.includes('version')) {
|
||||||
@ -72,7 +97,7 @@ describe('git-auth-helper tests', () => {
|
|||||||
|
|
||||||
return 1
|
return 1
|
||||||
})
|
})
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
// exec.exec is already mockExec
|
||||||
const workingDirectory = 'test'
|
const workingDirectory = 'test'
|
||||||
const lfs = false
|
const lfs = false
|
||||||
const doSparseCheckout = false
|
const doSparseCheckout = false
|
||||||
@ -91,9 +116,9 @@ describe('git-auth-helper tests', () => {
|
|||||||
|
|
||||||
describe('Test fetchDepth and fetchTags options', () => {
|
describe('Test fetchDepth and fetchTags options', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
mockFileExistsSync.mockReset()
|
||||||
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
|
mockDirectoryExistsSync.mockReset()
|
||||||
mockExec.mockImplementation((path, args, options) => {
|
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||||
console.log(args, options.listeners.stdout)
|
console.log(args, options.listeners.stdout)
|
||||||
|
|
||||||
if (args.includes('version')) {
|
if (args.includes('version')) {
|
||||||
@ -105,11 +130,11 @@ describe('Test fetchDepth and fetchTags options', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is true', async () => {
|
it('should call execGit with the correct arguments when fetchDepth is 0', async () => {
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
// exec.exec is already mockExec
|
||||||
const workingDirectory = 'test'
|
const workingDirectory = 'test'
|
||||||
const lfs = false
|
const lfs = false
|
||||||
const doSparseCheckout = false
|
const doSparseCheckout = false
|
||||||
@ -122,45 +147,7 @@ describe('Test fetchDepth and fetchTags options', () => {
|
|||||||
const refSpec = ['refspec1', 'refspec2']
|
const refSpec = ['refspec1', 'refspec2']
|
||||||
const options = {
|
const options = {
|
||||||
filter: 'filterValue',
|
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)
|
await git.fetch(refSpec, options)
|
||||||
@ -183,8 +170,46 @@ describe('Test fetchDepth and fetchTags options', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is false', async () => {
|
it('should call execGit with the correct arguments when fetchDepth is 0 and refSpec includes tags', async () => {
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
// exec.exec is already mockExec
|
||||||
|
|
||||||
|
const workingDirectory = 'test'
|
||||||
|
const lfs = false
|
||||||
|
const doSparseCheckout = false
|
||||||
|
git = await commandManager.createCommandManager(
|
||||||
|
workingDirectory,
|
||||||
|
lfs,
|
||||||
|
doSparseCheckout
|
||||||
|
)
|
||||||
|
const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
|
||||||
|
const options = {
|
||||||
|
filter: 'filterValue',
|
||||||
|
fetchDepth: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
await git.fetch(refSpec, options)
|
||||||
|
|
||||||
|
expect(mockExec).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
[
|
||||||
|
'-c',
|
||||||
|
'protocol.version=2',
|
||||||
|
'fetch',
|
||||||
|
'--no-tags',
|
||||||
|
'--prune',
|
||||||
|
'--no-recurse-submodules',
|
||||||
|
'--filter=filterValue',
|
||||||
|
'origin',
|
||||||
|
'refspec1',
|
||||||
|
'refspec2',
|
||||||
|
'+refs/tags/*:refs/tags/*'
|
||||||
|
],
|
||||||
|
expect.any(Object)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call execGit with the correct arguments when fetchDepth is 1', async () => {
|
||||||
|
// exec.exec is already mockExec
|
||||||
|
|
||||||
const workingDirectory = 'test'
|
const workingDirectory = 'test'
|
||||||
const lfs = false
|
const lfs = false
|
||||||
@ -197,8 +222,7 @@ describe('Test fetchDepth and fetchTags options', () => {
|
|||||||
const refSpec = ['refspec1', 'refspec2']
|
const refSpec = ['refspec1', 'refspec2']
|
||||||
const options = {
|
const options = {
|
||||||
filter: 'filterValue',
|
filter: 'filterValue',
|
||||||
fetchDepth: 1,
|
fetchDepth: 1
|
||||||
fetchTags: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await git.fetch(refSpec, options)
|
await git.fetch(refSpec, options)
|
||||||
@ -222,8 +246,8 @@ describe('Test fetchDepth and fetchTags options', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is true', async () => {
|
it('should call execGit with the correct arguments when fetchDepth is 1 and refSpec includes tags', async () => {
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
// exec.exec is already mockExec
|
||||||
|
|
||||||
const workingDirectory = 'test'
|
const workingDirectory = 'test'
|
||||||
const lfs = false
|
const lfs = false
|
||||||
@ -233,11 +257,10 @@ describe('Test fetchDepth and fetchTags options', () => {
|
|||||||
lfs,
|
lfs,
|
||||||
doSparseCheckout
|
doSparseCheckout
|
||||||
)
|
)
|
||||||
const refSpec = ['refspec1', 'refspec2']
|
const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
|
||||||
const options = {
|
const options = {
|
||||||
filter: 'filterValue',
|
filter: 'filterValue',
|
||||||
fetchDepth: 1,
|
fetchDepth: 1
|
||||||
fetchTags: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await git.fetch(refSpec, options)
|
await git.fetch(refSpec, options)
|
||||||
@ -248,20 +271,22 @@ describe('Test fetchDepth and fetchTags options', () => {
|
|||||||
'-c',
|
'-c',
|
||||||
'protocol.version=2',
|
'protocol.version=2',
|
||||||
'fetch',
|
'fetch',
|
||||||
|
'--no-tags',
|
||||||
'--prune',
|
'--prune',
|
||||||
'--no-recurse-submodules',
|
'--no-recurse-submodules',
|
||||||
'--filter=filterValue',
|
'--filter=filterValue',
|
||||||
'--depth=1',
|
'--depth=1',
|
||||||
'origin',
|
'origin',
|
||||||
'refspec1',
|
'refspec1',
|
||||||
'refspec2'
|
'refspec2',
|
||||||
|
'+refs/tags/*:refs/tags/*'
|
||||||
],
|
],
|
||||||
expect.any(Object)
|
expect.any(Object)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when showProgress is true', async () => {
|
it('should call execGit with the correct arguments when showProgress is true', async () => {
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
// exec.exec is already mockExec
|
||||||
|
|
||||||
const workingDirectory = 'test'
|
const workingDirectory = 'test'
|
||||||
const lfs = false
|
const lfs = false
|
||||||
@ -299,7 +324,7 @@ describe('Test fetchDepth and fetchTags options', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when fetchDepth is 42 and showProgress is true', async () => {
|
it('should call execGit with the correct arguments when fetchDepth is 42 and showProgress is true', async () => {
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
// exec.exec is already mockExec
|
||||||
|
|
||||||
const workingDirectory = 'test'
|
const workingDirectory = 'test'
|
||||||
const lfs = false
|
const lfs = false
|
||||||
@ -338,8 +363,8 @@ describe('Test fetchDepth and fetchTags options', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when fetchTags is true and showProgress is true', async () => {
|
it('should call execGit with the correct arguments when showProgress is true and refSpec includes tags', async () => {
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
// exec.exec is already mockExec
|
||||||
|
|
||||||
const workingDirectory = 'test'
|
const workingDirectory = 'test'
|
||||||
const lfs = false
|
const lfs = false
|
||||||
@ -349,10 +374,9 @@ describe('Test fetchDepth and fetchTags options', () => {
|
|||||||
lfs,
|
lfs,
|
||||||
doSparseCheckout
|
doSparseCheckout
|
||||||
)
|
)
|
||||||
const refSpec = ['refspec1', 'refspec2']
|
const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
|
||||||
const options = {
|
const options = {
|
||||||
filter: 'filterValue',
|
filter: 'filterValue',
|
||||||
fetchTags: true,
|
|
||||||
showProgress: true
|
showProgress: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,15 +388,187 @@ describe('Test fetchDepth and fetchTags options', () => {
|
|||||||
'-c',
|
'-c',
|
||||||
'protocol.version=2',
|
'protocol.version=2',
|
||||||
'fetch',
|
'fetch',
|
||||||
|
'--no-tags',
|
||||||
'--prune',
|
'--prune',
|
||||||
'--no-recurse-submodules',
|
'--no-recurse-submodules',
|
||||||
'--progress',
|
'--progress',
|
||||||
'--filter=filterValue',
|
'--filter=filterValue',
|
||||||
'origin',
|
'origin',
|
||||||
'refspec1',
|
'refspec1',
|
||||||
'refspec2'
|
'refspec2',
|
||||||
|
'+refs/tags/*:refs/tags/*'
|
||||||
],
|
],
|
||||||
expect.any(Object)
|
expect.any(Object)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('repository initialization object format', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockFileExistsSync.mockReset()
|
||||||
|
mockDirectoryExistsSync.mockReset()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes SHA-256 repositories with the matching object format', async () => {
|
||||||
|
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||||
|
if (args.includes('version')) {
|
||||||
|
options.listeners.stdout(Buffer.from('git version 2.50.1'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
// exec.exec is already mockExec
|
||||||
|
|
||||||
|
git = await commandManager.createCommandManager('test', false, false)
|
||||||
|
|
||||||
|
await git.init('sha256')
|
||||||
|
|
||||||
|
expect(mockExec).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
['init', '--object-format=sha256', 'test'],
|
||||||
|
expect.any(Object)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes SHA-1 repositories with existing default arguments', async () => {
|
||||||
|
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||||
|
if (args.includes('version')) {
|
||||||
|
options.listeners.stdout(Buffer.from('git version 2.50.1'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
// exec.exec is already mockExec
|
||||||
|
|
||||||
|
git = await commandManager.createCommandManager('test', false, false)
|
||||||
|
|
||||||
|
await git.init('sha1')
|
||||||
|
|
||||||
|
expect(mockExec).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
['init', 'test'],
|
||||||
|
expect.any(Object)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('git user-agent with orchestration ID', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockFileExistsSync.mockReset()
|
||||||
|
mockDirectoryExistsSync.mockReset()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
// Clean up environment variable to prevent test pollution
|
||||||
|
delete process.env['ACTIONS_ORCHESTRATION_ID']
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should include orchestration ID in user-agent when ACTIONS_ORCHESTRATION_ID is set', async () => {
|
||||||
|
const orchId = 'test-orch-id-12345'
|
||||||
|
process.env['ACTIONS_ORCHESTRATION_ID'] = orchId
|
||||||
|
|
||||||
|
let capturedEnv: any = null
|
||||||
|
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||||
|
if (args.includes('version')) {
|
||||||
|
options.listeners.stdout(Buffer.from('2.18'))
|
||||||
|
}
|
||||||
|
// Capture env on any command
|
||||||
|
capturedEnv = options.env
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
// exec.exec is already mockExec
|
||||||
|
|
||||||
|
const workingDirectory = 'test'
|
||||||
|
const lfs = false
|
||||||
|
const doSparseCheckout = false
|
||||||
|
git = await commandManager.createCommandManager(
|
||||||
|
workingDirectory,
|
||||||
|
lfs,
|
||||||
|
doSparseCheckout
|
||||||
|
)
|
||||||
|
|
||||||
|
// Call a git command to trigger env capture after user-agent is set
|
||||||
|
await git.init()
|
||||||
|
|
||||||
|
// Verify the user agent includes the orchestration ID
|
||||||
|
expect(git).toBeDefined()
|
||||||
|
expect(capturedEnv).toBeDefined()
|
||||||
|
expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
|
||||||
|
`git/2.18 (github-actions-checkout) actions_orchestration_id/${orchId}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sanitize invalid characters in orchestration ID', async () => {
|
||||||
|
const orchId = 'test (with) special/chars'
|
||||||
|
process.env['ACTIONS_ORCHESTRATION_ID'] = orchId
|
||||||
|
|
||||||
|
let capturedEnv: any = null
|
||||||
|
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||||
|
if (args.includes('version')) {
|
||||||
|
options.listeners.stdout(Buffer.from('2.18'))
|
||||||
|
}
|
||||||
|
// Capture env on any command
|
||||||
|
capturedEnv = options.env
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
// exec.exec is already mockExec
|
||||||
|
|
||||||
|
const workingDirectory = 'test'
|
||||||
|
const lfs = false
|
||||||
|
const doSparseCheckout = false
|
||||||
|
git = await commandManager.createCommandManager(
|
||||||
|
workingDirectory,
|
||||||
|
lfs,
|
||||||
|
doSparseCheckout
|
||||||
|
)
|
||||||
|
|
||||||
|
// Call a git command to trigger env capture after user-agent is set
|
||||||
|
await git.init()
|
||||||
|
|
||||||
|
// Verify the user agent has sanitized orchestration ID (spaces, parentheses, slash replaced)
|
||||||
|
expect(git).toBeDefined()
|
||||||
|
expect(capturedEnv).toBeDefined()
|
||||||
|
expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
|
||||||
|
'git/2.18 (github-actions-checkout) actions_orchestration_id/test__with__special_chars'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not modify user-agent when ACTIONS_ORCHESTRATION_ID is not set', async () => {
|
||||||
|
delete process.env['ACTIONS_ORCHESTRATION_ID']
|
||||||
|
|
||||||
|
let capturedEnv: any = null
|
||||||
|
mockExec.mockImplementation((path: any, args: any, options: any) => {
|
||||||
|
if (args.includes('version')) {
|
||||||
|
options.listeners.stdout(Buffer.from('2.18'))
|
||||||
|
}
|
||||||
|
// Capture env on any command
|
||||||
|
capturedEnv = options.env
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
// exec.exec is already mockExec
|
||||||
|
|
||||||
|
const workingDirectory = 'test'
|
||||||
|
const lfs = false
|
||||||
|
const doSparseCheckout = false
|
||||||
|
git = await commandManager.createCommandManager(
|
||||||
|
workingDirectory,
|
||||||
|
lfs,
|
||||||
|
doSparseCheckout
|
||||||
|
)
|
||||||
|
|
||||||
|
// Call a git command to trigger env capture after user-agent is set
|
||||||
|
await git.init()
|
||||||
|
|
||||||
|
// Verify the user agent does NOT contain orchestration ID
|
||||||
|
expect(git).toBeDefined()
|
||||||
|
expect(capturedEnv).toBeDefined()
|
||||||
|
expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
|
||||||
|
'git/2.18 (github-actions-checkout)'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@ -1,9 +1,36 @@
|
|||||||
import * as core from '@actions/core'
|
import {
|
||||||
|
jest,
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
expect,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
afterEach
|
||||||
|
} from '@jest/globals'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as gitDirectoryHelper from '../lib/git-directory-helper'
|
|
||||||
import * as io from '@actions/io'
|
import * as io from '@actions/io'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {IGitCommandManager} from '../lib/git-command-manager'
|
import {fileURLToPath} from 'url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
// Mock @actions/core before loading git-directory-helper
|
||||||
|
jest.unstable_mockModule('@actions/core', () => ({
|
||||||
|
error: jest.fn(),
|
||||||
|
warning: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
setFailed: jest.fn(),
|
||||||
|
startGroup: jest.fn(),
|
||||||
|
endGroup: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Dynamic imports after mocking
|
||||||
|
const core = await import('@actions/core')
|
||||||
|
const gitDirectoryHelper = await import('../src/git-directory-helper.js')
|
||||||
|
|
||||||
|
type IGitCommandManager =
|
||||||
|
import('../src/git-command-manager.js').IGitCommandManager
|
||||||
|
|
||||||
const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper')
|
const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper')
|
||||||
let repositoryPath: string
|
let repositoryPath: string
|
||||||
@ -19,16 +46,11 @@ describe('git-directory-helper tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Mock error/warning/info/debug
|
jest.clearAllMocks()
|
||||||
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// Unregister mocks
|
jest.clearAllMocks()
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const cleansWhenCleanTrue = 'cleans when clean true'
|
const cleansWhenCleanTrue = 'cleans when clean true'
|
||||||
@ -81,7 +103,7 @@ describe('git-directory-helper tests', () => {
|
|||||||
// Arrange
|
// Arrange
|
||||||
await setup(doesNotCheckoutDetachWhenNotAlreadyDetached)
|
await setup(doesNotCheckoutDetachWhenNotAlreadyDetached)
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
const mockIsDetached = git.isDetached as jest.Mock<any, any>
|
const mockIsDetached = git.isDetached as jest.Mock<any>
|
||||||
mockIsDetached.mockImplementation(async () => {
|
mockIsDetached.mockImplementation(async () => {
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@ -132,7 +154,7 @@ describe('git-directory-helper tests', () => {
|
|||||||
// Arrange
|
// Arrange
|
||||||
await setup(removesContentsWhenCleanFails)
|
await setup(removesContentsWhenCleanFails)
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
let mockTryClean = git.tryClean as jest.Mock<any, any>
|
let mockTryClean = git.tryClean as jest.Mock<any>
|
||||||
mockTryClean.mockImplementation(async () => {
|
mockTryClean.mockImplementation(async () => {
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@ -210,7 +232,7 @@ describe('git-directory-helper tests', () => {
|
|||||||
// Arrange
|
// Arrange
|
||||||
await setup(removesContentsWhenResetFails)
|
await setup(removesContentsWhenResetFails)
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
let mockTryReset = git.tryReset as jest.Mock<any, any>
|
let mockTryReset = git.tryReset as jest.Mock<any>
|
||||||
mockTryReset.mockImplementation(async () => {
|
mockTryReset.mockImplementation(async () => {
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@ -260,7 +282,7 @@ describe('git-directory-helper tests', () => {
|
|||||||
// Arrange
|
// Arrange
|
||||||
await setup(removesLocalBranches)
|
await setup(removesLocalBranches)
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
const mockBranchList = git.branchList as jest.Mock<any, any>
|
const mockBranchList = git.branchList as jest.Mock<any>
|
||||||
mockBranchList.mockImplementation(async (remote: boolean) => {
|
mockBranchList.mockImplementation(async (remote: boolean) => {
|
||||||
return remote ? [] : ['local-branch-1', 'local-branch-2']
|
return remote ? [] : ['local-branch-1', 'local-branch-2']
|
||||||
})
|
})
|
||||||
@ -291,7 +313,7 @@ describe('git-directory-helper tests', () => {
|
|||||||
|
|
||||||
//mock bad submodule
|
//mock bad submodule
|
||||||
|
|
||||||
const submoduleStatus = git.submoduleStatus as jest.Mock<any, any>
|
const submoduleStatus = git.submoduleStatus as jest.Mock<any>
|
||||||
submoduleStatus.mockImplementation(async (remote: boolean) => {
|
submoduleStatus.mockImplementation(async (remote: boolean) => {
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@ -319,7 +341,7 @@ describe('git-directory-helper tests', () => {
|
|||||||
await setup(doesNotCleanWhenSubmoduleStatusIsTrue)
|
await setup(doesNotCleanWhenSubmoduleStatusIsTrue)
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
|
||||||
const submoduleStatus = git.submoduleStatus as jest.Mock<any, any>
|
const submoduleStatus = git.submoduleStatus as jest.Mock<any>
|
||||||
submoduleStatus.mockImplementation(async (remote: boolean) => {
|
submoduleStatus.mockImplementation(async (remote: boolean) => {
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@ -381,7 +403,7 @@ describe('git-directory-helper tests', () => {
|
|||||||
// Arrange
|
// Arrange
|
||||||
await setup(removesAncestorRemoteBranch)
|
await setup(removesAncestorRemoteBranch)
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
const mockBranchList = git.branchList as jest.Mock<any, any>
|
const mockBranchList = git.branchList as jest.Mock<any>
|
||||||
mockBranchList.mockImplementation(async (remote: boolean) => {
|
mockBranchList.mockImplementation(async (remote: boolean) => {
|
||||||
return remote ? ['origin/remote-branch-1', 'origin/remote-branch-2'] : []
|
return remote ? ['origin/remote-branch-1', 'origin/remote-branch-2'] : []
|
||||||
})
|
})
|
||||||
@ -411,7 +433,7 @@ describe('git-directory-helper tests', () => {
|
|||||||
// Arrange
|
// Arrange
|
||||||
await setup(removesDescendantRemoteBranches)
|
await setup(removesDescendantRemoteBranches)
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
const mockBranchList = git.branchList as jest.Mock<any, any>
|
const mockBranchList = git.branchList as jest.Mock<any>
|
||||||
mockBranchList.mockImplementation(async (remote: boolean) => {
|
mockBranchList.mockImplementation(async (remote: boolean) => {
|
||||||
return remote
|
return remote
|
||||||
? ['origin/remote-branch-1/conflict', 'origin/remote-branch-2']
|
? ['origin/remote-branch-1/conflict', 'origin/remote-branch-2']
|
||||||
@ -471,6 +493,7 @@ async function setup(testName: string): Promise<void> {
|
|||||||
configExists: jest.fn(),
|
configExists: jest.fn(),
|
||||||
fetch: jest.fn(),
|
fetch: jest.fn(),
|
||||||
getDefaultBranch: jest.fn(),
|
getDefaultBranch: jest.fn(),
|
||||||
|
getSubmoduleConfigPaths: jest.fn(async () => []),
|
||||||
getWorkingDirectory: jest.fn(() => repositoryPath),
|
getWorkingDirectory: jest.fn(() => repositoryPath),
|
||||||
init: jest.fn(),
|
init: jest.fn(),
|
||||||
isDetached: jest.fn(),
|
isDetached: jest.fn(),
|
||||||
@ -493,27 +516,18 @@ async function setup(testName: string): Promise<void> {
|
|||||||
return true
|
return true
|
||||||
}),
|
}),
|
||||||
tryConfigUnset: jest.fn(),
|
tryConfigUnset: jest.fn(),
|
||||||
|
tryConfigUnsetValue: jest.fn(),
|
||||||
tryDisableAutomaticGarbageCollection: jest.fn(),
|
tryDisableAutomaticGarbageCollection: jest.fn(),
|
||||||
tryGetFetchUrl: jest.fn(async () => {
|
tryGetFetchUrl: jest.fn(async () => {
|
||||||
// Sanity check - this function shouldn't be called when the .git directory doesn't exist
|
// Sanity check - this function shouldn't be called when the .git directory doesn't exist
|
||||||
await fs.promises.stat(path.join(repositoryPath, '.git'))
|
await fs.promises.stat(path.join(repositoryPath, '.git'))
|
||||||
return repositoryUrl
|
return repositoryUrl
|
||||||
}),
|
}),
|
||||||
getSubmoduleConfigPaths: jest.fn(async () => {
|
tryGetConfigValues: jest.fn(),
|
||||||
return []
|
tryGetConfigKeys: jest.fn(),
|
||||||
}),
|
|
||||||
tryConfigUnsetValue: jest.fn(async () => {
|
|
||||||
return true
|
|
||||||
}),
|
|
||||||
tryGetConfigValues: jest.fn(async () => {
|
|
||||||
return []
|
|
||||||
}),
|
|
||||||
tryGetConfigKeys: jest.fn(async () => {
|
|
||||||
return []
|
|
||||||
}),
|
|
||||||
tryReset: jest.fn(async () => {
|
tryReset: jest.fn(async () => {
|
||||||
return true
|
return true
|
||||||
}),
|
}),
|
||||||
version: jest.fn()
|
version: jest.fn()
|
||||||
}
|
} as unknown as IGitCommandManager
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {GitVersion} from '../src/git-version'
|
import {describe, it, expect} from '@jest/globals'
|
||||||
import {MinimumGitSparseCheckoutVersion} from '../src/git-command-manager'
|
import {GitVersion} from '../src/git-version.js'
|
||||||
|
import {MinimumGitSparseCheckoutVersion} from '../src/git-command-manager.js'
|
||||||
|
|
||||||
describe('git-version tests', () => {
|
describe('git-version tests', () => {
|
||||||
it('basics', async () => {
|
it('basics', async () => {
|
||||||
|
|||||||
112
__test__/github-api-helper.test.ts
Normal file
112
__test__/github-api-helper.test.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals'
|
||||||
|
|
||||||
|
// Mock @actions/core
|
||||||
|
const mockDebug = jest.fn()
|
||||||
|
jest.unstable_mockModule('@actions/core', () => ({
|
||||||
|
debug: mockDebug,
|
||||||
|
info: jest.fn(),
|
||||||
|
warning: jest.fn(),
|
||||||
|
error: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock @actions/github
|
||||||
|
const mockGetOctokit = jest.fn()
|
||||||
|
jest.unstable_mockModule('@actions/github', () => ({
|
||||||
|
getOctokit: mockGetOctokit
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Dynamic imports after mocking
|
||||||
|
const githubApiHelper = await import('../src/github-api-helper.js')
|
||||||
|
|
||||||
|
describe('github-api-helper object format', () => {
|
||||||
|
let request: jest.Mock<any>
|
||||||
|
|
||||||
|
function mockHashAlgorithmApi(hashAlgorithm: string): void {
|
||||||
|
request = jest.fn(async () => ({
|
||||||
|
data: {
|
||||||
|
hash_algorithm: hashAlgorithm
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
mockGetOctokit.mockReturnValue({
|
||||||
|
request
|
||||||
|
} as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockDebug.mockClear()
|
||||||
|
mockGetOctokit.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects SHA-256 from the repository hash algorithm endpoint', async () => {
|
||||||
|
mockHashAlgorithmApi('sha256')
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
||||||
|
).resolves.toEqual({format: 'sha256', succeeded: true})
|
||||||
|
|
||||||
|
expect(mockGetOctokit).toHaveBeenCalledWith(
|
||||||
|
'token',
|
||||||
|
expect.objectContaining({baseUrl: 'https://api.github.com'})
|
||||||
|
)
|
||||||
|
expect(request).toHaveBeenCalledWith(
|
||||||
|
'GET /repos/{owner}/{repo}/hash-algorithm',
|
||||||
|
{owner: 'owner', repo: 'repo'}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects SHA-1 from the repository hash algorithm endpoint', async () => {
|
||||||
|
mockHashAlgorithmApi('sha1')
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
||||||
|
).resolves.toEqual({format: 'sha1', succeeded: true})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects object format from an existing commit without API calls', async () => {
|
||||||
|
const commitSha =
|
||||||
|
'9422233ca7ee1b17f1e905d0e141faf0c401556c41cdc6acd71c6bd685da2e92'
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
githubApiHelper.tryGetRepositoryObjectFormat(
|
||||||
|
'token',
|
||||||
|
'owner',
|
||||||
|
'repo',
|
||||||
|
undefined,
|
||||||
|
commitSha
|
||||||
|
)
|
||||||
|
).resolves.toEqual({format: 'sha256', succeeded: true})
|
||||||
|
|
||||||
|
expect(mockGetOctokit).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns unsuccessful when the hash algorithm endpoint value is not recognized', async () => {
|
||||||
|
mockHashAlgorithmApi('unknown')
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
||||||
|
).resolves.toEqual({format: '', succeeded: false})
|
||||||
|
expect(mockDebug).toHaveBeenCalledWith(
|
||||||
|
'Unable to determine repository object format from hash-algorithm endpoint'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns unsuccessful when the hash algorithm API lookup fails', async () => {
|
||||||
|
request = jest.fn(async () => {
|
||||||
|
throw new Error('not found')
|
||||||
|
})
|
||||||
|
mockGetOctokit.mockReturnValue({
|
||||||
|
request
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
||||||
|
).resolves.toEqual({format: '', succeeded: false})
|
||||||
|
expect(mockDebug).toHaveBeenCalledWith(
|
||||||
|
'Unable to determine repository object format from hash-algorithm endpoint: not found'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,10 +1,13 @@
|
|||||||
import * as core from '@actions/core'
|
import {
|
||||||
import * as fsHelper from '../lib/fs-helper'
|
jest,
|
||||||
import * as github from '@actions/github'
|
describe,
|
||||||
import * as inputHelper from '../lib/input-helper'
|
it,
|
||||||
|
expect,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
afterAll
|
||||||
|
} from '@jest/globals'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as workflowContextHelper from '../lib/workflow-context-helper'
|
|
||||||
import {IGitSourceSettings} from '../lib/git-source-settings'
|
|
||||||
|
|
||||||
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
|
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||||
const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
|
const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
|
||||||
@ -12,6 +15,15 @@ const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
|
|||||||
// Inputs for mock @actions/core
|
// Inputs for mock @actions/core
|
||||||
let inputs = {} as any
|
let inputs = {} as any
|
||||||
|
|
||||||
|
// Mutable mock github context
|
||||||
|
const mockGithubContext: any = {
|
||||||
|
ref: 'refs/heads/some-ref',
|
||||||
|
sha: '1234567890123456789012345678901234567890',
|
||||||
|
repo: {owner: 'some-owner', repo: 'some-repo'},
|
||||||
|
eventName: '',
|
||||||
|
payload: {}
|
||||||
|
}
|
||||||
|
|
||||||
// Replicate @actions/core getInput behavior: it trims whitespace by default
|
// Replicate @actions/core getInput behavior: it trims whitespace by default
|
||||||
// (String.prototype.trim(), which strips characters such as a leading U+FEFF BOM)
|
// (String.prototype.trim(), which strips characters such as a leading U+FEFF BOM)
|
||||||
// unless trimWhitespace is explicitly set to false.
|
// unless trimWhitespace is explicitly set to false.
|
||||||
@ -23,40 +35,49 @@ const getInputImpl = (name: string, options?: {trimWhitespace?: boolean}) => {
|
|||||||
return typeof val === 'string' ? val.trim() : val
|
return typeof val === 'string' ? val.trim() : val
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shallow clone original @actions/github context
|
// Mock @actions/core before loading input-helper
|
||||||
let originalContext = {...github.context}
|
jest.unstable_mockModule('@actions/core', () => ({
|
||||||
|
getInput: jest.fn(getInputImpl),
|
||||||
|
getBooleanInput: jest.fn((name: string) => inputs[name]),
|
||||||
|
getMultilineInput: jest.fn((name: string) =>
|
||||||
|
inputs[name] ? String(inputs[name]).split('\n').filter(Boolean) : []
|
||||||
|
),
|
||||||
|
error: jest.fn(),
|
||||||
|
warning: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
setFailed: jest.fn(),
|
||||||
|
setOutput: jest.fn(),
|
||||||
|
setSecret: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock @actions/github before loading input-helper
|
||||||
|
jest.unstable_mockModule('@actions/github', () => ({
|
||||||
|
context: mockGithubContext,
|
||||||
|
getOctokit: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock fs-helper
|
||||||
|
const mockDirectoryExistsSync = jest.fn((p: string) => p === gitHubWorkspace)
|
||||||
|
jest.unstable_mockModule('../src/fs-helper.js', () => ({
|
||||||
|
directoryExistsSync: mockDirectoryExistsSync,
|
||||||
|
fileExistsSync: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock workflow-context-helper
|
||||||
|
const mockGetOrganizationId = jest.fn(async () => 123456)
|
||||||
|
jest.unstable_mockModule('../src/workflow-context-helper.js', () => ({
|
||||||
|
getOrganizationId: mockGetOrganizationId
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Dynamic imports after mocking
|
||||||
|
const core = await import('@actions/core')
|
||||||
|
const inputHelper = await import('../src/input-helper.js')
|
||||||
|
type IGitSourceSettings =
|
||||||
|
import('../src/git-source-settings.js').IGitSourceSettings
|
||||||
|
|
||||||
describe('input-helper tests', () => {
|
describe('input-helper tests', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// Mock getInput
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation(getInputImpl as any)
|
|
||||||
|
|
||||||
// Mock error/warning/info/debug
|
|
||||||
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
|
||||||
|
|
||||||
// Mock github context
|
|
||||||
jest.spyOn(github.context, 'repo', 'get').mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
owner: 'some-owner',
|
|
||||||
repo: 'some-repo'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
github.context.ref = 'refs/heads/some-ref'
|
|
||||||
github.context.sha = '1234567890123456789012345678901234567890'
|
|
||||||
|
|
||||||
// Mock ./fs-helper directoryExistsSync()
|
|
||||||
jest
|
|
||||||
.spyOn(fsHelper, 'directoryExistsSync')
|
|
||||||
.mockImplementation((path: string) => path == gitHubWorkspace)
|
|
||||||
|
|
||||||
// Mock ./workflowContextHelper getOrganizationId()
|
|
||||||
jest
|
|
||||||
.spyOn(workflowContextHelper, 'getOrganizationId')
|
|
||||||
.mockImplementation(() => Promise.resolve(123456))
|
|
||||||
|
|
||||||
// GitHub workspace
|
// GitHub workspace
|
||||||
process.env['GITHUB_WORKSPACE'] = gitHubWorkspace
|
process.env['GITHUB_WORKSPACE'] = gitHubWorkspace
|
||||||
})
|
})
|
||||||
@ -64,6 +85,13 @@ describe('input-helper tests', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset inputs
|
// Reset inputs
|
||||||
inputs = {}
|
inputs = {}
|
||||||
|
jest.clearAllMocks()
|
||||||
|
// Re-apply default mocks
|
||||||
|
;(core.getInput as jest.Mock<any>).mockImplementation(getInputImpl as any)
|
||||||
|
mockDirectoryExistsSync.mockImplementation(
|
||||||
|
(p: string) => p === gitHubWorkspace
|
||||||
|
)
|
||||||
|
mockGetOrganizationId.mockResolvedValue(123456)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@ -74,11 +102,8 @@ describe('input-helper tests', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore @actions/github context
|
// Restore @actions/github context
|
||||||
github.context.ref = originalContext.ref
|
mockGithubContext.ref = 'refs/heads/some-ref'
|
||||||
github.context.sha = originalContext.sha
|
mockGithubContext.sha = '1234567890123456789012345678901234567890'
|
||||||
|
|
||||||
// Restore
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets defaults', async () => {
|
it('sets defaults', async () => {
|
||||||
@ -104,15 +129,15 @@ describe('input-helper tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('qualifies ref', async () => {
|
it('qualifies ref', async () => {
|
||||||
let originalRef = github.context.ref
|
let originalRef = mockGithubContext.ref
|
||||||
try {
|
try {
|
||||||
github.context.ref = 'some-unqualified-ref'
|
mockGithubContext.ref = 'some-unqualified-ref'
|
||||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||||
expect(settings).toBeTruthy()
|
expect(settings).toBeTruthy()
|
||||||
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
|
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
|
||||||
expect(settings.ref).toBe('refs/heads/some-unqualified-ref')
|
expect(settings.ref).toBe('refs/heads/some-unqualified-ref')
|
||||||
} finally {
|
} finally {
|
||||||
github.context.ref = originalRef
|
mockGithubContext.ref = originalRef
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -143,6 +168,16 @@ describe('input-helper tests', () => {
|
|||||||
expect(settings.commit).toBe('1111111111222222222233333333334444444444')
|
expect(settings.commit).toBe('1111111111222222222233333333334444444444')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('sets ref to empty when explicit sha-256', async () => {
|
||||||
|
inputs.ref =
|
||||||
|
'1111111111222222222233333333334444444444555555555566666666667777'
|
||||||
|
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||||
|
expect(settings.ref).toBeFalsy()
|
||||||
|
expect(settings.commit).toBe(
|
||||||
|
'1111111111222222222233333333334444444444555555555566666666667777'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('sets sha to empty when explicit ref', async () => {
|
it('sets sha to empty when explicit ref', async () => {
|
||||||
inputs.ref = 'refs/heads/some-other-ref'
|
inputs.ref = 'refs/heads/some-other-ref'
|
||||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||||
@ -160,6 +195,16 @@ describe('input-helper tests', () => {
|
|||||||
expect(settings.ref).toBe('522d932fae5296da51fdf431934425ecf891c6a2')
|
expect(settings.ref).toBe('522d932fae5296da51fdf431934425ecf891c6a2')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('does not reclassify a sha-256 ref as sha when a BOM is prefixed', async () => {
|
||||||
|
inputs.ref =
|
||||||
|
'\uFEFF1111111111222222222233333333334444444444555555555566666666667777'
|
||||||
|
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||||
|
expect(settings.commit).toBeFalsy()
|
||||||
|
expect(settings.ref).toBe(
|
||||||
|
'1111111111222222222233333333334444444444555555555566666666667777'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('treats a sha surrounded by ascii whitespace as a commit', async () => {
|
it('treats a sha surrounded by ascii whitespace as a commit', async () => {
|
||||||
// ASCII whitespace can only come from the workflow author's YAML (git ref
|
// ASCII whitespace can only come from the workflow author's YAML (git ref
|
||||||
// names cannot contain it), so trimming it and treating the value as a
|
// names cannot contain it), so trimming it and treating the value as a
|
||||||
@ -188,37 +233,35 @@ describe('input-helper tests', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('allows the default self-checkout on a fork pull_request_target', async () => {
|
it('allows the default self-checkout on a fork pull_request_target', async () => {
|
||||||
const originalEvent = github.context.eventName
|
const originalEvent = mockGithubContext.eventName
|
||||||
const originalPayload = github.context.payload
|
const originalPayload = mockGithubContext.payload
|
||||||
const originalSha = github.context.sha
|
|
||||||
try {
|
try {
|
||||||
github.context.eventName = 'pull_request_target'
|
mockGithubContext.eventName = 'pull_request_target'
|
||||||
github.context.payload = forkPayload as any
|
mockGithubContext.payload = forkPayload
|
||||||
// Simulate a rebase/fast-forward merge where the base tip (event SHA)
|
// Simulate a rebase/fast-forward merge where the base tip (event SHA)
|
||||||
// equals the PR head SHA. The default self-checkout must still succeed.
|
// equals the PR head SHA. The default self-checkout must still succeed.
|
||||||
github.context.sha = '1234567890123456789012345678901234567890'
|
mockGithubContext.sha = '1234567890123456789012345678901234567890'
|
||||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||||
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
|
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
|
||||||
} finally {
|
} finally {
|
||||||
github.context.eventName = originalEvent
|
mockGithubContext.eventName = originalEvent
|
||||||
github.context.payload = originalPayload
|
mockGithubContext.payload = originalPayload
|
||||||
github.context.sha = originalSha
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('refuses an explicit fork repository on pull_request_target', async () => {
|
it('refuses an explicit fork repository on pull_request_target', async () => {
|
||||||
const originalEvent = github.context.eventName
|
const originalEvent = mockGithubContext.eventName
|
||||||
const originalPayload = github.context.payload
|
const originalPayload = mockGithubContext.payload
|
||||||
try {
|
try {
|
||||||
github.context.eventName = 'pull_request_target'
|
mockGithubContext.eventName = 'pull_request_target'
|
||||||
github.context.payload = forkPayload as any
|
mockGithubContext.payload = forkPayload
|
||||||
inputs.repository = 'attacker/fork'
|
inputs.repository = 'attacker/fork'
|
||||||
await expect(inputHelper.getInputs()).rejects.toThrow(
|
await expect(inputHelper.getInputs()).rejects.toThrow(
|
||||||
/Refusing to check out fork pull request code/
|
/Refusing to check out fork pull request code/
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
github.context.eventName = originalEvent
|
mockGithubContext.eventName = originalEvent
|
||||||
github.context.payload = originalPayload
|
mockGithubContext.payload = originalPayload
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,13 +1,46 @@
|
|||||||
|
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals'
|
||||||
import * as assert from 'assert'
|
import * as assert from 'assert'
|
||||||
import * as refHelper from '../lib/ref-helper'
|
|
||||||
import {IGitCommandManager} from '../lib/git-command-manager'
|
// Mutable mock github context
|
||||||
|
const mockGithubContext: any = {
|
||||||
|
eventName: '',
|
||||||
|
payload: {},
|
||||||
|
repo: {owner: 'some-owner', repo: 'some-repo'},
|
||||||
|
ref: '',
|
||||||
|
sha: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock @actions/core
|
||||||
|
const mockDebug = jest.fn()
|
||||||
|
jest.unstable_mockModule('@actions/core', () => ({
|
||||||
|
debug: mockDebug,
|
||||||
|
info: jest.fn(),
|
||||||
|
warning: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
setFailed: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock @actions/github
|
||||||
|
const mockGetOctokit = jest.fn()
|
||||||
|
jest.unstable_mockModule('@actions/github', () => ({
|
||||||
|
context: mockGithubContext,
|
||||||
|
getOctokit: mockGetOctokit
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Dynamic imports after mocking
|
||||||
|
const refHelper = await import('../src/ref-helper.js')
|
||||||
|
type IGitCommandManager =
|
||||||
|
import('../src/git-command-manager.js').IGitCommandManager
|
||||||
|
|
||||||
const commit = '1234567890123456789012345678901234567890'
|
const commit = '1234567890123456789012345678901234567890'
|
||||||
|
const sha256Commit =
|
||||||
|
'1234567890123456789012345678901234567890123456789012345678901234'
|
||||||
let git: IGitCommandManager
|
let git: IGitCommandManager
|
||||||
|
|
||||||
describe('ref-helper tests', () => {
|
describe('ref-helper tests', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
git = {} as unknown as IGitCommandManager
|
git = {} as unknown as IGitCommandManager
|
||||||
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('getCheckoutInfo requires git', async () => {
|
it('getCheckoutInfo requires git', async () => {
|
||||||
@ -37,6 +70,12 @@ describe('ref-helper tests', () => {
|
|||||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('getCheckoutInfo sha-256 only', async () => {
|
||||||
|
const checkoutInfo = await refHelper.getCheckoutInfo(git, '', sha256Commit)
|
||||||
|
expect(checkoutInfo.ref).toBe(sha256Commit)
|
||||||
|
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
it('getCheckoutInfo refs/heads/', async () => {
|
it('getCheckoutInfo refs/heads/', async () => {
|
||||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||||
git,
|
git,
|
||||||
@ -152,7 +191,20 @@ describe('ref-helper tests', () => {
|
|||||||
it('getRefSpec sha + refs/tags/', async () => {
|
it('getRefSpec sha + refs/tags/', async () => {
|
||||||
const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit)
|
const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit)
|
||||||
expect(refSpec.length).toBe(1)
|
expect(refSpec.length).toBe(1)
|
||||||
expect(refSpec[0]).toBe(`+${commit}:refs/tags/my-tag`)
|
expect(refSpec[0]).toBe(`+refs/tags/my-tag:refs/tags/my-tag`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getRefSpec sha + refs/tags/ with fetchTags', async () => {
|
||||||
|
const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit, true)
|
||||||
|
expect(refSpec.length).toBe(1)
|
||||||
|
expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getRefSpec sha + refs/heads/ with fetchTags', async () => {
|
||||||
|
const refSpec = refHelper.getRefSpec('refs/heads/my/branch', commit, true)
|
||||||
|
expect(refSpec.length).toBe(2)
|
||||||
|
expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
|
||||||
|
expect(refSpec[1]).toBe(`+${commit}:refs/remotes/origin/my/branch`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('getRefSpec sha only', async () => {
|
it('getRefSpec sha only', async () => {
|
||||||
@ -168,6 +220,13 @@ describe('ref-helper tests', () => {
|
|||||||
expect(refSpec[1]).toBe('+refs/tags/my-ref*:refs/tags/my-ref*')
|
expect(refSpec[1]).toBe('+refs/tags/my-ref*:refs/tags/my-ref*')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('getRefSpec unqualified ref only with fetchTags', async () => {
|
||||||
|
const refSpec = refHelper.getRefSpec('my-ref', '', true)
|
||||||
|
expect(refSpec.length).toBe(2)
|
||||||
|
expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
|
||||||
|
expect(refSpec[1]).toBe('+refs/heads/my-ref*:refs/remotes/origin/my-ref*')
|
||||||
|
})
|
||||||
|
|
||||||
it('getRefSpec refs/heads/ only', async () => {
|
it('getRefSpec refs/heads/ only', async () => {
|
||||||
const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '')
|
const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '')
|
||||||
expect(refSpec.length).toBe(1)
|
expect(refSpec.length).toBe(1)
|
||||||
@ -187,4 +246,155 @@ describe('ref-helper tests', () => {
|
|||||||
expect(refSpec.length).toBe(1)
|
expect(refSpec.length).toBe(1)
|
||||||
expect(refSpec[0]).toBe('+refs/tags/my-tag:refs/tags/my-tag')
|
expect(refSpec[0]).toBe('+refs/tags/my-tag:refs/tags/my-tag')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('getRefSpec refs/tags/ only with fetchTags', async () => {
|
||||||
|
const refSpec = refHelper.getRefSpec('refs/tags/my-tag', '', true)
|
||||||
|
expect(refSpec.length).toBe(1)
|
||||||
|
expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getRefSpec refs/heads/ only with fetchTags', async () => {
|
||||||
|
const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '', true)
|
||||||
|
expect(refSpec.length).toBe(2)
|
||||||
|
expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
|
||||||
|
expect(refSpec[1]).toBe(
|
||||||
|
'+refs/heads/my/branch:refs/remotes/origin/my/branch'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('checkCommitInfo', () => {
|
||||||
|
const repositoryOwner = 'some-owner'
|
||||||
|
const repositoryName = 'some-repo'
|
||||||
|
const ref = 'refs/pull/123/merge'
|
||||||
|
const sha1Head = '1111111111222222222233333333334444444444'
|
||||||
|
const sha1Base = 'aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd'
|
||||||
|
const sha256Head =
|
||||||
|
'1111111111222222222233333333334444444444555555555566666666667777'
|
||||||
|
const sha256Base =
|
||||||
|
'aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffff0000'
|
||||||
|
let repoGetSpy: jest.Mock<any>
|
||||||
|
let originalEventName: string
|
||||||
|
let originalPayload: unknown
|
||||||
|
let originalRef: string
|
||||||
|
let originalSha: string
|
||||||
|
|
||||||
|
function setPullRequestContext(
|
||||||
|
expectedHeadSha: string,
|
||||||
|
expectedBaseSha: string,
|
||||||
|
mergeCommit: string
|
||||||
|
): void {
|
||||||
|
mockGithubContext.eventName = 'pull_request'
|
||||||
|
mockGithubContext.ref = ref
|
||||||
|
mockGithubContext.sha = mergeCommit
|
||||||
|
mockGithubContext.payload = {
|
||||||
|
action: 'synchronize',
|
||||||
|
after: expectedHeadSha,
|
||||||
|
number: 123,
|
||||||
|
pull_request: {
|
||||||
|
base: {
|
||||||
|
sha: expectedBaseSha
|
||||||
|
}
|
||||||
|
},
|
||||||
|
repository: {
|
||||||
|
private: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalEventName = mockGithubContext.eventName
|
||||||
|
originalPayload = mockGithubContext.payload
|
||||||
|
originalRef = mockGithubContext.ref
|
||||||
|
originalSha = mockGithubContext.sha
|
||||||
|
|
||||||
|
mockGithubContext.repo = {
|
||||||
|
owner: repositoryOwner,
|
||||||
|
repo: repositoryName
|
||||||
|
}
|
||||||
|
|
||||||
|
repoGetSpy = jest.fn(async () => ({}))
|
||||||
|
mockGetOctokit.mockReturnValue({
|
||||||
|
rest: {
|
||||||
|
repos: {
|
||||||
|
get: repoGetSpy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockGithubContext.eventName = originalEventName
|
||||||
|
mockGithubContext.payload = originalPayload
|
||||||
|
mockGithubContext.ref = originalRef
|
||||||
|
mockGithubContext.sha = originalSha
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns early for SHA-1 merge commit', async () => {
|
||||||
|
setPullRequestContext(sha1Head, sha1Base, commit)
|
||||||
|
|
||||||
|
await refHelper.checkCommitInfo(
|
||||||
|
'token',
|
||||||
|
`Merge ${sha1Head} into ${sha1Base}`,
|
||||||
|
repositoryOwner,
|
||||||
|
repositoryName,
|
||||||
|
ref,
|
||||||
|
commit
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockGetOctokit).not.toHaveBeenCalled()
|
||||||
|
expect(repoGetSpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches SHA-256 merge commit info', async () => {
|
||||||
|
const actualHeadSha =
|
||||||
|
'9999999999888888888877777777776666666666555555555544444444443333'
|
||||||
|
setPullRequestContext(sha256Head, sha256Base, sha256Commit)
|
||||||
|
|
||||||
|
await refHelper.checkCommitInfo(
|
||||||
|
'token',
|
||||||
|
`Merge ${actualHeadSha} into ${sha256Base}`,
|
||||||
|
repositoryOwner,
|
||||||
|
repositoryName,
|
||||||
|
ref,
|
||||||
|
sha256Commit
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockGetOctokit).toHaveBeenCalledWith(
|
||||||
|
'token',
|
||||||
|
expect.objectContaining({
|
||||||
|
userAgent: expect.stringContaining(
|
||||||
|
`expected_head_sha=${sha256Head};actual_head_sha=${actualHeadSha}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(repoGetSpy).toHaveBeenCalledWith({
|
||||||
|
owner: repositoryOwner,
|
||||||
|
repo: repositoryName
|
||||||
|
})
|
||||||
|
expect(mockDebug).toHaveBeenCalledWith(
|
||||||
|
`Expected head sha ${sha256Head}; actual head sha ${actualHeadSha}`
|
||||||
|
)
|
||||||
|
expect(mockDebug).not.toHaveBeenCalledWith('Unexpected message format')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not match 50-char hex as a valid merge', async () => {
|
||||||
|
const invalidHeadSha =
|
||||||
|
'99999999998888888888777777777766666666665555555555'
|
||||||
|
setPullRequestContext(sha1Head, sha1Base, commit)
|
||||||
|
|
||||||
|
await refHelper.checkCommitInfo(
|
||||||
|
'token',
|
||||||
|
`Merge ${invalidHeadSha} into ${sha1Base}`,
|
||||||
|
repositoryOwner,
|
||||||
|
repositoryName,
|
||||||
|
ref,
|
||||||
|
commit
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockGetOctokit).not.toHaveBeenCalled()
|
||||||
|
expect(repoGetSpy).not.toHaveBeenCalled()
|
||||||
|
expect(mockDebug).toHaveBeenCalledWith('Unexpected message format')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,16 +1,32 @@
|
|||||||
import * as core from '@actions/core'
|
import {
|
||||||
import {RetryHelper} from '../lib/retry-helper'
|
jest,
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
expect,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
afterAll
|
||||||
|
} from '@jest/globals'
|
||||||
|
|
||||||
|
let info: string[] = []
|
||||||
|
|
||||||
|
// Mock @actions/core before loading retry-helper
|
||||||
|
jest.unstable_mockModule('@actions/core', () => ({
|
||||||
|
info: jest.fn((message: string) => {
|
||||||
|
info.push(message)
|
||||||
|
}),
|
||||||
|
debug: jest.fn(),
|
||||||
|
warning: jest.fn(),
|
||||||
|
error: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Dynamic imports after mocking
|
||||||
|
const {RetryHelper} = await import('../src/retry-helper.js')
|
||||||
|
|
||||||
let info: string[]
|
|
||||||
let retryHelper: any
|
let retryHelper: any
|
||||||
|
|
||||||
describe('retry-helper tests', () => {
|
describe('retry-helper tests', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// Mock @actions/core info()
|
|
||||||
jest.spyOn(core, 'info').mockImplementation((message: string) => {
|
|
||||||
info.push(message)
|
|
||||||
})
|
|
||||||
|
|
||||||
retryHelper = new RetryHelper(3, 0, 0)
|
retryHelper = new RetryHelper(3, 0, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -20,7 +36,6 @@ describe('retry-helper tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
// Restore
|
|
||||||
jest.restoreAllMocks()
|
jest.restoreAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import * as github from '@actions/github'
|
import {
|
||||||
import {assertSafePrCheckout} from '../lib/unsafe-pr-checkout-helper'
|
jest,
|
||||||
|
describe,
|
||||||
// Shallow clone original @actions/github context
|
it,
|
||||||
const originalContext = {...github.context}
|
expect,
|
||||||
const originalEventName = github.context.eventName
|
beforeAll,
|
||||||
const originalPayload = github.context.payload
|
afterEach,
|
||||||
|
afterAll
|
||||||
|
} from '@jest/globals'
|
||||||
|
|
||||||
const BASE_REPO_ID = 100
|
const BASE_REPO_ID = 100
|
||||||
const FORK_REPO_ID = 200
|
const FORK_REPO_ID = 200
|
||||||
@ -15,9 +17,29 @@ const WORKFLOW_RUN_HEAD_COMMIT_SHA = '4444444444444444444444444444444444444444'
|
|||||||
const BASE_QUALIFIED_REPO = 'some-owner/some-repo'
|
const BASE_QUALIFIED_REPO = 'some-owner/some-repo'
|
||||||
const FORK_QUALIFIED_REPO = 'another-repo/fork'
|
const FORK_QUALIFIED_REPO = 'another-repo/fork'
|
||||||
|
|
||||||
|
// Mutable mock context
|
||||||
|
const mockContext: any = {
|
||||||
|
eventName: '',
|
||||||
|
payload: {},
|
||||||
|
repo: {owner: 'some-owner', repo: 'some-repo'},
|
||||||
|
ref: '',
|
||||||
|
sha: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.unstable_mockModule('@actions/github', () => ({
|
||||||
|
context: mockContext
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Dynamic imports after mocking
|
||||||
|
const {assertSafePrCheckout} =
|
||||||
|
await import('../src/unsafe-pr-checkout-helper.js')
|
||||||
|
|
||||||
|
const originalEventName = mockContext.eventName
|
||||||
|
const originalPayload = mockContext.payload
|
||||||
|
|
||||||
function setContext(eventName: string, payload: object): void {
|
function setContext(eventName: string, payload: object): void {
|
||||||
;(github.context as {eventName: string}).eventName = eventName
|
mockContext.eventName = eventName
|
||||||
;(github.context as {payload: object}).payload = payload
|
mockContext.payload = payload
|
||||||
}
|
}
|
||||||
|
|
||||||
function forkPullRequestTargetPayload(): object {
|
function forkPullRequestTargetPayload(): object {
|
||||||
@ -59,22 +81,17 @@ function forkWorkflowRunPayload(): object {
|
|||||||
|
|
||||||
describe('unsafe-pr-checkout-helper', () => {
|
describe('unsafe-pr-checkout-helper', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.spyOn(github.context, 'repo', 'get').mockReturnValue({
|
mockContext.repo = {owner: 'some-owner', repo: 'some-repo'}
|
||||||
owner: 'some-owner',
|
|
||||||
repo: 'some-repo'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
;(github.context as {eventName: string}).eventName = originalEventName
|
mockContext.eventName = originalEventName
|
||||||
;(github.context as {payload: object}).payload = originalPayload
|
mockContext.payload = originalPayload
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
;(github.context as {eventName: string}).eventName =
|
mockContext.eventName = originalEventName
|
||||||
originalContext.eventName
|
mockContext.payload = originalPayload
|
||||||
;(github.context as {payload: object}).payload = originalContext.payload
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('allows pull_request events untouched', () => {
|
it('allows pull_request events untouched', () => {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import * as urlHelper from '../src/url-helper'
|
import {jest, describe, it, expect, beforeEach, afterAll} from '@jest/globals'
|
||||||
|
import * as urlHelper from '../src/url-helper.js'
|
||||||
|
|
||||||
describe('getServerUrl tests', () => {
|
describe('getServerUrl tests', () => {
|
||||||
it('basics', async () => {
|
it('basics', async () => {
|
||||||
|
|||||||
9
__test__/verify-fetch-tags.sh
Executable file
9
__test__/verify-fetch-tags.sh
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Verify tags were fetched
|
||||||
|
TAG_COUNT=$(git -C ./fetch-tags-test tag | wc -l)
|
||||||
|
if [ "$TAG_COUNT" -eq 0 ]; then
|
||||||
|
echo "Expected tags to be fetched, but found none"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Found $TAG_COUNT tags"
|
||||||
@ -17,7 +17,7 @@ fi
|
|||||||
|
|
||||||
echo "Testing persisted credential"
|
echo "Testing persisted credential"
|
||||||
pushd ./submodules-recursive/submodule-level-1/submodule-level-2
|
pushd ./submodules-recursive/submodule-level-1/submodule-level-2
|
||||||
git config --local --name-only --get-regexp http.+extraheader && git fetch
|
git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
|
||||||
if [ "$?" != "0" ]; then
|
if [ "$?" != "0" ]; then
|
||||||
echo "Failed to validate persisted credential"
|
echo "Failed to validate persisted credential"
|
||||||
popd
|
popd
|
||||||
|
|||||||
@ -17,7 +17,7 @@ fi
|
|||||||
|
|
||||||
echo "Testing persisted credential"
|
echo "Testing persisted credential"
|
||||||
pushd ./submodules-true/submodule-level-1
|
pushd ./submodules-true/submodule-level-1
|
||||||
git config --local --name-only --get-regexp http.+extraheader && git fetch
|
git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
|
||||||
if [ "$?" != "0" ]; then
|
if [ "$?" != "0" ]; then
|
||||||
echo "Failed to validate persisted credential"
|
echo "Failed to validate persisted credential"
|
||||||
popd
|
popd
|
||||||
|
|||||||
51
__test__/verify-worktree.sh
Executable file
51
__test__/verify-worktree.sh
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Verify worktree credentials
|
||||||
|
# This test verifies that git credentials work in worktrees created after checkout
|
||||||
|
# Usage: verify-worktree.sh <checkout-path> <worktree-name>
|
||||||
|
|
||||||
|
CHECKOUT_PATH="$1"
|
||||||
|
WORKTREE_NAME="$2"
|
||||||
|
|
||||||
|
if [ -z "$CHECKOUT_PATH" ] || [ -z "$WORKTREE_NAME" ]; then
|
||||||
|
echo "Usage: verify-worktree.sh <checkout-path> <worktree-name>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$CHECKOUT_PATH"
|
||||||
|
|
||||||
|
# Add safe directory for container environments
|
||||||
|
git config --global --add safe.directory "*" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Show the includeIf configuration
|
||||||
|
echo "Git config includeIf entries:"
|
||||||
|
git config --list --show-origin | grep -i include || true
|
||||||
|
|
||||||
|
# Create the worktree
|
||||||
|
echo "Creating worktree..."
|
||||||
|
git worktree add "../$WORKTREE_NAME" HEAD --detach
|
||||||
|
|
||||||
|
# Change to worktree directory
|
||||||
|
cd "../$WORKTREE_NAME"
|
||||||
|
|
||||||
|
# Verify we're in a worktree
|
||||||
|
echo "Verifying worktree gitdir:"
|
||||||
|
cat .git
|
||||||
|
|
||||||
|
# Verify credentials are available in worktree by checking extraheader is configured
|
||||||
|
echo "Checking credentials in worktree..."
|
||||||
|
if git config --list --show-origin | grep -q "extraheader"; then
|
||||||
|
echo "Credentials are configured in worktree"
|
||||||
|
else
|
||||||
|
echo "ERROR: Credentials are NOT configured in worktree"
|
||||||
|
echo "Full git config:"
|
||||||
|
git config --list --show-origin
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify fetch works in the worktree
|
||||||
|
echo "Fetching in worktree..."
|
||||||
|
git fetch origin
|
||||||
|
|
||||||
|
echo "Worktree credentials test passed!"
|
||||||
@ -113,6 +113,6 @@ outputs:
|
|||||||
commit:
|
commit:
|
||||||
description: 'The commit SHA that was checked out'
|
description: 'The commit SHA that was checked out'
|
||||||
runs:
|
runs:
|
||||||
using: node20
|
using: node24
|
||||||
main: dist/index.js
|
main: dist/index.js
|
||||||
post: dist/index.js
|
post: dist/index.js
|
||||||
|
|||||||
52844
dist/index.js
vendored
52844
dist/index.js
vendored
File diff suppressed because one or more lines are too long
3
dist/package.json
vendored
Normal file
3
dist/package.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
62
eslint.config.mjs
Normal file
62
eslint.config.mjs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import github from 'eslint-plugin-github'
|
||||||
|
import jest from 'eslint-plugin-jest'
|
||||||
|
|
||||||
|
const githubConfigs = github.getFlatConfigs()
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: ['**/node_modules/**', '**/lib/**', '**/dist/**']
|
||||||
|
},
|
||||||
|
githubConfigs.recommended,
|
||||||
|
...githubConfigs.typescript,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'eslint-comments/no-use': 'off',
|
||||||
|
'import/no-namespace': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'error',
|
||||||
|
'@typescript-eslint/explicit-member-accessibility': [
|
||||||
|
'error',
|
||||||
|
{accessibility: 'no-public'}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-require-imports': 'error',
|
||||||
|
'@typescript-eslint/array-type': 'error',
|
||||||
|
'@typescript-eslint/await-thenable': 'error',
|
||||||
|
camelcase: 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': [
|
||||||
|
'error',
|
||||||
|
{allowExpressions: true}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-array-constructor': 'error',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'error',
|
||||||
|
'@typescript-eslint/no-extraneous-class': 'error',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
|
'@typescript-eslint/no-for-in-array': 'error',
|
||||||
|
'@typescript-eslint/no-inferrable-types': 'error',
|
||||||
|
'@typescript-eslint/no-misused-new': 'error',
|
||||||
|
'@typescript-eslint/no-namespace': 'error',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||||
|
'@typescript-eslint/no-unnecessary-qualifier': 'error',
|
||||||
|
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||||
|
'@typescript-eslint/no-useless-constructor': 'error',
|
||||||
|
'@typescript-eslint/prefer-for-of': 'warn',
|
||||||
|
'@typescript-eslint/prefer-function-type': 'warn',
|
||||||
|
'@typescript-eslint/prefer-includes': 'error',
|
||||||
|
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
|
||||||
|
'@typescript-eslint/promise-function-async': 'error',
|
||||||
|
'@typescript-eslint/require-array-sort-compare': 'error',
|
||||||
|
'@typescript-eslint/restrict-plus-operands': 'error',
|
||||||
|
'@typescript-eslint/unbound-method': 'error'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['__test__/**/*.ts'],
|
||||||
|
...jest.configs['flat/recommended']
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -1,12 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
clearMocks: true,
|
|
||||||
fakeTimers: {},
|
|
||||||
moduleFileExtensions: ['js', 'ts'],
|
|
||||||
testEnvironment: 'node',
|
|
||||||
testMatch: ['**/*.test.ts'],
|
|
||||||
testRunner: 'jest-circus/runner',
|
|
||||||
transform: {
|
|
||||||
'^.+\\.ts$': 'ts-jest'
|
|
||||||
},
|
|
||||||
verbose: true
|
|
||||||
}
|
|
||||||
24
jest.config.ts
Normal file
24
jest.config.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export default {
|
||||||
|
clearMocks: true,
|
||||||
|
moduleFileExtensions: ['js', 'ts'],
|
||||||
|
roots: ['<rootDir>'],
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testMatch: ['**/*.test.ts'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.ts$': [
|
||||||
|
'ts-jest',
|
||||||
|
{
|
||||||
|
useESM: true,
|
||||||
|
diagnostics: {
|
||||||
|
ignoreCodes: [151002]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
extensionsToTreatAsEsm: ['.ts'],
|
||||||
|
transformIgnorePatterns: ['node_modules/(?!(@actions)/)'],
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^(\\.{1,2}/.*)\\.js$': '$1'
|
||||||
|
},
|
||||||
|
verbose: true
|
||||||
|
}
|
||||||
3785
package-lock.json
generated
3785
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "checkout",
|
"name": "checkout",
|
||||||
"version": "4.3.0",
|
"version": "7.0.1",
|
||||||
"description": "checkout action",
|
"description": "checkout action",
|
||||||
|
"type": "module",
|
||||||
"main": "lib/main.js",
|
"main": "lib/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && ncc build && node lib/misc/generate-docs.js",
|
"build": "tsc && ncc build src/main.ts -o dist && node lib/misc/generate-docs.js",
|
||||||
"format": "prettier --write '**/*.ts'",
|
"format": "prettier --write '**/*.ts'",
|
||||||
"format-check": "prettier --check '**/*.ts'",
|
"format-check": "prettier --check '**/*.ts'",
|
||||||
"lint": "eslint src/**/*.ts",
|
"lint": "eslint src/**/*.ts",
|
||||||
"test": "jest",
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
||||||
"licensed-check": "src/misc/licensed-check.sh",
|
"licensed-check": "src/misc/licensed-check.sh",
|
||||||
"licensed-generate": "src/misc/licensed-generate.sh"
|
"licensed-generate": "src/misc/licensed-generate.sh"
|
||||||
},
|
},
|
||||||
@ -27,29 +28,30 @@
|
|||||||
"url": "https://github.com/actions/checkout/issues"
|
"url": "https://github.com/actions/checkout/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/actions/checkout#readme",
|
"homepage": "https://github.com/actions/checkout#readme",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^3.0.1",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^3.0.0",
|
||||||
"@actions/github": "^6.0.0",
|
"@actions/github": "^9.1.1",
|
||||||
"@actions/io": "^1.1.3",
|
"@actions/io": "^3.0.2",
|
||||||
"@actions/tool-cache": "^2.0.1",
|
"@actions/tool-cache": "^4.0.0"
|
||||||
"uuid": "^9.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^20.12.12",
|
"@types/node": "^24.1.0",
|
||||||
"@types/uuid": "^9.0.8",
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
"@typescript-eslint/parser": "^8.54.0",
|
||||||
"@typescript-eslint/parser": "^7.9.0",
|
"@vercel/ncc": "^0.44.0",
|
||||||
"@vercel/ncc": "^0.38.1",
|
"eslint": "^9.39.2",
|
||||||
"eslint": "^8.57.0",
|
"eslint-plugin-github": "^6.0.0",
|
||||||
"eslint-plugin-github": "^4.10.2",
|
"eslint-plugin-jest": "^29.12.1",
|
||||||
"eslint-plugin-jest": "^28.8.2",
|
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-circus": "^29.7.0",
|
"js-yaml": "^4.2.0",
|
||||||
"js-yaml": "^4.1.0",
|
"prettier": "^3.8.4",
|
||||||
"prettier": "^3.3.3",
|
"ts-jest": "^29.4.11",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,12 +5,12 @@ import * as fs from 'fs'
|
|||||||
import * as io from '@actions/io'
|
import * as io from '@actions/io'
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as regexpHelper from './regexp-helper'
|
import * as regexpHelper from './regexp-helper.js'
|
||||||
import * as stateHelper from './state-helper'
|
import * as stateHelper from './state-helper.js'
|
||||||
import * as urlHelper from './url-helper'
|
import * as urlHelper from './url-helper.js'
|
||||||
import {v4 as uuid} from 'uuid'
|
import {randomUUID} from 'crypto'
|
||||||
import {IGitCommandManager} from './git-command-manager'
|
import {IGitCommandManager} from './git-command-manager.js'
|
||||||
import {IGitSourceSettings} from './git-source-settings'
|
import {IGitSourceSettings} from './git-source-settings.js'
|
||||||
|
|
||||||
const IS_WINDOWS = process.platform === 'win32'
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
const SSH_COMMAND_KEY = 'core.sshCommand'
|
const SSH_COMMAND_KEY = 'core.sshCommand'
|
||||||
@ -43,6 +43,7 @@ class GitAuthHelper {
|
|||||||
private sshKeyPath = ''
|
private sshKeyPath = ''
|
||||||
private sshKnownHostsPath = ''
|
private sshKnownHostsPath = ''
|
||||||
private temporaryHomePath = ''
|
private temporaryHomePath = ''
|
||||||
|
private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
gitCommandManager: IGitCommandManager,
|
gitCommandManager: IGitCommandManager,
|
||||||
@ -89,7 +90,7 @@ class GitAuthHelper {
|
|||||||
// Create a temp home directory
|
// Create a temp home directory
|
||||||
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
||||||
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||||
const uniqueId = uuid()
|
const uniqueId = randomUUID()
|
||||||
this.temporaryHomePath = path.join(runnerTemp, uniqueId)
|
this.temporaryHomePath = path.join(runnerTemp, uniqueId)
|
||||||
await fs.promises.mkdir(this.temporaryHomePath, {recursive: true})
|
await fs.promises.mkdir(this.temporaryHomePath, {recursive: true})
|
||||||
|
|
||||||
@ -126,16 +127,21 @@ class GitAuthHelper {
|
|||||||
|
|
||||||
async configureGlobalAuth(): Promise<void> {
|
async configureGlobalAuth(): Promise<void> {
|
||||||
// 'configureTempGlobalConfig' noops if already set, just returns the path
|
// 'configureTempGlobalConfig' noops if already set, just returns the path
|
||||||
const newGitConfigPath = await this.configureTempGlobalConfig()
|
await this.configureTempGlobalConfig()
|
||||||
try {
|
try {
|
||||||
// Configure the token
|
// Configure the token
|
||||||
await this.configureToken(newGitConfigPath, true)
|
await this.configureToken(true)
|
||||||
|
|
||||||
// Configure HTTPS instead of SSH
|
// Configure HTTPS instead of SSH
|
||||||
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
||||||
if (!this.settings.sshKey) {
|
if (!this.settings.sshKey) {
|
||||||
for (const insteadOfValue of this.insteadOfValues) {
|
for (const insteadOfValue of this.insteadOfValues) {
|
||||||
await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
|
await this.git.config(
|
||||||
|
this.insteadOfKey,
|
||||||
|
insteadOfValue,
|
||||||
|
true, // globalConfig?
|
||||||
|
true // add?
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -150,24 +156,60 @@ class GitAuthHelper {
|
|||||||
|
|
||||||
async configureSubmoduleAuth(): Promise<void> {
|
async configureSubmoduleAuth(): Promise<void> {
|
||||||
// Remove possible previous HTTPS instead of SSH
|
// Remove possible previous HTTPS instead of SSH
|
||||||
await this.removeGitConfig(this.insteadOfKey, true)
|
await this.removeSubmoduleGitConfig(this.insteadOfKey)
|
||||||
|
|
||||||
if (this.settings.persistCredentials) {
|
if (this.settings.persistCredentials) {
|
||||||
// Configure a placeholder value. This approach avoids the credential being captured
|
// Get the credentials config file path in RUNNER_TEMP
|
||||||
// by process creation audit events, which are commonly logged. For more information,
|
const credentialsConfigPath = this.getCredentialsConfigPath()
|
||||||
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
|
||||||
const output = await this.git.submoduleForeach(
|
// Container credentials config path
|
||||||
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
|
const containerCredentialsPath = path.posix.join(
|
||||||
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
|
'/github/runner_temp',
|
||||||
|
path.basename(credentialsConfigPath)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get submodule config file paths.
|
||||||
|
const configPaths = await this.git.getSubmoduleConfigPaths(
|
||||||
this.settings.nestedSubmodules
|
this.settings.nestedSubmodules
|
||||||
)
|
)
|
||||||
|
|
||||||
// Replace the placeholder
|
// For each submodule, configure includeIf entries pointing to the shared credentials file.
|
||||||
const configPaths: string[] =
|
// Configure both host and container paths to support Docker container actions.
|
||||||
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
|
|
||||||
for (const configPath of configPaths) {
|
for (const configPath of configPaths) {
|
||||||
core.debug(`Replacing token placeholder in '${configPath}'`)
|
// Submodule Git directory
|
||||||
await this.replaceTokenPlaceholder(configPath)
|
let submoduleGitDir = path.dirname(configPath) // The config file is at .git/modules/submodule-name/config
|
||||||
|
submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||||
|
|
||||||
|
// Configure host includeIf
|
||||||
|
await this.git.config(
|
||||||
|
`includeIf.gitdir:${submoduleGitDir}.path`,
|
||||||
|
credentialsConfigPath,
|
||||||
|
false, // globalConfig?
|
||||||
|
false, // add?
|
||||||
|
configPath
|
||||||
|
)
|
||||||
|
|
||||||
|
// Container submodule git directory
|
||||||
|
const githubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||||
|
assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
|
||||||
|
let relativeSubmoduleGitDir = path.relative(
|
||||||
|
githubWorkspace,
|
||||||
|
submoduleGitDir
|
||||||
|
)
|
||||||
|
relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||||
|
const containerSubmoduleGitDir = path.posix.join(
|
||||||
|
'/github/workspace',
|
||||||
|
relativeSubmoduleGitDir
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure container includeIf
|
||||||
|
await this.git.config(
|
||||||
|
`includeIf.gitdir:${containerSubmoduleGitDir}.path`,
|
||||||
|
containerCredentialsPath,
|
||||||
|
false, // globalConfig?
|
||||||
|
false, // add?
|
||||||
|
configPath
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.settings.sshKey) {
|
if (this.settings.sshKey) {
|
||||||
@ -201,6 +243,10 @@ class GitAuthHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures SSH authentication by writing the SSH key and known hosts,
|
||||||
|
* and setting up the GIT_SSH_COMMAND environment variable.
|
||||||
|
*/
|
||||||
private async configureSsh(): Promise<void> {
|
private async configureSsh(): Promise<void> {
|
||||||
if (!this.settings.sshKey) {
|
if (!this.settings.sshKey) {
|
||||||
return
|
return
|
||||||
@ -209,7 +255,7 @@ class GitAuthHelper {
|
|||||||
// Write key
|
// Write key
|
||||||
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
||||||
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||||
const uniqueId = uuid()
|
const uniqueId = randomUUID()
|
||||||
this.sshKeyPath = path.join(runnerTemp, uniqueId)
|
this.sshKeyPath = path.join(runnerTemp, uniqueId)
|
||||||
stateHelper.setSshKeyPath(this.sshKeyPath)
|
stateHelper.setSshKeyPath(this.sshKeyPath)
|
||||||
await fs.promises.mkdir(runnerTemp, {recursive: true})
|
await fs.promises.mkdir(runnerTemp, {recursive: true})
|
||||||
@ -272,57 +318,127 @@ class GitAuthHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async configureToken(
|
/**
|
||||||
configPath?: string,
|
* Configures token-based authentication by creating a credentials config file
|
||||||
globalConfig?: boolean
|
* and setting up includeIf entries to reference it.
|
||||||
): Promise<void> {
|
* @param globalConfig Whether to configure global config instead of local
|
||||||
// Validate args
|
*/
|
||||||
assert.ok(
|
private async configureToken(globalConfig?: boolean): Promise<void> {
|
||||||
(configPath && globalConfig) || (!configPath && !globalConfig),
|
// Get the credentials config file path in RUNNER_TEMP
|
||||||
'Unexpected configureToken parameter combinations'
|
const credentialsConfigPath = this.getCredentialsConfigPath()
|
||||||
)
|
|
||||||
|
|
||||||
// Default config path
|
// Write placeholder to the separate credentials config file using git config.
|
||||||
if (!configPath && !globalConfig) {
|
// This approach avoids the credential being captured by process creation audit events,
|
||||||
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
|
// which are commonly logged. For more information, refer to
|
||||||
}
|
// https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
||||||
|
|
||||||
// Configure a placeholder value. This approach avoids the credential being captured
|
|
||||||
// by process creation audit events, which are commonly logged. For more information,
|
|
||||||
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
|
||||||
await this.git.config(
|
await this.git.config(
|
||||||
this.tokenConfigKey,
|
this.tokenConfigKey,
|
||||||
this.tokenPlaceholderConfigValue,
|
this.tokenPlaceholderConfigValue,
|
||||||
globalConfig
|
false, // globalConfig?
|
||||||
|
false, // add?
|
||||||
|
credentialsConfigPath
|
||||||
)
|
)
|
||||||
|
|
||||||
// Replace the placeholder
|
// Replace the placeholder in the credentials config file
|
||||||
await this.replaceTokenPlaceholder(configPath || '')
|
let content = (await fs.promises.readFile(credentialsConfigPath)).toString()
|
||||||
}
|
|
||||||
|
|
||||||
private async replaceTokenPlaceholder(configPath: string): Promise<void> {
|
|
||||||
assert.ok(configPath, 'configPath is not defined')
|
|
||||||
let content = (await fs.promises.readFile(configPath)).toString()
|
|
||||||
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
|
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
|
||||||
if (
|
if (
|
||||||
placeholderIndex < 0 ||
|
placeholderIndex < 0 ||
|
||||||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
|
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
|
||||||
) {
|
) {
|
||||||
throw new Error(`Unable to replace auth placeholder in ${configPath}`)
|
throw new Error(
|
||||||
|
`Unable to replace auth placeholder in ${credentialsConfigPath}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
|
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
this.tokenPlaceholderConfigValue,
|
this.tokenPlaceholderConfigValue,
|
||||||
this.tokenConfigValue
|
this.tokenConfigValue
|
||||||
)
|
)
|
||||||
await fs.promises.writeFile(configPath, content)
|
await fs.promises.writeFile(credentialsConfigPath, content)
|
||||||
|
|
||||||
|
// Add include or includeIf to reference the credentials config
|
||||||
|
if (globalConfig) {
|
||||||
|
// Global config file is temporary
|
||||||
|
await this.git.config(
|
||||||
|
'include.path',
|
||||||
|
credentialsConfigPath,
|
||||||
|
true // globalConfig?
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Host git directory
|
||||||
|
let gitDir = path.join(this.git.getWorkingDirectory(), '.git')
|
||||||
|
gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||||
|
|
||||||
|
// Configure host includeIf
|
||||||
|
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
|
||||||
|
await this.git.config(hostIncludeKey, credentialsConfigPath)
|
||||||
|
|
||||||
|
// Configure host includeIf for worktrees
|
||||||
|
const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`
|
||||||
|
await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath)
|
||||||
|
|
||||||
|
// Container git directory
|
||||||
|
const workingDirectory = this.git.getWorkingDirectory()
|
||||||
|
const githubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||||
|
assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
|
||||||
|
let relativePath = path.relative(githubWorkspace, workingDirectory)
|
||||||
|
relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||||
|
const containerGitDir = path.posix.join(
|
||||||
|
'/github/workspace',
|
||||||
|
relativePath,
|
||||||
|
'.git'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Container credentials config path
|
||||||
|
const containerCredentialsPath = path.posix.join(
|
||||||
|
'/github/runner_temp',
|
||||||
|
path.basename(credentialsConfigPath)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure container includeIf
|
||||||
|
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
|
||||||
|
await this.git.config(containerIncludeKey, containerCredentialsPath)
|
||||||
|
|
||||||
|
// Configure container includeIf for worktrees
|
||||||
|
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`
|
||||||
|
await this.git.config(
|
||||||
|
containerWorktreeIncludeKey,
|
||||||
|
containerCredentialsPath
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or creates the path to the credentials config file in RUNNER_TEMP.
|
||||||
|
* @returns The absolute path to the credentials config file
|
||||||
|
*/
|
||||||
|
private getCredentialsConfigPath(): string {
|
||||||
|
if (this.credentialsConfigPath) {
|
||||||
|
return this.credentialsConfigPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
||||||
|
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||||
|
|
||||||
|
// Create a unique filename for this checkout instance
|
||||||
|
const configFileName = `git-credentials-${randomUUID()}.config`
|
||||||
|
this.credentialsConfigPath = path.join(runnerTemp, configFileName)
|
||||||
|
|
||||||
|
core.debug(`Credentials config path: ${this.credentialsConfigPath}`)
|
||||||
|
return this.credentialsConfigPath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes SSH authentication configuration by cleaning up SSH keys,
|
||||||
|
* known hosts files, and SSH command configurations.
|
||||||
|
*/
|
||||||
private async removeSsh(): Promise<void> {
|
private async removeSsh(): Promise<void> {
|
||||||
// SSH key
|
// SSH key
|
||||||
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
|
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
|
||||||
if (keyPath) {
|
if (keyPath) {
|
||||||
try {
|
try {
|
||||||
|
core.info(`Removing SSH key '${keyPath}'`)
|
||||||
await io.rmRF(keyPath)
|
await io.rmRF(keyPath)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
core.debug(`${(err as any)?.message ?? err}`)
|
core.debug(`${(err as any)?.message ?? err}`)
|
||||||
@ -335,88 +451,91 @@ class GitAuthHelper {
|
|||||||
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
|
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
|
||||||
if (knownHostsPath) {
|
if (knownHostsPath) {
|
||||||
try {
|
try {
|
||||||
|
core.info(`Removing SSH known hosts '${knownHostsPath}'`)
|
||||||
await io.rmRF(knownHostsPath)
|
await io.rmRF(knownHostsPath)
|
||||||
} catch {
|
} catch (err) {
|
||||||
// Intentionally empty
|
core.debug(`${(err as any)?.message ?? err}`)
|
||||||
|
core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSH command
|
// SSH command
|
||||||
|
core.info('Removing SSH command configuration')
|
||||||
await this.removeGitConfig(SSH_COMMAND_KEY)
|
await this.removeGitConfig(SSH_COMMAND_KEY)
|
||||||
|
await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes token-based authentication by cleaning up HTTP headers,
|
||||||
|
* includeIf entries, and credentials config files.
|
||||||
|
*/
|
||||||
private async removeToken(): Promise<void> {
|
private async removeToken(): Promise<void> {
|
||||||
// Remove HTTP extra header from local git config and submodule configs
|
// Remove HTTP extra header
|
||||||
|
core.info('Removing HTTP extra header')
|
||||||
await this.removeGitConfig(this.tokenConfigKey)
|
await this.removeGitConfig(this.tokenConfigKey)
|
||||||
|
await this.removeSubmoduleGitConfig(this.tokenConfigKey)
|
||||||
|
|
||||||
//
|
// Collect credentials config paths that need to be removed
|
||||||
// Cleanup actions/checkout@v6 style credentials
|
const credentialsPaths = new Set<string>()
|
||||||
//
|
|
||||||
const skipV6Cleanup = process.env['ACTIONS_CHECKOUT_SKIP_V6_CLEANUP']
|
// Remove includeIf entries that point to git-credentials-*.config files
|
||||||
if (skipV6Cleanup === '1' || skipV6Cleanup?.toLowerCase() === 'true') {
|
core.info('Removing includeIf entries pointing to credentials config files')
|
||||||
core.debug(
|
const mainCredentialsPaths = await this.removeIncludeIfCredentials()
|
||||||
'Skipping v6 style cleanup due to ACTIONS_CHECKOUT_SKIP_V6_CLEANUP'
|
mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
|
||||||
)
|
|
||||||
return
|
// 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Remove credentials config files
|
||||||
// Collect credentials config paths that need to be removed
|
for (const credentialsPath of credentialsPaths) {
|
||||||
const credentialsPaths = new Set<string>()
|
// Only remove credentials config files if they are under RUNNER_TEMP
|
||||||
|
const runnerTemp = process.env['RUNNER_TEMP']
|
||||||
// Remove includeIf entries that point to git-credentials-*.config files
|
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||||
const mainCredentialsPaths = await this.removeIncludeIfCredentials()
|
if (credentialsPath.startsWith(runnerTemp)) {
|
||||||
mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
|
try {
|
||||||
|
core.info(`Removing credentials config '${credentialsPath}'`)
|
||||||
// Remove submodule includeIf entries that point to git-credentials-*.config files
|
await io.rmRF(credentialsPath)
|
||||||
try {
|
} catch (err) {
|
||||||
const submoduleConfigPaths =
|
core.debug(`${(err as any)?.message ?? err}`)
|
||||||
await this.git.getSubmoduleConfigPaths(true)
|
core.warning(
|
||||||
for (const configPath of submoduleConfigPaths) {
|
`Failed to remove credentials config '${credentialsPath}'`
|
||||||
const submoduleCredentialsPaths =
|
)
|
||||||
await this.removeIncludeIfCredentials(configPath)
|
|
||||||
submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} else {
|
||||||
core.debug(`Unable to get submodule config paths: ${err}`)
|
core.debug(
|
||||||
|
`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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']
|
|
||||||
if (runnerTemp && credentialsPath.startsWith(runnerTemp)) {
|
|
||||||
try {
|
|
||||||
await io.rmRF(credentialsPath)
|
|
||||||
} catch (err) {
|
|
||||||
core.debug(
|
|
||||||
`Failed to remove credentials config '${credentialsPath}': ${err}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
core.debug(`Failed to cleanup v6 style credentials: ${err}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removeGitConfig(
|
/**
|
||||||
configKey: string,
|
* Removes a git config key from the local repository config.
|
||||||
submoduleOnly: boolean = false
|
* @param configKey The git config key to remove
|
||||||
): Promise<void> {
|
*/
|
||||||
if (!submoduleOnly) {
|
private async removeGitConfig(configKey: string): Promise<void> {
|
||||||
if (
|
if (
|
||||||
(await this.git.configExists(configKey)) &&
|
(await this.git.configExists(configKey)) &&
|
||||||
!(await this.git.tryConfigUnset(configKey))
|
!(await this.git.tryConfigUnset(configKey))
|
||||||
) {
|
) {
|
||||||
// Load the config contents
|
// Load the config contents
|
||||||
core.warning(`Failed to remove '${configKey}' from the git config`)
|
core.warning(`Failed to remove '${configKey}' from the git config`)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a git config key from all submodule configs.
|
||||||
|
* @param configKey The git config key to remove
|
||||||
|
*/
|
||||||
|
private async removeSubmoduleGitConfig(configKey: string): Promise<void> {
|
||||||
const pattern = regexpHelper.escape(configKey)
|
const pattern = regexpHelper.escape(configKey)
|
||||||
await this.git.submoduleForeach(
|
await this.git.submoduleForeach(
|
||||||
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
|
// Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
|
||||||
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
|
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
@ -424,7 +543,6 @@ class GitAuthHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes includeIf entries that point to git-credentials-*.config files.
|
* Removes includeIf entries that point to git-credentials-*.config files.
|
||||||
* This handles cleanup of credentials configured by newer versions of the action.
|
|
||||||
* @param configPath Optional path to a specific git config file to operate on
|
* @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
|
* @returns Array of unique credentials config file paths that were found and removed
|
||||||
*/
|
*/
|
||||||
@ -460,16 +578,18 @@ class GitAuthHelper {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Ignore errors - this is cleanup code
|
// Ignore errors - this is cleanup code
|
||||||
core.debug(
|
if (configPath) {
|
||||||
`Error during includeIf cleanup${configPath ? ` for ${configPath}` : ''}: ${err}`
|
core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`)
|
||||||
)
|
} else {
|
||||||
|
core.debug(`Error during includeIf cleanup: ${err}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(credentialsPaths)
|
return Array.from(credentialsPaths)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests if a path matches the git-credentials-*.config pattern used by newer versions.
|
* Tests if a path matches the git-credentials-*.config pattern.
|
||||||
* @param path The path to test
|
* @param path The path to test
|
||||||
* @returns True if the path matches the credentials config pattern
|
* @returns True if the path matches the credentials config pattern
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as exec from '@actions/exec'
|
import * as exec from '@actions/exec'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as fshelper from './fs-helper'
|
import * as fshelper from './fs-helper.js'
|
||||||
import * as io from '@actions/io'
|
import * as io from '@actions/io'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as refHelper from './ref-helper'
|
import * as refHelper from './ref-helper.js'
|
||||||
import * as regexpHelper from './regexp-helper'
|
import * as regexpHelper from './regexp-helper.js'
|
||||||
import * as retryHelper from './retry-helper'
|
import * as retryHelper from './retry-helper.js'
|
||||||
import {GitVersion} from './git-version'
|
import {GitVersion} from './git-version.js'
|
||||||
|
|
||||||
// Auth header not supported before 2.9
|
// Auth header not supported before 2.9
|
||||||
// Wire protocol v2 not supported before 2.18
|
// Wire protocol v2 not supported before 2.18
|
||||||
@ -28,7 +28,8 @@ export interface IGitCommandManager {
|
|||||||
configKey: string,
|
configKey: string,
|
||||||
configValue: string,
|
configValue: string,
|
||||||
globalConfig?: boolean,
|
globalConfig?: boolean,
|
||||||
add?: boolean
|
add?: boolean,
|
||||||
|
configFile?: string
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
|
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
|
||||||
fetch(
|
fetch(
|
||||||
@ -36,14 +37,13 @@ export interface IGitCommandManager {
|
|||||||
options: {
|
options: {
|
||||||
filter?: string
|
filter?: string
|
||||||
fetchDepth?: number
|
fetchDepth?: number
|
||||||
fetchTags?: boolean
|
|
||||||
showProgress?: boolean
|
showProgress?: boolean
|
||||||
}
|
}
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
getDefaultBranch(repositoryUrl: string): Promise<string>
|
getDefaultBranch(repositoryUrl: string): Promise<string>
|
||||||
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
|
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
|
||||||
getWorkingDirectory(): string
|
getWorkingDirectory(): string
|
||||||
init(): Promise<void>
|
init(objectFormat?: string): Promise<void>
|
||||||
isDetached(): Promise<boolean>
|
isDetached(): Promise<boolean>
|
||||||
lfsFetch(ref: string): Promise<void>
|
lfsFetch(ref: string): Promise<void>
|
||||||
lfsInstall(): Promise<void>
|
lfsInstall(): Promise<void>
|
||||||
@ -240,9 +240,15 @@ class GitCommandManager {
|
|||||||
configKey: string,
|
configKey: string,
|
||||||
configValue: string,
|
configValue: string,
|
||||||
globalConfig?: boolean,
|
globalConfig?: boolean,
|
||||||
add?: boolean
|
add?: boolean,
|
||||||
|
configFile?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const args: string[] = ['config', globalConfig ? '--global' : '--local']
|
const args: string[] = ['config']
|
||||||
|
if (configFile) {
|
||||||
|
args.push('--file', configFile)
|
||||||
|
} else {
|
||||||
|
args.push(globalConfig ? '--global' : '--local')
|
||||||
|
}
|
||||||
if (add) {
|
if (add) {
|
||||||
args.push('--add')
|
args.push('--add')
|
||||||
}
|
}
|
||||||
@ -273,14 +279,13 @@ class GitCommandManager {
|
|||||||
options: {
|
options: {
|
||||||
filter?: string
|
filter?: string
|
||||||
fetchDepth?: number
|
fetchDepth?: number
|
||||||
fetchTags?: boolean
|
|
||||||
showProgress?: boolean
|
showProgress?: boolean
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const args = ['-c', 'protocol.version=2', 'fetch']
|
const args = ['-c', 'protocol.version=2', 'fetch']
|
||||||
if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
|
// Always use --no-tags for explicit control over tag fetching
|
||||||
args.push('--no-tags')
|
// Tags are fetched explicitly via refspec when needed
|
||||||
}
|
args.push('--no-tags')
|
||||||
|
|
||||||
args.push('--prune', '--no-recurse-submodules')
|
args.push('--prune', '--no-recurse-submodules')
|
||||||
if (options.showProgress) {
|
if (options.showProgress) {
|
||||||
@ -359,8 +364,14 @@ class GitCommandManager {
|
|||||||
return this.workingDirectory
|
return this.workingDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(objectFormat?: string): Promise<void> {
|
||||||
await this.execGit(['init', this.workingDirectory])
|
const args = ['init']
|
||||||
|
if (objectFormat === 'sha256') {
|
||||||
|
args.push('--object-format=sha256')
|
||||||
|
}
|
||||||
|
args.push(this.workingDirectory)
|
||||||
|
|
||||||
|
await this.execGit(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
async isDetached(): Promise<boolean> {
|
async isDetached(): Promise<boolean> {
|
||||||
@ -499,7 +510,7 @@ class GitCommandManager {
|
|||||||
} else {
|
} else {
|
||||||
args.push(globalConfig ? '--global' : '--local')
|
args.push(globalConfig ? '--global' : '--local')
|
||||||
}
|
}
|
||||||
args.push('--unset', configKey, configValue)
|
args.push('--unset', configKey, regexpHelper.escape(configValue))
|
||||||
|
|
||||||
const output = await this.execGit(args, true)
|
const output = await this.execGit(args, true)
|
||||||
return output.exitCode === 0
|
return output.exitCode === 0
|
||||||
@ -723,7 +734,19 @@ class GitCommandManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Set the user agent
|
// Set the user agent
|
||||||
const gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
|
let gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
|
||||||
|
|
||||||
|
// Append orchestration ID if set
|
||||||
|
const orchId = process.env['ACTIONS_ORCHESTRATION_ID']
|
||||||
|
if (orchId) {
|
||||||
|
// Sanitize the orchestration ID to ensure it contains only valid characters
|
||||||
|
// Valid characters: 0-9, a-z, _, -, .
|
||||||
|
const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_')
|
||||||
|
if (sanitizedId) {
|
||||||
|
gitHttpUserAgent = `${gitHttpUserAgent} actions_orchestration_id/${sanitizedId}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
|
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
|
||||||
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
|
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import * as assert from 'assert'
|
import * as assert from 'assert'
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as fsHelper from './fs-helper'
|
import * as fsHelper from './fs-helper.js'
|
||||||
import * as io from '@actions/io'
|
import * as io from '@actions/io'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {IGitCommandManager} from './git-command-manager'
|
import {IGitCommandManager} from './git-command-manager.js'
|
||||||
|
|
||||||
export async function prepareExistingDirectory(
|
export async function prepareExistingDirectory(
|
||||||
git: IGitCommandManager | undefined,
|
git: IGitCommandManager | undefined,
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as fsHelper from './fs-helper'
|
import * as fsHelper from './fs-helper.js'
|
||||||
import * as gitAuthHelper from './git-auth-helper'
|
import * as gitAuthHelper from './git-auth-helper.js'
|
||||||
import * as gitCommandManager from './git-command-manager'
|
import * as gitCommandManager from './git-command-manager.js'
|
||||||
import * as gitDirectoryHelper from './git-directory-helper'
|
import * as gitDirectoryHelper from './git-directory-helper.js'
|
||||||
import * as githubApiHelper from './github-api-helper'
|
import * as githubApiHelper from './github-api-helper.js'
|
||||||
import * as io from '@actions/io'
|
import * as io from '@actions/io'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as refHelper from './ref-helper'
|
import * as refHelper from './ref-helper.js'
|
||||||
import * as stateHelper from './state-helper'
|
import * as stateHelper from './state-helper.js'
|
||||||
import * as urlHelper from './url-helper'
|
import * as urlHelper from './url-helper.js'
|
||||||
import {
|
import {
|
||||||
MinimumGitSparseCheckoutVersion,
|
MinimumGitSparseCheckoutVersion,
|
||||||
IGitCommandManager
|
IGitCommandManager
|
||||||
} from './git-command-manager'
|
} from './git-command-manager.js'
|
||||||
import {IGitSourceSettings} from './git-source-settings'
|
import {IGitSourceSettings} from './git-source-settings.js'
|
||||||
|
|
||||||
export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||||
// Repository URL
|
// Repository URL
|
||||||
@ -109,8 +109,25 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
|||||||
if (
|
if (
|
||||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||||
) {
|
) {
|
||||||
|
core.startGroup('Determining repository object format')
|
||||||
|
const objectFormatResult =
|
||||||
|
await githubApiHelper.tryGetRepositoryObjectFormat(
|
||||||
|
settings.authToken,
|
||||||
|
settings.repositoryOwner,
|
||||||
|
settings.repositoryName,
|
||||||
|
settings.githubServerUrl,
|
||||||
|
settings.commit
|
||||||
|
)
|
||||||
|
const objectFormat = objectFormatResult.succeeded
|
||||||
|
? objectFormatResult.format
|
||||||
|
: ''
|
||||||
|
if (objectFormat === 'sha256') {
|
||||||
|
core.info('Detected SHA-256 repository object format')
|
||||||
|
}
|
||||||
|
core.endGroup()
|
||||||
|
|
||||||
core.startGroup('Initializing the repository')
|
core.startGroup('Initializing the repository')
|
||||||
await git.init()
|
await git.init(objectFormat)
|
||||||
await git.remoteAdd('origin', repositoryUrl)
|
await git.remoteAdd('origin', repositoryUrl)
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
@ -159,7 +176,6 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
|||||||
const fetchOptions: {
|
const fetchOptions: {
|
||||||
filter?: string
|
filter?: string
|
||||||
fetchDepth?: number
|
fetchDepth?: number
|
||||||
fetchTags?: boolean
|
|
||||||
showProgress?: boolean
|
showProgress?: boolean
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
@ -182,12 +198,35 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
|||||||
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
|
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
|
||||||
refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
||||||
await git.fetch(refSpec, fetchOptions)
|
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 {
|
} else {
|
||||||
fetchOptions.fetchDepth = settings.fetchDepth
|
fetchOptions.fetchDepth = settings.fetchDepth
|
||||||
fetchOptions.fetchTags = settings.fetchTags
|
const refSpec = refHelper.getRefSpec(
|
||||||
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
settings.ref,
|
||||||
|
settings.commit,
|
||||||
|
settings.fetchTags
|
||||||
|
)
|
||||||
await git.fetch(refSpec, fetchOptions)
|
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()
|
core.endGroup()
|
||||||
|
|
||||||
|
|||||||
@ -4,13 +4,18 @@ import * as fs from 'fs'
|
|||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import * as io from '@actions/io'
|
import * as io from '@actions/io'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as retryHelper from './retry-helper'
|
import * as retryHelper from './retry-helper.js'
|
||||||
import * as toolCache from '@actions/tool-cache'
|
import * as toolCache from '@actions/tool-cache'
|
||||||
import {v4 as uuid} from 'uuid'
|
import {randomUUID} from 'crypto'
|
||||||
import {getServerApiUrl} from './url-helper'
|
import {getServerApiUrl} from './url-helper.js'
|
||||||
|
|
||||||
const IS_WINDOWS = process.platform === 'win32'
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
|
export interface RepositoryObjectFormatResult {
|
||||||
|
format: string
|
||||||
|
succeeded: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export async function downloadRepository(
|
export async function downloadRepository(
|
||||||
authToken: string,
|
authToken: string,
|
||||||
owner: string,
|
owner: string,
|
||||||
@ -34,7 +39,7 @@ export async function downloadRepository(
|
|||||||
|
|
||||||
// Write archive to disk
|
// Write archive to disk
|
||||||
core.info('Writing archive to disk')
|
core.info('Writing archive to disk')
|
||||||
const uniqueId = uuid()
|
const uniqueId = randomUUID()
|
||||||
const archivePath = IS_WINDOWS
|
const archivePath = IS_WINDOWS
|
||||||
? path.join(repositoryPath, `${uniqueId}.zip`)
|
? path.join(repositoryPath, `${uniqueId}.zip`)
|
||||||
: path.join(repositoryPath, `${uniqueId}.tar.gz`)
|
: path.join(repositoryPath, `${uniqueId}.tar.gz`)
|
||||||
@ -122,6 +127,53 @@ export async function getDefaultBranch(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function tryGetRepositoryObjectFormat(
|
||||||
|
authToken: string,
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
baseUrl?: string,
|
||||||
|
commit?: string
|
||||||
|
): Promise<RepositoryObjectFormatResult> {
|
||||||
|
const commitFormat = getObjectFormat(commit)
|
||||||
|
if (commitFormat) {
|
||||||
|
return {format: commitFormat, succeeded: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const octokit = github.getOctokit(authToken, {
|
||||||
|
baseUrl: getServerApiUrl(baseUrl)
|
||||||
|
})
|
||||||
|
const response = await octokit.request(
|
||||||
|
'GET /repos/{owner}/{repo}/hash-algorithm',
|
||||||
|
{owner, repo}
|
||||||
|
)
|
||||||
|
const hashAlgorithm = response.data.hash_algorithm
|
||||||
|
if (hashAlgorithm === 'sha256' || hashAlgorithm === 'sha1') {
|
||||||
|
return {format: hashAlgorithm, succeeded: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug(
|
||||||
|
'Unable to determine repository object format from hash-algorithm endpoint'
|
||||||
|
)
|
||||||
|
return {format: '', succeeded: false}
|
||||||
|
} catch (err) {
|
||||||
|
core.debug(
|
||||||
|
`Unable to determine repository object format from hash-algorithm endpoint: ${(err as any)?.message ?? err}`
|
||||||
|
)
|
||||||
|
return {format: '', succeeded: false}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getObjectFormat(sha?: string): string {
|
||||||
|
if (/^[0-9a-fA-F]{64}$/.test(sha || '')) {
|
||||||
|
return 'sha256'
|
||||||
|
}
|
||||||
|
if (/^[0-9a-fA-F]{40}$/.test(sha || '')) {
|
||||||
|
return 'sha1'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadArchive(
|
async function downloadArchive(
|
||||||
authToken: string,
|
authToken: string,
|
||||||
owner: string,
|
owner: string,
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as fsHelper from './fs-helper'
|
import * as fsHelper from './fs-helper.js'
|
||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as unsafePrCheckoutHelper from './unsafe-pr-checkout-helper'
|
import * as unsafePrCheckoutHelper from './unsafe-pr-checkout-helper.js'
|
||||||
import * as workflowContextHelper from './workflow-context-helper'
|
import * as workflowContextHelper from './workflow-context-helper.js'
|
||||||
import {IGitSourceSettings} from './git-source-settings'
|
import {IGitSourceSettings} from './git-source-settings.js'
|
||||||
|
|
||||||
export async function getInputs(): Promise<IGitSourceSettings> {
|
export async function getInputs(): Promise<IGitSourceSettings> {
|
||||||
const result = {} as unknown as IGitSourceSettings
|
const result = {} as unknown as IGitSourceSettings
|
||||||
@ -89,7 +89,7 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SHA?
|
// SHA?
|
||||||
else if (asciiTrimmedRef.match(/^[0-9a-fA-F]{40}$/)) {
|
else if (asciiTrimmedRef.match(/^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$/)) {
|
||||||
result.commit = asciiTrimmedRef
|
result.commit = asciiTrimmedRef
|
||||||
result.ref = ''
|
result.ref = ''
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/main.ts
18
src/main.ts
@ -1,9 +1,11 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as coreCommand from '@actions/core/lib/command'
|
import * as gitSourceProvider from './git-source-provider.js'
|
||||||
import * as gitSourceProvider from './git-source-provider'
|
import * as inputHelper from './input-helper.js'
|
||||||
import * as inputHelper from './input-helper'
|
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as stateHelper from './state-helper'
|
import * as stateHelper from './state-helper.js'
|
||||||
|
import {fileURLToPath} from 'url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@ -11,10 +13,8 @@ async function run(): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Register problem matcher
|
// Register problem matcher
|
||||||
coreCommand.issueCommand(
|
core.info(
|
||||||
'add-matcher',
|
`::add-matcher::${path.join(__dirname, 'problem-matcher.json')}`
|
||||||
{},
|
|
||||||
path.join(__dirname, 'problem-matcher.json')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get sources
|
// Get sources
|
||||||
@ -22,7 +22,7 @@ async function run(): Promise<void> {
|
|||||||
core.setOutput('ref', sourceSettings.ref)
|
core.setOutput('ref', sourceSettings.ref)
|
||||||
} finally {
|
} finally {
|
||||||
// Unregister problem matcher
|
// Unregister problem matcher
|
||||||
coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '')
|
core.info('::remove-matcher owner=checkout-git::')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(`${(error as any)?.message ?? error}`)
|
core.setFailed(`${(error as any)?.message ?? error}`)
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import * as fs from 'fs'
|
|||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as yaml from 'js-yaml'
|
import * as yaml from 'js-yaml'
|
||||||
|
import {fileURLToPath} from 'url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
//
|
//
|
||||||
// SUMMARY
|
// SUMMARY
|
||||||
@ -120,7 +123,7 @@ function updateUsage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateUsage(
|
updateUsage(
|
||||||
'actions/checkout@v4',
|
'actions/checkout@v7',
|
||||||
path.join(__dirname, '..', '..', 'action.yml'),
|
path.join(__dirname, '..', '..', 'action.yml'),
|
||||||
path.join(__dirname, '..', '..', 'README.md')
|
path.join(__dirname, '..', '..', 'README.md')
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import {IGitCommandManager} from './git-command-manager'
|
import {IGitCommandManager} from './git-command-manager.js'
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import {getServerApiUrl, isGhes} from './url-helper'
|
import {getServerApiUrl, isGhes} from './url-helper.js'
|
||||||
|
|
||||||
export const tagsRefSpec = '+refs/tags/*:refs/tags/*'
|
export const tagsRefSpec = '+refs/tags/*:refs/tags/*'
|
||||||
|
|
||||||
@ -76,55 +76,75 @@ export function getRefSpecForAllHistory(ref: string, commit: string): string[] {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRefSpec(ref: string, commit: string): string[] {
|
export function getRefSpec(
|
||||||
|
ref: string,
|
||||||
|
commit: string,
|
||||||
|
fetchTags?: boolean
|
||||||
|
): string[] {
|
||||||
if (!ref && !commit) {
|
if (!ref && !commit) {
|
||||||
throw new Error('Args ref and commit cannot both be empty')
|
throw new Error('Args ref and commit cannot both be empty')
|
||||||
}
|
}
|
||||||
|
|
||||||
const upperRef = (ref || '').toUpperCase()
|
const upperRef = (ref || '').toUpperCase()
|
||||||
|
const result: string[] = []
|
||||||
|
|
||||||
|
// When fetchTags is true, always include the tags refspec
|
||||||
|
if (fetchTags) {
|
||||||
|
result.push(tagsRefSpec)
|
||||||
|
}
|
||||||
|
|
||||||
// SHA
|
// SHA
|
||||||
if (commit) {
|
if (commit) {
|
||||||
// refs/heads
|
// refs/heads
|
||||||
if (upperRef.startsWith('REFS/HEADS/')) {
|
if (upperRef.startsWith('REFS/HEADS/')) {
|
||||||
const branch = ref.substring('refs/heads/'.length)
|
const branch = ref.substring('refs/heads/'.length)
|
||||||
return [`+${commit}:refs/remotes/origin/${branch}`]
|
result.push(`+${commit}:refs/remotes/origin/${branch}`)
|
||||||
}
|
}
|
||||||
// refs/pull/
|
// refs/pull/
|
||||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||||
const branch = ref.substring('refs/pull/'.length)
|
const branch = ref.substring('refs/pull/'.length)
|
||||||
return [`+${commit}:refs/remotes/pull/${branch}`]
|
result.push(`+${commit}:refs/remotes/pull/${branch}`)
|
||||||
}
|
}
|
||||||
// refs/tags/
|
// refs/tags/
|
||||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||||
return [`+${commit}:${ref}`]
|
if (!fetchTags) {
|
||||||
|
result.push(`+${ref}:${ref}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Otherwise no destination ref
|
// Otherwise no destination ref
|
||||||
else {
|
else {
|
||||||
return [commit]
|
result.push(commit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Unqualified ref, check for a matching branch or tag
|
// Unqualified ref, check for a matching branch or tag
|
||||||
else if (!upperRef.startsWith('REFS/')) {
|
else if (!upperRef.startsWith('REFS/')) {
|
||||||
return [
|
result.push(`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`)
|
||||||
`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`,
|
if (!fetchTags) {
|
||||||
`+refs/tags/${ref}*:refs/tags/${ref}*`
|
result.push(`+refs/tags/${ref}*:refs/tags/${ref}*`)
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
// refs/heads/
|
// refs/heads/
|
||||||
else if (upperRef.startsWith('REFS/HEADS/')) {
|
else if (upperRef.startsWith('REFS/HEADS/')) {
|
||||||
const branch = ref.substring('refs/heads/'.length)
|
const branch = ref.substring('refs/heads/'.length)
|
||||||
return [`+${ref}:refs/remotes/origin/${branch}`]
|
result.push(`+${ref}:refs/remotes/origin/${branch}`)
|
||||||
}
|
}
|
||||||
// refs/pull/
|
// refs/pull/
|
||||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||||
const branch = ref.substring('refs/pull/'.length)
|
const branch = ref.substring('refs/pull/'.length)
|
||||||
return [`+${ref}:refs/remotes/pull/${branch}`]
|
result.push(`+${ref}:refs/remotes/pull/${branch}`)
|
||||||
}
|
}
|
||||||
// refs/tags/
|
// refs/tags/
|
||||||
else {
|
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||||
return [`+${ref}:${ref}`]
|
if (!fetchTags) {
|
||||||
|
result.push(`+${ref}:${ref}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Other refs
|
||||||
|
else {
|
||||||
|
result.push(`+${ref}:${ref}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -170,8 +190,10 @@ export async function testRef(
|
|||||||
// refs/tags/
|
// refs/tags/
|
||||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||||
const tagName = ref.substring('refs/tags/'.length)
|
const tagName = ref.substring('refs/tags/'.length)
|
||||||
|
// Use ^{commit} to dereference annotated tags to their underlying commit
|
||||||
return (
|
return (
|
||||||
(await git.tagExists(tagName)) && commit === (await git.revParse(ref))
|
(await git.tagExists(tagName)) &&
|
||||||
|
commit === (await git.revParse(`${ref}^{commit}`))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Unexpected
|
// Unexpected
|
||||||
@ -236,7 +258,9 @@ export async function checkCommitInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract details from message
|
// Extract details from message
|
||||||
const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/)
|
const match = commitInfo.match(
|
||||||
|
/Merge ([0-9a-f]{40}|[0-9a-f]{64}) into ([0-9a-f]{40}|[0-9a-f]{64})/
|
||||||
|
)
|
||||||
if (!match) {
|
if (!match) {
|
||||||
core.debug('Unexpected message format')
|
core.debug('Unexpected message format')
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import {fromPayload} from './ref-helper'
|
import {fromPayload} from './ref-helper.js'
|
||||||
|
|
||||||
const PR_REF_PATTERN = /^refs\/pull\/[0-9]+\/(?:head|merge)$/
|
const PR_REF_PATTERN = /^refs\/pull\/[0-9]+\/(?:head|merge)$/
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import * as assert from 'assert'
|
import * as assert from 'assert'
|
||||||
import {URL} from 'url'
|
import {URL} from 'url'
|
||||||
import {IGitSourceSettings} from './git-source-settings'
|
import {IGitSourceSettings} from './git-source-settings.js'
|
||||||
|
|
||||||
export function getFetchUrl(settings: IGitSourceSettings): string {
|
export function getFetchUrl(settings: IGitSourceSettings): string {
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|||||||
@ -1,17 +1,13 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "ES2022",
|
||||||
"module": "commonjs",
|
"module": "NodeNext",
|
||||||
"lib": [
|
"moduleResolution": "NodeNext",
|
||||||
"es6"
|
|
||||||
],
|
|
||||||
"outDir": "./lib",
|
"outDir": "./lib",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"declaration": true,
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true
|
||||||
"skipLibCheck": true
|
|
||||||
},
|
},
|
||||||
"exclude": ["__test__", "lib", "node_modules"]
|
"exclude": ["__test__", "lib", "node_modules", "jest.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user