mirror of
https://github.com/actions/checkout.git
synced 2026-07-11 19:23:49 +08:00
Compare commits
44 Commits
v4.1.6
...
90dd5a9352
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90dd5a9352 | ||
|
|
14150cf66c | ||
|
|
8f2d56a328 | ||
|
|
7bbc85187c | ||
|
|
8e8c483db8 | ||
|
|
033fa0dc0b | ||
|
|
c2d88d3ecc | ||
|
|
1af3b93b68 | ||
|
|
71cf2267d8 | ||
|
|
069c695914 | ||
|
|
94b4159221 | ||
|
|
84bd6b238a | ||
|
|
93a0983ecb | ||
|
|
4b4c83ea7a | ||
|
|
2272602ba7 | ||
|
|
ff7abcd0c3 | ||
|
|
08c6903cd8 | ||
|
|
9f265659d3 | ||
|
|
08eba0b27e | ||
|
|
631c7dc4f8 | ||
|
|
8edcb1bdb4 | ||
|
|
09d2acae67 | ||
|
|
85e6279cec | ||
|
|
009b9ae9e4 | ||
|
|
cbb722410c | ||
|
|
3b9b8c884f | ||
|
|
11bd71901b | ||
|
|
e3d2460bbb | ||
|
|
163217dfcd | ||
|
|
eef61447b9 | ||
|
|
6b42224f41 | ||
|
|
de5a000abf | ||
|
|
d632683dd7 | ||
|
|
6d193bf280 | ||
|
|
db0cee9a51 | ||
|
|
b684943689 | ||
|
|
2d7d9f7ff5 | ||
|
|
9a9194f871 | ||
|
|
dd960bd3c3 | ||
|
|
692973e3d9 | ||
|
|
6ccd57f4c5 | ||
|
|
b17fe1e4d5 | ||
|
|
b80ff79f17 | ||
|
|
b1ec3021b8 |
121
.github/copilot-instructions.md
vendored
Normal file
121
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
# GitHub Actions Checkout - AI Coding Instructions
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is the official `actions/checkout` GitHub Action for checking out repositories in workflows. It's a TypeScript project that compiles to a single bundled JavaScript file (`dist/index.js`) and supports both git-based and REST API-based repository downloads.
|
||||
|
||||
## Architecture & Key Components
|
||||
|
||||
### Core Entry Points
|
||||
- **`src/main.ts`**: Main entry point with `run()` and `cleanup()` functions, determined by `stateHelper.IsPost`
|
||||
- **`src/git-source-provider.ts`**: Primary orchestrator for repository acquisition (git vs REST API fallback)
|
||||
- **`src/input-helper.ts`**: Input validation and GitHub Actions input processing
|
||||
- **`action.yml`**: Defines the action interface with comprehensive input/output schema
|
||||
|
||||
### Critical Data Flow
|
||||
1. `main.ts` → `inputHelper.getInputs()` → validates and transforms action inputs
|
||||
2. `main.ts` → `gitSourceProvider.getSource(settings)` → orchestrates repository download
|
||||
3. `git-source-provider.ts` decides: use Git CLI or fallback to GitHub REST API
|
||||
4. State management via `state-helper.ts` for POST action cleanup
|
||||
|
||||
### Authentication & Security Patterns
|
||||
- **Token-based auth**: Uses `@actions/core` to handle GitHub tokens securely
|
||||
- **SSH key management**: Configures temporary SSH keys in `git-auth-helper.ts`
|
||||
- **Safe directory**: Automatically configures `git config safe.directory` for container compatibility
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Essential Commands
|
||||
```bash
|
||||
npm ci # Install dependencies
|
||||
npm run build # TypeScript → JavaScript + bundle with ncc + generate docs
|
||||
npm run format # Prettier formatting
|
||||
npm run lint # ESLint validation
|
||||
npm test # Jest test suite
|
||||
```
|
||||
|
||||
### Build Process (Critical!)
|
||||
- **`npm run build`** runs: `tsc && ncc build && node lib/misc/generate-docs.js`
|
||||
- **Documentation sync**: `src/misc/generate-docs.ts` auto-updates README.md usage section from `action.yml`
|
||||
- **Bundling**: Uses `@vercel/ncc` to create single `dist/index.js` file
|
||||
- **Always run `npm run build` before commits** - the `dist/` directory must be up-to-date
|
||||
|
||||
### Testing Strategy
|
||||
- **Unit tests**: Jest tests in `__test__/` for all core modules
|
||||
- **Integration tests**: Shell scripts (`__test__/verify-*.sh`) test real git operations
|
||||
- **E2E tests**: `.github/workflows/test.yml` tests across OS matrix with actual GitHub repos
|
||||
|
||||
## Project-Specific Conventions
|
||||
|
||||
### TypeScript Patterns
|
||||
- **Interface-driven**: `IGitSourceSettings` centralizes all configuration
|
||||
- **Async/await**: All I/O operations use async patterns, not promises
|
||||
- **Error handling**: Use `core.setFailed()` for action failures, `core.warning()` for non-critical issues
|
||||
|
||||
### Git Operation Patterns
|
||||
```typescript
|
||||
// Check Git version and fallback pattern
|
||||
const git = await getGitCommandManager(settings)
|
||||
if (git) {
|
||||
// Use Git CLI
|
||||
await git.fetch(refSpec, fetchDepth)
|
||||
} else {
|
||||
// Fallback to REST API
|
||||
await githubApiHelper.downloadRepository(...)
|
||||
}
|
||||
```
|
||||
|
||||
### State Management (Unique Pattern!)
|
||||
- **Dual-phase execution**: Same script runs twice (MAIN + POST) determined by `stateHelper.IsPost`
|
||||
- **State persistence**: Use `core.saveState()` / `core.getState()` to pass data between phases
|
||||
- **Cleanup responsibility**: POST phase cleans up auth tokens, SSH keys, etc.
|
||||
|
||||
### Input Validation Approach
|
||||
- **GitHub context integration**: Defaults repository from `github.context.repo`
|
||||
- **Path safety**: Validates paths are within `GITHUB_WORKSPACE`
|
||||
- **Flexible refs**: Handles branches, tags, SHAs, and PR refs uniformly
|
||||
|
||||
## Integration Points
|
||||
|
||||
### GitHub Actions SDK Usage
|
||||
- **`@actions/core`**: Input/output, logging, state management
|
||||
- **`@actions/github`**: GitHub context and API access
|
||||
- **`@actions/exec`**: Git command execution
|
||||
- **`@actions/io`**: File system operations
|
||||
|
||||
### Git Integration
|
||||
- **Version compatibility**: Minimum Git 2.18, with feature detection for sparse-checkout
|
||||
- **Authentication modes**: Token-based (default) or SSH key-based
|
||||
- **Advanced features**: LFS, submodules, sparse-checkout, partial clones
|
||||
|
||||
### Container Support
|
||||
- **Safe directory**: Critical for container workflows - auto-configures git safe.directory
|
||||
- **Credential persistence**: Configures git credential helper for authenticated operations
|
||||
|
||||
## Common Debugging Patterns
|
||||
|
||||
### Enable Debug Logging
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
env:
|
||||
ACTIONS_STEP_DEBUG: true
|
||||
```
|
||||
|
||||
### REST API Fallback Testing
|
||||
```bash
|
||||
# Force REST API mode by overriding Git version
|
||||
__test__/override-git-version.sh
|
||||
```
|
||||
|
||||
### Authentication Issues
|
||||
- Check `GITHUB_TOKEN` permissions: needs `contents: read`
|
||||
- For private repos: requires PAT with repo access
|
||||
- Container issues: verify safe.directory configuration
|
||||
|
||||
## Key Files for Understanding
|
||||
- `src/git-source-provider.ts` - Main orchestration logic
|
||||
- `src/input-helper.ts` - Action interface and validation
|
||||
- `src/git-auth-helper.ts` - Authentication and credential management
|
||||
- `action.yml` - Complete input/output specification
|
||||
- `.github/workflows/test.yml` - Comprehensive test scenarios
|
||||
6
.github/workflows/check-dist.yml
vendored
6
.github/workflows/check-dist.yml
vendored
@@ -22,12 +22,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set Node.js 20.x
|
||||
- name: Set Node.js 24.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
node-version: 24.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
41
.github/workflows/deno.yml
vendored
Normal file
41
.github/workflows/deno.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# This workflow will install Deno then run `deno lint` and `deno test`.
|
||||
# For more information see: https://github.com/denoland/setup-deno
|
||||
|
||||
name: Deno
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Setup repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v1.1.2
|
||||
with:
|
||||
deno-version: v1.x
|
||||
|
||||
# Uncomment this step to verify the use of 'deno fmt' on each commit.
|
||||
# - name: Verify formatting
|
||||
# run: deno fmt --check
|
||||
|
||||
- name: Run linter
|
||||
run: deno lint
|
||||
|
||||
- name: Run tests
|
||||
run: deno test -A
|
||||
2
.github/workflows/licensed.yml
vendored
2
.github/workflows/licensed.yml
vendored
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check licenses
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6
|
||||
- run: npm ci
|
||||
- run: npm run licensed-check
|
||||
20
.github/workflows/publish-immutable-actions.yml
vendored
Normal file
20
.github/workflows/publish-immutable-actions.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: 'Publish Immutable Action Version'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checking out
|
||||
uses: actions/checkout@v6
|
||||
- name: Publish
|
||||
id: publish
|
||||
uses: actions/publish-immutable-action@0.0.3
|
||||
71
.github/workflows/test.yml
vendored
71
.github/workflows/test.yml
vendored
@@ -18,8 +18,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
- uses: actions/checkout@v3
|
||||
node-version: 24.x
|
||||
- uses: actions/checkout@v6
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run format-check
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Basic checkout
|
||||
- name: Checkout basic
|
||||
@@ -165,6 +165,22 @@ jobs:
|
||||
- name: Verify submodules recursive
|
||||
run: __test__/verify-submodules-recursive.sh
|
||||
|
||||
# Worktree credentials
|
||||
- name: Checkout for worktree test
|
||||
uses: ./
|
||||
with:
|
||||
path: worktree-test
|
||||
- name: Verify worktree credentials
|
||||
shell: bash
|
||||
run: __test__/verify-worktree.sh worktree-test worktree-branch
|
||||
|
||||
# Worktree credentials in container step
|
||||
- name: Verify worktree credentials in container step
|
||||
if: runner.os == 'Linux'
|
||||
uses: docker://bitnami/git:latest
|
||||
with:
|
||||
args: bash __test__/verify-worktree.sh worktree-test container-worktree-branch
|
||||
|
||||
# Basic checkout using REST API
|
||||
- name: Remove basic
|
||||
if: runner.os != 'windows'
|
||||
@@ -202,7 +218,7 @@ jobs:
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
@@ -234,7 +250,7 @@ jobs:
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
@@ -264,13 +280,13 @@ jobs:
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: v3
|
||||
path: localClone
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
uses: ./v3
|
||||
uses: ./localClone
|
||||
with:
|
||||
ref: test-data/v2/basic
|
||||
- name: Verify basic
|
||||
@@ -291,7 +307,40 @@ jobs:
|
||||
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
|
||||
|
||||
# needed to make checkout post cleanup succeed
|
||||
- name: Fix Checkout v3
|
||||
uses: actions/checkout@v3
|
||||
- name: Fix Checkout v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: v3
|
||||
path: localClone
|
||||
|
||||
test-output:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: actions-checkout
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
id: checkout
|
||||
uses: ./actions-checkout
|
||||
with:
|
||||
path: cloned-using-local-action
|
||||
ref: test-data/v2/basic
|
||||
|
||||
# Verify output
|
||||
- name: Verify output
|
||||
run: |
|
||||
echo "Commit: ${{ steps.checkout.outputs.commit }}"
|
||||
echo "Ref: ${{ steps.checkout.outputs.ref }}"
|
||||
|
||||
if [ "${{ steps.checkout.outputs.ref }}" != "test-data/v2/basic" ]; then
|
||||
echo "Expected ref to be test-data/v2/basic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${{ steps.checkout.outputs.commit }}" != "82f71901cf8c021332310dcc8cdba84c4193ff5d" ]; then
|
||||
echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
3
.github/workflows/update-main-version.yml
vendored
3
.github/workflows/update-main-version.yml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
type: choice
|
||||
description: The major version to update
|
||||
options:
|
||||
- v5
|
||||
- v4
|
||||
- v3
|
||||
- v2
|
||||
@@ -22,7 +23,7 @@ jobs:
|
||||
# Note this update workflow can also be used as a rollback tool.
|
||||
# For that reason, it's best to pin `actions/checkout` to a known, stable version
|
||||
# (typically, about two releases back).
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- 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:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Use `docker/login-action` to log in to GHCR.io.
|
||||
# Once published, the packages are scoped to the account defined here.
|
||||
- name: Log in to the ghcr.io container registry
|
||||
uses: docker/login-action@v3.1.0
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
# Use `docker/build-push-action` to build (and optionally publish) the image.
|
||||
- name: Build Docker Image (with optional Push)
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
uses: docker/build-push-action@v6.5.0
|
||||
with:
|
||||
context: .
|
||||
file: images/test-ubuntu-git.Dockerfile
|
||||
|
||||
4
.licenses/npm/@octokit/endpoint.dep.yml
generated
4
.licenses/npm/@octokit/endpoint.dep.yml
generated
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/endpoint"
|
||||
version: 9.0.5
|
||||
version: 9.0.6
|
||||
type: npm
|
||||
summary: Turns REST API endpoints into generic request options
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/plugin-paginate-rest"
|
||||
version: 9.2.1
|
||||
version: 9.2.2
|
||||
type: npm
|
||||
summary: Octokit plugin to paginate REST API endpoint responses
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
4
.licenses/npm/@octokit/request-error.dep.yml
generated
4
.licenses/npm/@octokit/request-error.dep.yml
generated
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/request-error"
|
||||
version: 5.1.0
|
||||
version: 5.1.1
|
||||
type: npm
|
||||
summary: Error class for Octokit request errors
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
4
.licenses/npm/@octokit/request.dep.yml
generated
4
.licenses/npm/@octokit/request.dep.yml
generated
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: "@octokit/request"
|
||||
version: 8.4.0
|
||||
version: 8.4.1
|
||||
type: npm
|
||||
summary: Send parameterized requests to GitHub's APIs with sensible defaults in browsers
|
||||
and Node
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
2
.licenses/npm/undici.dep.yml
generated
2
.licenses/npm/undici.dep.yml
generated
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: undici
|
||||
version: 5.28.4
|
||||
version: 5.29.0
|
||||
type: npm
|
||||
summary: An HTTP/1.1 client, written from scratch for Node.js
|
||||
homepage: https://undici.nodejs.org
|
||||
|
||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -1,5 +1,45 @@
|
||||
# Changelog
|
||||
|
||||
## v6.0.0
|
||||
* Persist creds to a separate file by @ericsciple in https://github.com/actions/checkout/pull/2286
|
||||
* Update README to include Node.js 24 support details and requirements by @salmanmkc in https://github.com/actions/checkout/pull/2248
|
||||
|
||||
## v5.0.1
|
||||
* Port v6 cleanup to v5 by @ericsciple in https://github.com/actions/checkout/pull/2301
|
||||
|
||||
## v5.0.0
|
||||
* Update actions checkout to use node 24 by @salmanmkc in https://github.com/actions/checkout/pull/2226
|
||||
|
||||
## v4.3.1
|
||||
* Port v6 cleanup to v4 by @ericsciple in https://github.com/actions/checkout/pull/2305
|
||||
|
||||
## v4.3.0
|
||||
* docs: update README.md by @motss in https://github.com/actions/checkout/pull/1971
|
||||
* Add internal repos for checking out multiple repositories by @mouismail in https://github.com/actions/checkout/pull/1977
|
||||
* Documentation update - add recommended permissions to Readme by @benwells in https://github.com/actions/checkout/pull/2043
|
||||
* Adjust positioning of user email note and permissions heading by @joshmgross in https://github.com/actions/checkout/pull/2044
|
||||
* Update README.md by @nebuk89 in https://github.com/actions/checkout/pull/2194
|
||||
* Update CODEOWNERS for actions by @TingluoHuang in https://github.com/actions/checkout/pull/2224
|
||||
* Update package dependencies by @salmanmkc in https://github.com/actions/checkout/pull/2236
|
||||
|
||||
## v4.2.2
|
||||
* `url-helper.ts` now leverages well-known environment variables by @jww3 in https://github.com/actions/checkout/pull/1941
|
||||
* Expand unit test coverage for `isGhes` by @jww3 in https://github.com/actions/checkout/pull/1946
|
||||
|
||||
## v4.2.1
|
||||
* Check out other refs/* by commit if provided, fall back to ref by @orhantoy in https://github.com/actions/checkout/pull/1924
|
||||
|
||||
## v4.2.0
|
||||
|
||||
* Add Ref and Commit outputs by @lucacome in https://github.com/actions/checkout/pull/1180
|
||||
* Dependency updates by @dependabot- https://github.com/actions/checkout/pull/1777, https://github.com/actions/checkout/pull/1872
|
||||
|
||||
## v4.1.7
|
||||
* Bump the minor-npm-dependencies group across 1 directory with 4 updates by @dependabot in https://github.com/actions/checkout/pull/1739
|
||||
* Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/actions/checkout/pull/1697
|
||||
* Check out other refs/* by commit by @orhantoy in https://github.com/actions/checkout/pull/1774
|
||||
* Pin actions/checkout's own workflows to a known, good, stable version. by @jww3 in https://github.com/actions/checkout/pull/1776
|
||||
|
||||
## v4.1.6
|
||||
* Check platform to set archive extension appropriately by @cory-miller in https://github.com/actions/checkout/pull/1732
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
* @actions/actions-launch
|
||||
* @actions/actions-runtime
|
||||
|
||||
213
LICENSE
213
LICENSE
@@ -1,22 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
The MIT License (MIT)
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
Copyright (c) 2018 GitHub, Inc. and contributors
|
||||
1. Definitions.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
141
README.md
141
README.md
@@ -1,6 +1,22 @@
|
||||
[](https://github.com/actions/checkout/actions/workflows/test.yml)
|
||||
|
||||
# Checkout V4
|
||||
# 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.
|
||||
|
||||
@@ -10,6 +26,24 @@ The auth token is persisted in the local git config. This enables your scripts t
|
||||
|
||||
When Git 2.18 or higher is not in your PATH, falls back to the REST API to download the files.
|
||||
|
||||
### Note
|
||||
|
||||
Thank you for your interest in this GitHub action, however, right now we are not taking contributions.
|
||||
|
||||
We continue to focus our resources on strategic areas that help our customers be successful while making developers' lives easier. While GitHub Actions remains a key part of this vision, we are allocating resources towards other areas of Actions and are not taking contributions to this repository at this time. The GitHub public roadmap is the best place to follow along for any updates on features we’re working on and what stage they’re in.
|
||||
|
||||
We are taking the following steps to better direct requests related to GitHub Actions, including:
|
||||
|
||||
1. We will be directing questions and support requests to our [Community Discussions area](https://github.com/orgs/community/discussions/categories/actions)
|
||||
|
||||
2. High Priority bugs can be reported through Community Discussions or you can report these to our support team https://support.github.com/contact/bug-report.
|
||||
|
||||
3. Security Issues should be handled as per our [security.md](security.md)
|
||||
|
||||
We will still provide security updates for this project and fix major breaking changes during this time.
|
||||
|
||||
You are welcome to still raise bugs in this repo.
|
||||
|
||||
# What's new
|
||||
|
||||
Please refer to the [release page](https://github.com/actions/checkout/releases/latest) for the latest release notes.
|
||||
@@ -18,7 +52,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
|
||||
<!-- start usage -->
|
||||
```yaml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Repository name with owner. For example, actions/checkout
|
||||
# Default: ${{ github.repository }}
|
||||
@@ -131,23 +165,33 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
|
||||
# Scenarios
|
||||
|
||||
- [Fetch only the root files](#Fetch-only-the-root-files)
|
||||
- [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder)
|
||||
- [Fetch only a single file](#Fetch-only-a-single-file)
|
||||
- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches)
|
||||
- [Checkout a different branch](#Checkout-a-different-branch)
|
||||
- [Checkout HEAD^](#Checkout-HEAD)
|
||||
- [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side)
|
||||
- [Checkout multiple repos (nested)](#Checkout-multiple-repos-nested)
|
||||
- [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
- [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit)
|
||||
- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
|
||||
- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token)
|
||||
- [Checkout V5](#checkout-v5)
|
||||
- [What's new](#whats-new)
|
||||
- [Checkout V4](#checkout-v4)
|
||||
- [Note](#note)
|
||||
- [What's new](#whats-new-1)
|
||||
- [Usage](#usage)
|
||||
- [Scenarios](#scenarios)
|
||||
- [Fetch only the root files](#fetch-only-the-root-files)
|
||||
- [Fetch only the root files and `.github` and `src` folder](#fetch-only-the-root-files-and-github-and-src-folder)
|
||||
- [Fetch only a single file](#fetch-only-a-single-file)
|
||||
- [Fetch all history for all tags and branches](#fetch-all-history-for-all-tags-and-branches)
|
||||
- [Checkout a different branch](#checkout-a-different-branch)
|
||||
- [Checkout HEAD^](#checkout-head)
|
||||
- [Checkout multiple repos (side by side)](#checkout-multiple-repos-side-by-side)
|
||||
- [Checkout multiple repos (nested)](#checkout-multiple-repos-nested)
|
||||
- [Checkout multiple repos (private)](#checkout-multiple-repos-private)
|
||||
- [Checkout pull request HEAD commit instead of merge commit](#checkout-pull-request-head-commit-instead-of-merge-commit)
|
||||
- [Checkout pull request on closed event](#checkout-pull-request-on-closed-event)
|
||||
- [Push a commit using the built-in token](#push-a-commit-using-the-built-in-token)
|
||||
- [Push a commit to a PR using the built-in token](#push-a-commit-to-a-pr-using-the-built-in-token)
|
||||
- [Recommended permissions](#recommended-permissions)
|
||||
- [License](#license)
|
||||
|
||||
## Fetch only the root files
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: .
|
||||
```
|
||||
@@ -155,7 +199,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Fetch only the root files and `.github` and `src` folder
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github
|
||||
@@ -165,7 +209,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Fetch only a single file
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
README.md
|
||||
@@ -175,7 +219,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Fetch all history for all tags and branches
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
```
|
||||
@@ -183,7 +227,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Checkout a different branch
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: my-branch
|
||||
```
|
||||
@@ -191,7 +235,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Checkout HEAD^
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- run: git checkout HEAD^
|
||||
@@ -201,42 +245,42 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: main
|
||||
|
||||
- name: Checkout tools repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: my-org/my-tools
|
||||
path: my-tools
|
||||
```
|
||||
> - If your secondary repository is private you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
> - If your secondary repository is private or internal you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
|
||||
## Checkout multiple repos (nested)
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Checkout tools repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: my-org/my-tools
|
||||
path: my-tools
|
||||
```
|
||||
> - If your secondary repository is private you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
> - If your secondary repository is private or internal you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
||||
|
||||
## Checkout multiple repos (private)
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: main
|
||||
|
||||
- name: Checkout private tools
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: my-org/my-private-tools
|
||||
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
|
||||
@@ -249,7 +293,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
||||
## Checkout pull request HEAD commit instead of merge commit
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
```
|
||||
@@ -265,7 +309,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
```
|
||||
|
||||
## Push a commit using the built-in token
|
||||
@@ -276,7 +320,31 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- run: |
|
||||
date > generated.txt
|
||||
# Note: the following account information will not work on GHES
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git commit -m "generated"
|
||||
git push
|
||||
```
|
||||
*NOTE:* The user email is `{user.id}+{user.login}@users.noreply.github.com`. See users API: https://api.github.com/users/github-actions%5Bbot%5D
|
||||
|
||||
## Push a commit to a PR using the built-in token
|
||||
|
||||
In a pull request trigger, `ref` is required as GitHub Actions checks out in detached HEAD mode, meaning it doesn’t check out your branch by default.
|
||||
|
||||
```yaml
|
||||
on: pull_request
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- run: |
|
||||
date > generated.txt
|
||||
# Note: the following account information will not work on GHES
|
||||
@@ -287,6 +355,17 @@ jobs:
|
||||
git push
|
||||
```
|
||||
|
||||
*NOTE:* The user email is `{user.id}+{user.login}@users.noreply.github.com`. See users API: https://api.github.com/users/github-actions%5Bbot%5D
|
||||
|
||||
# Recommended permissions
|
||||
|
||||
When using the `checkout` action in your GitHub Actions workflow, it is recommended to set the following `GITHUB_TOKEN` permissions to ensure proper functionality, unless alternative auth is provided via the `token` or `ssh-key` inputs:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read
|
||||
```
|
||||
|
||||
# License
|
||||
|
||||
The scripts and documentation in this project are released under the [MIT License](LICENSE)
|
||||
|
||||
@@ -86,16 +86,29 @@ describe('git-auth-helper tests', () => {
|
||||
// Act
|
||||
await authHelper.configureAuth()
|
||||
|
||||
// Assert config
|
||||
const configContent = (
|
||||
// Assert config - check that .git/config contains includeIf entries
|
||||
const localConfigContent = (
|
||||
await fs.promises.readFile(localGitConfigPath)
|
||||
).toString()
|
||||
expect(
|
||||
localConfigContent.indexOf('includeIf.gitdir:')
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Assert credentials config file contains the actual credentials
|
||||
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||
)
|
||||
expect(credentialsFiles.length).toBe(1)
|
||||
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
|
||||
const credentialsContent = (
|
||||
await fs.promises.readFile(credentialsConfigPath)
|
||||
).toString()
|
||||
const basicCredential = Buffer.from(
|
||||
`x-access-token:${settings.authToken}`,
|
||||
'utf8'
|
||||
).toString('base64')
|
||||
expect(
|
||||
configContent.indexOf(
|
||||
credentialsContent.indexOf(
|
||||
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||
)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
@@ -120,7 +133,7 @@ describe('git-auth-helper tests', () => {
|
||||
'inject https://github.com as github server url'
|
||||
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
|
||||
await testAuthHeader(
|
||||
configureAuth_AcceptsGitHubServerUrl,
|
||||
configureAuth_AcceptsGitHubServerUrlSetToGHEC,
|
||||
'https://github.com'
|
||||
)
|
||||
})
|
||||
@@ -141,12 +154,17 @@ describe('git-auth-helper tests', () => {
|
||||
// Act
|
||||
await authHelper.configureAuth()
|
||||
|
||||
// Assert config
|
||||
const configContent = (
|
||||
await fs.promises.readFile(localGitConfigPath)
|
||||
// Assert config - check credentials config file (not local .git/config)
|
||||
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||
)
|
||||
expect(credentialsFiles.length).toBe(1)
|
||||
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
|
||||
const credentialsContent = (
|
||||
await fs.promises.readFile(credentialsConfigPath)
|
||||
).toString()
|
||||
expect(
|
||||
configContent.indexOf(
|
||||
credentialsContent.indexOf(
|
||||
`http.https://github.com/.extraheader AUTHORIZATION`
|
||||
)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
@@ -251,13 +269,16 @@ describe('git-auth-helper tests', () => {
|
||||
expectedSshCommand
|
||||
)
|
||||
|
||||
// Asserty git config
|
||||
// Assert git config
|
||||
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter(x => x)
|
||||
expect(gitConfigLines).toHaveLength(1)
|
||||
expect(gitConfigLines[0]).toMatch(/^http\./)
|
||||
// Should have includeIf entries pointing to credentials file
|
||||
expect(gitConfigLines.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0)
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
|
||||
@@ -419,8 +440,20 @@ describe('git-auth-helper tests', () => {
|
||||
expect(
|
||||
configContent.indexOf('value-from-global-config')
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
// Global config should have include.path pointing to credentials file
|
||||
expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Check credentials in the separate config file
|
||||
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||
)
|
||||
expect(credentialsFiles.length).toBeGreaterThan(0)
|
||||
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
|
||||
const credentialsContent = (
|
||||
await fs.promises.readFile(credentialsConfigPath)
|
||||
).toString()
|
||||
expect(
|
||||
configContent.indexOf(
|
||||
credentialsContent.indexOf(
|
||||
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||
)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
@@ -463,8 +496,20 @@ describe('git-auth-helper tests', () => {
|
||||
const configContent = (
|
||||
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
|
||||
).toString()
|
||||
// Global config should have include.path pointing to credentials file
|
||||
expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Check credentials in the separate config file
|
||||
const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||
)
|
||||
expect(credentialsFiles.length).toBeGreaterThan(0)
|
||||
const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
|
||||
const credentialsContent = (
|
||||
await fs.promises.readFile(credentialsConfigPath)
|
||||
).toString()
|
||||
expect(
|
||||
configContent.indexOf(
|
||||
credentialsContent.indexOf(
|
||||
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||
)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
@@ -550,15 +595,15 @@ describe('git-auth-helper tests', () => {
|
||||
await authHelper.configureSubmoduleAuth()
|
||||
|
||||
// Assert
|
||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
|
||||
// Should configure insteadOf (2 calls for two values)
|
||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
|
||||
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
||||
/unset-all.*insteadOf/
|
||||
)
|
||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
|
||||
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
|
||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(
|
||||
/url.*insteadOf.*git@github.com:/
|
||||
)
|
||||
expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
|
||||
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
|
||||
/url.*insteadOf.*org-123456@github.com:/
|
||||
)
|
||||
}
|
||||
@@ -589,12 +634,12 @@ describe('git-auth-helper tests', () => {
|
||||
await authHelper.configureSubmoduleAuth()
|
||||
|
||||
// Assert
|
||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
|
||||
// Should configure sshCommand (1 call)
|
||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
|
||||
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
||||
/unset-all.*insteadOf/
|
||||
)
|
||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
|
||||
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
|
||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -660,19 +705,201 @@ describe('git-auth-helper tests', () => {
|
||||
await setup(removeAuth_removesToken)
|
||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
await authHelper.configureAuth()
|
||||
let gitConfigContent = (
|
||||
|
||||
// Verify includeIf entries exist in local config
|
||||
let localConfigContent = (
|
||||
await fs.promises.readFile(localGitConfigPath)
|
||||
).toString()
|
||||
expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
|
||||
expect(
|
||||
localConfigContent.indexOf('includeIf.gitdir:')
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Verify both host and container includeIf entries are present
|
||||
const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
|
||||
expect(
|
||||
localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
expect(
|
||||
localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Verify credentials file exists
|
||||
let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||
)
|
||||
expect(credentialsFiles.length).toBe(1)
|
||||
const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
|
||||
|
||||
// Verify credentials file contains the auth token
|
||||
let credentialsContent = (
|
||||
await fs.promises.readFile(credentialsFilePath)
|
||||
).toString()
|
||||
const basicCredential = Buffer.from(
|
||||
`x-access-token:${settings.authToken}`,
|
||||
'utf8'
|
||||
).toString('base64')
|
||||
expect(
|
||||
credentialsContent.indexOf(
|
||||
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
||||
)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Verify the includeIf entries point to the credentials file
|
||||
const containerCredentialsPath = path.posix.join(
|
||||
'/github/runner_temp',
|
||||
path.basename(credentialsFilePath)
|
||||
)
|
||||
expect(
|
||||
localConfigContent.indexOf(credentialsFilePath)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
expect(
|
||||
localConfigContent.indexOf(containerCredentialsPath)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Act
|
||||
await authHelper.removeAuth()
|
||||
|
||||
// Assert git config
|
||||
gitConfigContent = (
|
||||
// Assert all includeIf entries removed from local git config
|
||||
localConfigContent = (
|
||||
await fs.promises.readFile(localGitConfigPath)
|
||||
).toString()
|
||||
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
|
||||
expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0)
|
||||
expect(
|
||||
localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
|
||||
).toBeLessThan(0)
|
||||
expect(
|
||||
localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
|
||||
).toBeLessThan(0)
|
||||
expect(localConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
|
||||
expect(localConfigContent.indexOf(containerCredentialsPath)).toBeLessThan(0)
|
||||
|
||||
// Assert credentials config file deleted
|
||||
credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||
)
|
||||
expect(credentialsFiles.length).toBe(0)
|
||||
|
||||
// Verify credentials file no longer exists on disk
|
||||
try {
|
||||
await fs.promises.stat(credentialsFilePath)
|
||||
throw new Error('Credentials file should have been deleted')
|
||||
} catch (err) {
|
||||
if ((err as any)?.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const removeAuth_removesTokenFromSubmodules =
|
||||
'removeAuth removes token from submodules'
|
||||
it(removeAuth_removesTokenFromSubmodules, async () => {
|
||||
// Arrange
|
||||
await setup(removeAuth_removesTokenFromSubmodules)
|
||||
|
||||
// Create fake submodule config paths
|
||||
const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
|
||||
const submodule2Dir = path.join(workspace, '.git', 'modules', 'submodule-2')
|
||||
const submodule1ConfigPath = path.join(submodule1Dir, 'config')
|
||||
const submodule2ConfigPath = path.join(submodule2Dir, 'config')
|
||||
|
||||
await fs.promises.mkdir(submodule1Dir, {recursive: true})
|
||||
await fs.promises.mkdir(submodule2Dir, {recursive: true})
|
||||
await fs.promises.writeFile(submodule1ConfigPath, '')
|
||||
await fs.promises.writeFile(submodule2ConfigPath, '')
|
||||
|
||||
// Mock getSubmoduleConfigPaths to return our fake submodules (for both configure and remove)
|
||||
const mockGetSubmoduleConfigPaths =
|
||||
git.getSubmoduleConfigPaths as jest.Mock<any, any>
|
||||
mockGetSubmoduleConfigPaths.mockResolvedValue([
|
||||
submodule1ConfigPath,
|
||||
submodule2ConfigPath
|
||||
])
|
||||
|
||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
await authHelper.configureAuth()
|
||||
await authHelper.configureSubmoduleAuth()
|
||||
|
||||
// Verify credentials file exists
|
||||
let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||
)
|
||||
expect(credentialsFiles.length).toBe(1)
|
||||
const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
|
||||
|
||||
// Verify submodule 1 config has includeIf entries
|
||||
let submodule1Content = (
|
||||
await fs.promises.readFile(submodule1ConfigPath)
|
||||
).toString()
|
||||
const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
|
||||
expect(
|
||||
submodule1Content.indexOf(`includeIf.gitdir:${submodule1GitDir}.path`)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
expect(
|
||||
submodule1Content.indexOf(credentialsFilePath)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Verify submodule 2 config has includeIf entries
|
||||
let submodule2Content = (
|
||||
await fs.promises.readFile(submodule2ConfigPath)
|
||||
).toString()
|
||||
const submodule2GitDir = submodule2Dir.replace(/\\/g, '/')
|
||||
expect(
|
||||
submodule2Content.indexOf(`includeIf.gitdir:${submodule2GitDir}.path`)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
expect(
|
||||
submodule2Content.indexOf(credentialsFilePath)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Verify both host and container paths are in each submodule config
|
||||
const containerCredentialsPath = path.posix.join(
|
||||
'/github/runner_temp',
|
||||
path.basename(credentialsFilePath)
|
||||
)
|
||||
expect(
|
||||
submodule1Content.indexOf(containerCredentialsPath)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
expect(
|
||||
submodule2Content.indexOf(containerCredentialsPath)
|
||||
).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Act - ensure mock persists for removeAuth
|
||||
mockGetSubmoduleConfigPaths.mockResolvedValue([
|
||||
submodule1ConfigPath,
|
||||
submodule2ConfigPath
|
||||
])
|
||||
await authHelper.removeAuth()
|
||||
|
||||
// Assert submodule 1 includeIf entries removed
|
||||
submodule1Content = (
|
||||
await fs.promises.readFile(submodule1ConfigPath)
|
||||
).toString()
|
||||
expect(submodule1Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
|
||||
expect(submodule1Content.indexOf(credentialsFilePath)).toBeLessThan(0)
|
||||
expect(submodule1Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
|
||||
|
||||
// Assert submodule 2 includeIf entries removed
|
||||
submodule2Content = (
|
||||
await fs.promises.readFile(submodule2ConfigPath)
|
||||
).toString()
|
||||
expect(submodule2Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
|
||||
expect(submodule2Content.indexOf(credentialsFilePath)).toBeLessThan(0)
|
||||
expect(submodule2Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
|
||||
|
||||
// Assert credentials config file deleted
|
||||
credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
|
||||
f => f.startsWith('git-credentials-') && f.endsWith('.config')
|
||||
)
|
||||
expect(credentialsFiles.length).toBe(0)
|
||||
|
||||
// Verify credentials file no longer exists on disk
|
||||
try {
|
||||
await fs.promises.stat(credentialsFilePath)
|
||||
throw new Error('Credentials file should have been deleted')
|
||||
} catch (err) {
|
||||
if ((err as any)?.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const removeGlobalConfig_removesOverride =
|
||||
@@ -701,6 +928,52 @@ describe('git-auth-helper tests', () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const testCredentialsConfigPath_matchesCredentialsConfigPaths =
|
||||
'testCredentialsConfigPath matches credentials config paths'
|
||||
it(testCredentialsConfigPath_matchesCredentialsConfigPaths, async () => {
|
||||
// Arrange
|
||||
await setup(testCredentialsConfigPath_matchesCredentialsConfigPaths)
|
||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
|
||||
// Get a real credentials config path
|
||||
const credentialsConfigPath = await (
|
||||
authHelper as any
|
||||
).getCredentialsConfigPath()
|
||||
|
||||
// Act & Assert
|
||||
expect(
|
||||
(authHelper as any).testCredentialsConfigPath(credentialsConfigPath)
|
||||
).toBe(true)
|
||||
expect(
|
||||
(authHelper as any).testCredentialsConfigPath(
|
||||
'/some/path/git-credentials-12345678-abcd-1234-5678-123456789012.config'
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
(authHelper as any).testCredentialsConfigPath(
|
||||
'/some/path/git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
|
||||
)
|
||||
).toBe(true)
|
||||
|
||||
// Test invalid paths
|
||||
expect(
|
||||
(authHelper as any).testCredentialsConfigPath(
|
||||
'/some/path/other-config.config'
|
||||
)
|
||||
).toBe(false)
|
||||
expect(
|
||||
(authHelper as any).testCredentialsConfigPath(
|
||||
'/some/path/git-credentials-invalid.config'
|
||||
)
|
||||
).toBe(false)
|
||||
expect(
|
||||
(authHelper as any).testCredentialsConfigPath(
|
||||
'/some/path/git-credentials-.config'
|
||||
)
|
||||
).toBe(false)
|
||||
expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
async function setup(testName: string): Promise<void> {
|
||||
@@ -715,6 +988,7 @@ async function setup(testName: string): Promise<void> {
|
||||
await fs.promises.mkdir(tempHomedir, {recursive: true})
|
||||
process.env['RUNNER_TEMP'] = runnerTemp
|
||||
process.env['HOME'] = tempHomedir
|
||||
process.env['GITHUB_WORKSPACE'] = workspace
|
||||
|
||||
// Create git config
|
||||
globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
|
||||
@@ -733,10 +1007,20 @@ async function setup(testName: string): Promise<void> {
|
||||
checkout: jest.fn(),
|
||||
checkoutDetach: jest.fn(),
|
||||
config: jest.fn(
|
||||
async (key: string, value: string, globalConfig?: boolean) => {
|
||||
const configPath = globalConfig
|
||||
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||
: localGitConfigPath
|
||||
async (
|
||||
key: string,
|
||||
value: string,
|
||||
globalConfig?: boolean,
|
||||
add?: boolean,
|
||||
configFile?: string
|
||||
) => {
|
||||
const configPath =
|
||||
configFile ||
|
||||
(globalConfig
|
||||
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||
: localGitConfigPath)
|
||||
// Ensure directory exists
|
||||
await fs.promises.mkdir(path.dirname(configPath), {recursive: true})
|
||||
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
|
||||
}
|
||||
),
|
||||
@@ -756,6 +1040,7 @@ async function setup(testName: string): Promise<void> {
|
||||
env: {},
|
||||
fetch: jest.fn(),
|
||||
getDefaultBranch: jest.fn(),
|
||||
getSubmoduleConfigPaths: jest.fn(async () => []),
|
||||
getWorkingDirectory: jest.fn(() => workspace),
|
||||
init: jest.fn(),
|
||||
isDetached: jest.fn(),
|
||||
@@ -794,8 +1079,72 @@ async function setup(testName: string): Promise<void> {
|
||||
return true
|
||||
}
|
||||
),
|
||||
tryConfigUnsetValue: jest.fn(
|
||||
async (
|
||||
key: string,
|
||||
value: string,
|
||||
globalConfig?: boolean,
|
||||
configPath?: string
|
||||
): Promise<boolean> => {
|
||||
const targetConfigPath =
|
||||
configPath ||
|
||||
(globalConfig
|
||||
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||
: localGitConfigPath)
|
||||
let content = await fs.promises.readFile(targetConfigPath)
|
||||
let lines = content
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter(x => x)
|
||||
.filter(x => !(x.startsWith(key) && x.includes(value)))
|
||||
await fs.promises.writeFile(targetConfigPath, lines.join('\n'))
|
||||
return true
|
||||
}
|
||||
),
|
||||
tryDisableAutomaticGarbageCollection: jest.fn(),
|
||||
tryGetFetchUrl: jest.fn(),
|
||||
tryGetConfigValues: jest.fn(
|
||||
async (
|
||||
key: string,
|
||||
globalConfig?: boolean,
|
||||
configPath?: string
|
||||
): Promise<string[]> => {
|
||||
const targetConfigPath =
|
||||
configPath ||
|
||||
(globalConfig
|
||||
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||
: localGitConfigPath)
|
||||
const content = await fs.promises.readFile(targetConfigPath)
|
||||
const lines = content
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter(x => x && x.startsWith(key))
|
||||
.map(x => x.substring(key.length).trim())
|
||||
return lines
|
||||
}
|
||||
),
|
||||
tryGetConfigKeys: jest.fn(
|
||||
async (
|
||||
pattern: string,
|
||||
globalConfig?: boolean,
|
||||
configPath?: string
|
||||
): Promise<string[]> => {
|
||||
const targetConfigPath =
|
||||
configPath ||
|
||||
(globalConfig
|
||||
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
||||
: localGitConfigPath)
|
||||
const content = await fs.promises.readFile(targetConfigPath)
|
||||
const lines = content
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter(x => x)
|
||||
const keys = lines
|
||||
.filter(x => new RegExp(pattern).test(x.split(' ')[0]))
|
||||
.map(x => x.split(' ')[0])
|
||||
return [...new Set(keys)] // Remove duplicates
|
||||
}
|
||||
),
|
||||
tryReset: jest.fn(),
|
||||
version: jest.fn()
|
||||
}
|
||||
@@ -830,6 +1179,7 @@ async function setup(testName: string): Promise<void> {
|
||||
|
||||
async function getActualSshKeyPath(): Promise<string> {
|
||||
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
||||
.filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
|
||||
.sort()
|
||||
.map(x => path.join(runnerTemp, x))
|
||||
if (actualTempFiles.length === 0) {
|
||||
@@ -843,6 +1193,7 @@ async function getActualSshKeyPath(): Promise<string> {
|
||||
|
||||
async function getActualSshKnownHostsPath(): Promise<string> {
|
||||
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
||||
.filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
|
||||
.sort()
|
||||
.map(x => path.join(runnerTemp, x))
|
||||
if (actualTempFiles.length === 0) {
|
||||
|
||||
@@ -471,6 +471,7 @@ async function setup(testName: string): Promise<void> {
|
||||
configExists: jest.fn(),
|
||||
fetch: jest.fn(),
|
||||
getDefaultBranch: jest.fn(),
|
||||
getSubmoduleConfigPaths: jest.fn(async () => []),
|
||||
getWorkingDirectory: jest.fn(() => repositoryPath),
|
||||
init: jest.fn(),
|
||||
isDetached: jest.fn(),
|
||||
@@ -493,12 +494,15 @@ async function setup(testName: string): Promise<void> {
|
||||
return true
|
||||
}),
|
||||
tryConfigUnset: jest.fn(),
|
||||
tryConfigUnsetValue: jest.fn(),
|
||||
tryDisableAutomaticGarbageCollection: jest.fn(),
|
||||
tryGetFetchUrl: jest.fn(async () => {
|
||||
// Sanity check - this function shouldn't be called when the .git directory doesn't exist
|
||||
await fs.promises.stat(path.join(repositoryPath, '.git'))
|
||||
return repositoryUrl
|
||||
}),
|
||||
tryGetConfigValues: jest.fn(),
|
||||
tryGetConfigKeys: jest.fn(),
|
||||
tryReset: jest.fn(async () => {
|
||||
return true
|
||||
}),
|
||||
|
||||
@@ -67,6 +67,26 @@ describe('ref-helper tests', () => {
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
'refs/gh/queue/main/pr-123',
|
||||
commit
|
||||
)
|
||||
expect(checkoutInfo.ref).toBe(commit)
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/ without commit', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
'refs/non-standard-ref',
|
||||
''
|
||||
)
|
||||
expect(checkoutInfo.ref).toBe('refs/non-standard-ref')
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo unqualified branch only', async () => {
|
||||
git.branchExists = jest.fn(async (remote: boolean, pattern: string) => {
|
||||
return true
|
||||
|
||||
92
__test__/url-helper.test.ts
Normal file
92
__test__/url-helper.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as urlHelper from '../src/url-helper'
|
||||
|
||||
describe('getServerUrl tests', () => {
|
||||
it('basics', async () => {
|
||||
// Note that URL::toString will append a trailing / when passed just a domain name ...
|
||||
expect(urlHelper.getServerUrl().toString()).toBe('https://github.com/')
|
||||
expect(urlHelper.getServerUrl(' ').toString()).toBe('https://github.com/')
|
||||
expect(urlHelper.getServerUrl(' ').toString()).toBe('https://github.com/')
|
||||
expect(urlHelper.getServerUrl('http://contoso.com').toString()).toBe(
|
||||
'http://contoso.com/'
|
||||
)
|
||||
expect(urlHelper.getServerUrl('https://contoso.com').toString()).toBe(
|
||||
'https://contoso.com/'
|
||||
)
|
||||
expect(urlHelper.getServerUrl('https://contoso.com/').toString()).toBe(
|
||||
'https://contoso.com/'
|
||||
)
|
||||
|
||||
// ... but can't make that same assumption when passed an URL that includes some deeper path.
|
||||
expect(urlHelper.getServerUrl('https://contoso.com/a/b').toString()).toBe(
|
||||
'https://contoso.com/a/b'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isGhes tests', () => {
|
||||
const pristineEnv = process.env
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
process.env = {...pristineEnv}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
process.env = pristineEnv
|
||||
})
|
||||
|
||||
it('basics', async () => {
|
||||
delete process.env['GITHUB_SERVER_URL']
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://github.com')).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://contoso.ghe.com')).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://test.github.localhost')).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://src.onpremise.fabrikam.com')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable is not defined', async () => {
|
||||
delete process.env['GITHUB_SERVER_URL']
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable is set to github.com', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://github.com'
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://contoso.ghe.com'
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://mock-github.localhost'
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns true when the GITHUB_SERVER_URL environment variable is set to some other URL', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://src.onpremise.fabrikam.com'
|
||||
expect(urlHelper.isGhes()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getServerApiUrl tests', () => {
|
||||
it('basics', async () => {
|
||||
expect(urlHelper.getServerApiUrl()).toBe('https://api.github.com')
|
||||
expect(urlHelper.getServerApiUrl('https://github.com')).toBe(
|
||||
'https://api.github.com'
|
||||
)
|
||||
expect(urlHelper.getServerApiUrl('https://GitHub.com')).toBe(
|
||||
'https://api.github.com'
|
||||
)
|
||||
expect(urlHelper.getServerApiUrl('https://contoso.ghe.com')).toBe(
|
||||
'https://api.contoso.ghe.com'
|
||||
)
|
||||
expect(urlHelper.getServerApiUrl('https://fabrikam.GHE.COM')).toBe(
|
||||
'https://api.fabrikam.ghe.com'
|
||||
)
|
||||
expect(
|
||||
urlHelper.getServerApiUrl('https://src.onpremise.fabrikam.com')
|
||||
).toBe('https://src.onpremise.fabrikam.com/api/v3')
|
||||
})
|
||||
})
|
||||
@@ -17,7 +17,7 @@ fi
|
||||
|
||||
echo "Testing persisted credential"
|
||||
pushd ./submodules-recursive/submodule-level-1/submodule-level-2
|
||||
git config --local --name-only --get-regexp http.+extraheader && git fetch
|
||||
git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "Failed to validate persisted credential"
|
||||
popd
|
||||
|
||||
@@ -17,7 +17,7 @@ fi
|
||||
|
||||
echo "Testing persisted credential"
|
||||
pushd ./submodules-true/submodule-level-1
|
||||
git config --local --name-only --get-regexp http.+extraheader && git fetch
|
||||
git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "Failed to validate persisted credential"
|
||||
popd
|
||||
|
||||
51
__test__/verify-worktree.sh
Executable file
51
__test__/verify-worktree.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Verify worktree credentials
|
||||
# This test verifies that git credentials work in worktrees created after checkout
|
||||
# Usage: verify-worktree.sh <checkout-path> <worktree-name>
|
||||
|
||||
CHECKOUT_PATH="$1"
|
||||
WORKTREE_NAME="$2"
|
||||
|
||||
if [ -z "$CHECKOUT_PATH" ] || [ -z "$WORKTREE_NAME" ]; then
|
||||
echo "Usage: verify-worktree.sh <checkout-path> <worktree-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$CHECKOUT_PATH"
|
||||
|
||||
# Add safe directory for container environments
|
||||
git config --global --add safe.directory "*" 2>/dev/null || true
|
||||
|
||||
# Show the includeIf configuration
|
||||
echo "Git config includeIf entries:"
|
||||
git config --list --show-origin | grep -i include || true
|
||||
|
||||
# Create the worktree
|
||||
echo "Creating worktree..."
|
||||
git worktree add "../$WORKTREE_NAME" HEAD --detach
|
||||
|
||||
# Change to worktree directory
|
||||
cd "../$WORKTREE_NAME"
|
||||
|
||||
# Verify we're in a worktree
|
||||
echo "Verifying worktree gitdir:"
|
||||
cat .git
|
||||
|
||||
# Verify credentials are available in worktree by checking extraheader is configured
|
||||
echo "Checking credentials in worktree..."
|
||||
if git config --list --show-origin | grep -q "extraheader"; then
|
||||
echo "Credentials are configured in worktree"
|
||||
else
|
||||
echo "ERROR: Credentials are NOT configured in worktree"
|
||||
echo "Full git config:"
|
||||
git config --list --show-origin
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify fetch works in the worktree
|
||||
echo "Fetching in worktree..."
|
||||
git fetch origin
|
||||
|
||||
echo "Worktree credentials test passed!"
|
||||
@@ -98,7 +98,12 @@ inputs:
|
||||
github-server-url:
|
||||
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
|
||||
required: false
|
||||
outputs:
|
||||
ref:
|
||||
description: 'The branch, tag or SHA that was checked out'
|
||||
commit:
|
||||
description: 'The commit SHA that was checked out'
|
||||
runs:
|
||||
using: node20
|
||||
using: node24
|
||||
main: dist/index.js
|
||||
post: dist/index.js
|
||||
|
||||
577
dist/index.js
vendored
577
dist/index.js
vendored
File diff suppressed because it is too large
Load Diff
852
package-lock.json
generated
852
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "checkout",
|
||||
"version": "4.1.6",
|
||||
"version": "5.0.0",
|
||||
"description": "checkout action",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
@@ -37,19 +37,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/node": "^24.1.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
||||
"@typescript-eslint/parser": "^7.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
||||
"@typescript-eslint/parser": "^7.9.0",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-github": "^4.10.2",
|
||||
"eslint-plugin-jest": "^28.2.0",
|
||||
"eslint-plugin-jest": "^28.8.2",
|
||||
"jest": "^29.7.0",
|
||||
"jest-circus": "^29.7.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^5.4.5"
|
||||
"prettier": "^3.3.3",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class GitAuthHelper {
|
||||
private sshKeyPath = ''
|
||||
private sshKnownHostsPath = ''
|
||||
private temporaryHomePath = ''
|
||||
private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP
|
||||
|
||||
constructor(
|
||||
gitCommandManager: IGitCommandManager,
|
||||
@@ -126,16 +127,21 @@ class GitAuthHelper {
|
||||
|
||||
async configureGlobalAuth(): Promise<void> {
|
||||
// 'configureTempGlobalConfig' noops if already set, just returns the path
|
||||
const newGitConfigPath = await this.configureTempGlobalConfig()
|
||||
await this.configureTempGlobalConfig()
|
||||
try {
|
||||
// Configure the token
|
||||
await this.configureToken(newGitConfigPath, true)
|
||||
await this.configureToken(true)
|
||||
|
||||
// Configure HTTPS instead of SSH
|
||||
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
||||
if (!this.settings.sshKey) {
|
||||
for (const insteadOfValue of this.insteadOfValues) {
|
||||
await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
|
||||
await this.git.config(
|
||||
this.insteadOfKey,
|
||||
insteadOfValue,
|
||||
true, // globalConfig?
|
||||
true // add?
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -150,24 +156,60 @@ class GitAuthHelper {
|
||||
|
||||
async configureSubmoduleAuth(): Promise<void> {
|
||||
// Remove possible previous HTTPS instead of SSH
|
||||
await this.removeGitConfig(this.insteadOfKey, true)
|
||||
await this.removeSubmoduleGitConfig(this.insteadOfKey)
|
||||
|
||||
if (this.settings.persistCredentials) {
|
||||
// Configure a placeholder value. This approach avoids the credential being captured
|
||||
// by process creation audit events, which are commonly logged. For more information,
|
||||
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
||||
const output = await this.git.submoduleForeach(
|
||||
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
|
||||
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
|
||||
// Get the credentials config file path in RUNNER_TEMP
|
||||
const credentialsConfigPath = this.getCredentialsConfigPath()
|
||||
|
||||
// Container credentials config path
|
||||
const containerCredentialsPath = path.posix.join(
|
||||
'/github/runner_temp',
|
||||
path.basename(credentialsConfigPath)
|
||||
)
|
||||
|
||||
// Get submodule config file paths.
|
||||
const configPaths = await this.git.getSubmoduleConfigPaths(
|
||||
this.settings.nestedSubmodules
|
||||
)
|
||||
|
||||
// Replace the placeholder
|
||||
const configPaths: string[] =
|
||||
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
|
||||
// For each submodule, configure includeIf entries pointing to the shared credentials file.
|
||||
// Configure both host and container paths to support Docker container actions.
|
||||
for (const configPath of configPaths) {
|
||||
core.debug(`Replacing token placeholder in '${configPath}'`)
|
||||
await this.replaceTokenPlaceholder(configPath)
|
||||
// Submodule Git directory
|
||||
let submoduleGitDir = path.dirname(configPath) // The config file is at .git/modules/submodule-name/config
|
||||
submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||
|
||||
// Configure host includeIf
|
||||
await this.git.config(
|
||||
`includeIf.gitdir:${submoduleGitDir}.path`,
|
||||
credentialsConfigPath,
|
||||
false, // globalConfig?
|
||||
false, // add?
|
||||
configPath
|
||||
)
|
||||
|
||||
// Container submodule git directory
|
||||
const githubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||
assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
|
||||
let relativeSubmoduleGitDir = path.relative(
|
||||
githubWorkspace,
|
||||
submoduleGitDir
|
||||
)
|
||||
relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||
const containerSubmoduleGitDir = path.posix.join(
|
||||
'/github/workspace',
|
||||
relativeSubmoduleGitDir
|
||||
)
|
||||
|
||||
// Configure container includeIf
|
||||
await this.git.config(
|
||||
`includeIf.gitdir:${containerSubmoduleGitDir}.path`,
|
||||
containerCredentialsPath,
|
||||
false, // globalConfig?
|
||||
false, // add?
|
||||
configPath
|
||||
)
|
||||
}
|
||||
|
||||
if (this.settings.sshKey) {
|
||||
@@ -201,6 +243,10 @@ class GitAuthHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures SSH authentication by writing the SSH key and known hosts,
|
||||
* and setting up the GIT_SSH_COMMAND environment variable.
|
||||
*/
|
||||
private async configureSsh(): Promise<void> {
|
||||
if (!this.settings.sshKey) {
|
||||
return
|
||||
@@ -272,57 +318,127 @@ class GitAuthHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private async configureToken(
|
||||
configPath?: string,
|
||||
globalConfig?: boolean
|
||||
): Promise<void> {
|
||||
// Validate args
|
||||
assert.ok(
|
||||
(configPath && globalConfig) || (!configPath && !globalConfig),
|
||||
'Unexpected configureToken parameter combinations'
|
||||
)
|
||||
/**
|
||||
* Configures token-based authentication by creating a credentials config file
|
||||
* and setting up includeIf entries to reference it.
|
||||
* @param globalConfig Whether to configure global config instead of local
|
||||
*/
|
||||
private async configureToken(globalConfig?: boolean): Promise<void> {
|
||||
// Get the credentials config file path in RUNNER_TEMP
|
||||
const credentialsConfigPath = this.getCredentialsConfigPath()
|
||||
|
||||
// Default config path
|
||||
if (!configPath && !globalConfig) {
|
||||
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
|
||||
}
|
||||
|
||||
// Configure a placeholder value. This approach avoids the credential being captured
|
||||
// by process creation audit events, which are commonly logged. For more information,
|
||||
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
||||
// Write placeholder to the separate credentials config file using git config.
|
||||
// This approach avoids the credential being captured by process creation audit events,
|
||||
// which are commonly logged. For more information, refer to
|
||||
// https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
||||
await this.git.config(
|
||||
this.tokenConfigKey,
|
||||
this.tokenPlaceholderConfigValue,
|
||||
globalConfig
|
||||
false, // globalConfig?
|
||||
false, // add?
|
||||
credentialsConfigPath
|
||||
)
|
||||
|
||||
// Replace the placeholder
|
||||
await this.replaceTokenPlaceholder(configPath || '')
|
||||
}
|
||||
|
||||
private async replaceTokenPlaceholder(configPath: string): Promise<void> {
|
||||
assert.ok(configPath, 'configPath is not defined')
|
||||
let content = (await fs.promises.readFile(configPath)).toString()
|
||||
// Replace the placeholder in the credentials config file
|
||||
let content = (await fs.promises.readFile(credentialsConfigPath)).toString()
|
||||
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
|
||||
if (
|
||||
placeholderIndex < 0 ||
|
||||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
|
||||
) {
|
||||
throw new Error(`Unable to replace auth placeholder in ${configPath}`)
|
||||
throw new Error(
|
||||
`Unable to replace auth placeholder in ${credentialsConfigPath}`
|
||||
)
|
||||
}
|
||||
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
|
||||
content = content.replace(
|
||||
this.tokenPlaceholderConfigValue,
|
||||
this.tokenConfigValue
|
||||
)
|
||||
await fs.promises.writeFile(configPath, content)
|
||||
await fs.promises.writeFile(credentialsConfigPath, content)
|
||||
|
||||
// Add include or includeIf to reference the credentials config
|
||||
if (globalConfig) {
|
||||
// Global config file is temporary
|
||||
await this.git.config(
|
||||
'include.path',
|
||||
credentialsConfigPath,
|
||||
true // globalConfig?
|
||||
)
|
||||
} else {
|
||||
// Host git directory
|
||||
let gitDir = path.join(this.git.getWorkingDirectory(), '.git')
|
||||
gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||
|
||||
// Configure host includeIf
|
||||
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
|
||||
await this.git.config(hostIncludeKey, credentialsConfigPath)
|
||||
|
||||
// Configure host includeIf for worktrees
|
||||
const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`
|
||||
await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath)
|
||||
|
||||
// Container git directory
|
||||
const workingDirectory = this.git.getWorkingDirectory()
|
||||
const githubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||
assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
|
||||
let relativePath = path.relative(githubWorkspace, workingDirectory)
|
||||
relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||
const containerGitDir = path.posix.join(
|
||||
'/github/workspace',
|
||||
relativePath,
|
||||
'.git'
|
||||
)
|
||||
|
||||
// Container credentials config path
|
||||
const containerCredentialsPath = path.posix.join(
|
||||
'/github/runner_temp',
|
||||
path.basename(credentialsConfigPath)
|
||||
)
|
||||
|
||||
// Configure container includeIf
|
||||
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
|
||||
await this.git.config(containerIncludeKey, containerCredentialsPath)
|
||||
|
||||
// Configure container includeIf for worktrees
|
||||
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`
|
||||
await this.git.config(
|
||||
containerWorktreeIncludeKey,
|
||||
containerCredentialsPath
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates the path to the credentials config file in RUNNER_TEMP.
|
||||
* @returns The absolute path to the credentials config file
|
||||
*/
|
||||
private getCredentialsConfigPath(): string {
|
||||
if (this.credentialsConfigPath) {
|
||||
return this.credentialsConfigPath
|
||||
}
|
||||
|
||||
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
||||
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||
|
||||
// Create a unique filename for this checkout instance
|
||||
const configFileName = `git-credentials-${uuid()}.config`
|
||||
this.credentialsConfigPath = path.join(runnerTemp, configFileName)
|
||||
|
||||
core.debug(`Credentials config path: ${this.credentialsConfigPath}`)
|
||||
return this.credentialsConfigPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes SSH authentication configuration by cleaning up SSH keys,
|
||||
* known hosts files, and SSH command configurations.
|
||||
*/
|
||||
private async removeSsh(): Promise<void> {
|
||||
// SSH key
|
||||
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
|
||||
if (keyPath) {
|
||||
try {
|
||||
core.info(`Removing SSH key '${keyPath}'`)
|
||||
await io.rmRF(keyPath)
|
||||
} catch (err) {
|
||||
core.debug(`${(err as any)?.message ?? err}`)
|
||||
@@ -335,40 +451,149 @@ class GitAuthHelper {
|
||||
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
|
||||
if (knownHostsPath) {
|
||||
try {
|
||||
core.info(`Removing SSH known hosts '${knownHostsPath}'`)
|
||||
await io.rmRF(knownHostsPath)
|
||||
} catch {
|
||||
// Intentionally empty
|
||||
} catch (err) {
|
||||
core.debug(`${(err as any)?.message ?? err}`)
|
||||
core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`)
|
||||
}
|
||||
}
|
||||
|
||||
// SSH command
|
||||
core.info('Removing SSH command configuration')
|
||||
await this.removeGitConfig(SSH_COMMAND_KEY)
|
||||
await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes token-based authentication by cleaning up HTTP headers,
|
||||
* includeIf entries, and credentials config files.
|
||||
*/
|
||||
private async removeToken(): Promise<void> {
|
||||
// HTTP extra header
|
||||
// Remove HTTP extra header
|
||||
core.info('Removing HTTP extra header')
|
||||
await this.removeGitConfig(this.tokenConfigKey)
|
||||
}
|
||||
await this.removeSubmoduleGitConfig(this.tokenConfigKey)
|
||||
|
||||
private async removeGitConfig(
|
||||
configKey: string,
|
||||
submoduleOnly: boolean = false
|
||||
): Promise<void> {
|
||||
if (!submoduleOnly) {
|
||||
if (
|
||||
(await this.git.configExists(configKey)) &&
|
||||
!(await this.git.tryConfigUnset(configKey))
|
||||
) {
|
||||
// Load the config contents
|
||||
core.warning(`Failed to remove '${configKey}' from the git config`)
|
||||
}
|
||||
// Collect credentials config paths that need to be removed
|
||||
const credentialsPaths = new Set<string>()
|
||||
|
||||
// Remove includeIf entries that point to git-credentials-*.config files
|
||||
core.info('Removing includeIf entries pointing to credentials config files')
|
||||
const mainCredentialsPaths = await this.removeIncludeIfCredentials()
|
||||
mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
|
||||
|
||||
// Remove submodule includeIf entries that point to git-credentials-*.config files
|
||||
const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true)
|
||||
for (const configPath of submoduleConfigPaths) {
|
||||
const submoduleCredentialsPaths =
|
||||
await this.removeIncludeIfCredentials(configPath)
|
||||
submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
|
||||
}
|
||||
|
||||
// Remove credentials config files
|
||||
for (const credentialsPath of credentialsPaths) {
|
||||
// Only remove credentials config files if they are under RUNNER_TEMP
|
||||
const runnerTemp = process.env['RUNNER_TEMP']
|
||||
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||
if (credentialsPath.startsWith(runnerTemp)) {
|
||||
try {
|
||||
core.info(`Removing credentials config '${credentialsPath}'`)
|
||||
await io.rmRF(credentialsPath)
|
||||
} catch (err) {
|
||||
core.debug(`${(err as any)?.message ?? err}`)
|
||||
core.warning(
|
||||
`Failed to remove credentials config '${credentialsPath}'`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
core.debug(
|
||||
`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a git config key from the local repository config.
|
||||
* @param configKey The git config key to remove
|
||||
*/
|
||||
private async removeGitConfig(configKey: string): Promise<void> {
|
||||
if (
|
||||
(await this.git.configExists(configKey)) &&
|
||||
!(await this.git.tryConfigUnset(configKey))
|
||||
) {
|
||||
// Load the config contents
|
||||
core.warning(`Failed to remove '${configKey}' from the git config`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a git config key from all submodule configs.
|
||||
* @param configKey The git config key to remove
|
||||
*/
|
||||
private async removeSubmoduleGitConfig(configKey: string): Promise<void> {
|
||||
const pattern = regexpHelper.escape(configKey)
|
||||
await this.git.submoduleForeach(
|
||||
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
|
||||
// Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
|
||||
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes includeIf entries that point to git-credentials-*.config files.
|
||||
* @param configPath Optional path to a specific git config file to operate on
|
||||
* @returns Array of unique credentials config file paths that were found and removed
|
||||
*/
|
||||
private async removeIncludeIfCredentials(
|
||||
configPath?: string
|
||||
): Promise<string[]> {
|
||||
const credentialsPaths = new Set<string>()
|
||||
|
||||
try {
|
||||
// Get all includeIf.gitdir keys
|
||||
const keys = await this.git.tryGetConfigKeys(
|
||||
'^includeIf\\.gitdir:',
|
||||
false, // globalConfig?
|
||||
configPath
|
||||
)
|
||||
|
||||
for (const key of keys) {
|
||||
// Get all values for this key
|
||||
const values = await this.git.tryGetConfigValues(
|
||||
key,
|
||||
false, // globalConfig?
|
||||
configPath
|
||||
)
|
||||
if (values.length > 0) {
|
||||
// Remove only values that match git-credentials-<uuid>.config pattern
|
||||
for (const value of values) {
|
||||
if (this.testCredentialsConfigPath(value)) {
|
||||
credentialsPaths.add(value)
|
||||
await this.git.tryConfigUnsetValue(key, value, false, configPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors - this is cleanup code
|
||||
if (configPath) {
|
||||
core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`)
|
||||
} else {
|
||||
core.debug(`Error during includeIf cleanup: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(credentialsPaths)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a path matches the git-credentials-*.config pattern.
|
||||
* @param path The path to test
|
||||
* @returns True if the path matches the credentials config pattern
|
||||
*/
|
||||
private testCredentialsConfigPath(path: string): boolean {
|
||||
return /git-credentials-[0-9a-f-]+\.config$/i.test(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ export interface IGitCommandManager {
|
||||
configKey: string,
|
||||
configValue: string,
|
||||
globalConfig?: boolean,
|
||||
add?: boolean
|
||||
add?: boolean,
|
||||
configFile?: string
|
||||
): Promise<void>
|
||||
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
|
||||
fetch(
|
||||
@@ -41,6 +42,7 @@ export interface IGitCommandManager {
|
||||
}
|
||||
): Promise<void>
|
||||
getDefaultBranch(repositoryUrl: string): Promise<string>
|
||||
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
|
||||
getWorkingDirectory(): string
|
||||
init(): Promise<void>
|
||||
isDetached(): Promise<boolean>
|
||||
@@ -59,8 +61,24 @@ export interface IGitCommandManager {
|
||||
tagExists(pattern: string): Promise<boolean>
|
||||
tryClean(): Promise<boolean>
|
||||
tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
|
||||
tryConfigUnsetValue(
|
||||
configKey: string,
|
||||
configValue: string,
|
||||
globalConfig?: boolean,
|
||||
configFile?: string
|
||||
): Promise<boolean>
|
||||
tryDisableAutomaticGarbageCollection(): Promise<boolean>
|
||||
tryGetFetchUrl(): Promise<string>
|
||||
tryGetConfigValues(
|
||||
configKey: string,
|
||||
globalConfig?: boolean,
|
||||
configFile?: string
|
||||
): Promise<string[]>
|
||||
tryGetConfigKeys(
|
||||
pattern: string,
|
||||
globalConfig?: boolean,
|
||||
configFile?: string
|
||||
): Promise<string[]>
|
||||
tryReset(): Promise<boolean>
|
||||
version(): Promise<GitVersion>
|
||||
}
|
||||
@@ -223,9 +241,15 @@ class GitCommandManager {
|
||||
configKey: string,
|
||||
configValue: string,
|
||||
globalConfig?: boolean,
|
||||
add?: boolean
|
||||
add?: boolean,
|
||||
configFile?: string
|
||||
): Promise<void> {
|
||||
const args: string[] = ['config', globalConfig ? '--global' : '--local']
|
||||
const args: string[] = ['config']
|
||||
if (configFile) {
|
||||
args.push('--file', configFile)
|
||||
} else {
|
||||
args.push(globalConfig ? '--global' : '--local')
|
||||
}
|
||||
if (add) {
|
||||
args.push('--add')
|
||||
}
|
||||
@@ -323,6 +347,21 @@ class GitCommandManager {
|
||||
throw new Error('Unexpected output when retrieving default branch')
|
||||
}
|
||||
|
||||
async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> {
|
||||
// Get submodule config file paths.
|
||||
// Use `--show-origin` to get the config file path for each submodule.
|
||||
const output = await this.submoduleForeach(
|
||||
`git config --local --show-origin --name-only --get-regexp remote.origin.url`,
|
||||
recursive
|
||||
)
|
||||
|
||||
// Extract config file paths from the output (lines starting with "file:").
|
||||
const configPaths =
|
||||
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
|
||||
|
||||
return configPaths
|
||||
}
|
||||
|
||||
getWorkingDirectory(): string {
|
||||
return this.workingDirectory
|
||||
}
|
||||
@@ -455,6 +494,24 @@ class GitCommandManager {
|
||||
return output.exitCode === 0
|
||||
}
|
||||
|
||||
async tryConfigUnsetValue(
|
||||
configKey: string,
|
||||
configValue: string,
|
||||
globalConfig?: boolean,
|
||||
configFile?: string
|
||||
): Promise<boolean> {
|
||||
const args = ['config']
|
||||
if (configFile) {
|
||||
args.push('--file', configFile)
|
||||
} else {
|
||||
args.push(globalConfig ? '--global' : '--local')
|
||||
}
|
||||
args.push('--unset', configKey, configValue)
|
||||
|
||||
const output = await this.execGit(args, true)
|
||||
return output.exitCode === 0
|
||||
}
|
||||
|
||||
async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
|
||||
const output = await this.execGit(
|
||||
['config', '--local', 'gc.auto', '0'],
|
||||
@@ -481,6 +538,56 @@ class GitCommandManager {
|
||||
return stdout
|
||||
}
|
||||
|
||||
async tryGetConfigValues(
|
||||
configKey: string,
|
||||
globalConfig?: boolean,
|
||||
configFile?: string
|
||||
): Promise<string[]> {
|
||||
const args = ['config']
|
||||
if (configFile) {
|
||||
args.push('--file', configFile)
|
||||
} else {
|
||||
args.push(globalConfig ? '--global' : '--local')
|
||||
}
|
||||
args.push('--get-all', configKey)
|
||||
|
||||
const output = await this.execGit(args, true)
|
||||
|
||||
if (output.exitCode !== 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return output.stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(value => value.trim())
|
||||
}
|
||||
|
||||
async tryGetConfigKeys(
|
||||
pattern: string,
|
||||
globalConfig?: boolean,
|
||||
configFile?: string
|
||||
): Promise<string[]> {
|
||||
const args = ['config']
|
||||
if (configFile) {
|
||||
args.push('--file', configFile)
|
||||
} else {
|
||||
args.push(globalConfig ? '--global' : '--local')
|
||||
}
|
||||
args.push('--name-only', '--get-regexp', pattern)
|
||||
|
||||
const output = await this.execGit(args, true)
|
||||
|
||||
if (output.exitCode !== 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return output.stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(key => key.trim())
|
||||
}
|
||||
|
||||
async tryReset(): Promise<boolean> {
|
||||
const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
|
||||
return output.exitCode === 0
|
||||
|
||||
@@ -261,7 +261,8 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
const commitInfo = await git.log1()
|
||||
|
||||
// Log commit sha
|
||||
await git.log1("--format='%H'")
|
||||
const commitSHA = await git.log1('--format=%H')
|
||||
core.setOutput('commit', commitSHA.trim())
|
||||
|
||||
// Check for incorrect pull request merge commit
|
||||
await refHelper.checkCommitInfo(
|
||||
|
||||
@@ -19,6 +19,7 @@ async function run(): Promise<void> {
|
||||
|
||||
// Get sources
|
||||
await gitSourceProvider.getSource(sourceSettings)
|
||||
core.setOutput('ref', sourceSettings.ref)
|
||||
} finally {
|
||||
// Unregister problem matcher
|
||||
coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '')
|
||||
|
||||
@@ -120,7 +120,7 @@ function updateUsage(
|
||||
}
|
||||
|
||||
updateUsage(
|
||||
'actions/checkout@v4',
|
||||
'actions/checkout@v6',
|
||||
path.join(__dirname, '..', '..', 'action.yml'),
|
||||
path.join(__dirname, '..', '..', 'README.md')
|
||||
)
|
||||
|
||||
@@ -42,9 +42,13 @@ export async function getCheckoutInfo(
|
||||
result.ref = `refs/remotes/pull/${branch}`
|
||||
}
|
||||
// refs/tags/
|
||||
else if (upperRef.startsWith('REFS/')) {
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
result.ref = ref
|
||||
}
|
||||
// refs/
|
||||
else if (upperRef.startsWith('REFS/')) {
|
||||
result.ref = commit ? commit : ref
|
||||
}
|
||||
// Unqualified ref, check for a matching branch or tag
|
||||
else {
|
||||
if (await git.branchExists(true, `origin/${ref}`)) {
|
||||
|
||||
@@ -21,26 +21,61 @@ export function getFetchUrl(settings: IGitSourceSettings): string {
|
||||
}
|
||||
|
||||
export function getServerUrl(url?: string): URL {
|
||||
let urlValue =
|
||||
url && url.trim().length > 0
|
||||
? url
|
||||
: process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
return new URL(urlValue)
|
||||
let resolvedUrl = process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
if (hasContent(url, WhitespaceMode.Trim)) {
|
||||
resolvedUrl = url!
|
||||
}
|
||||
|
||||
return new URL(resolvedUrl)
|
||||
}
|
||||
|
||||
export function getServerApiUrl(url?: string): string {
|
||||
let apiUrl = 'https://api.github.com'
|
||||
if (hasContent(url, WhitespaceMode.Trim)) {
|
||||
let serverUrl = getServerUrl(url)
|
||||
if (isGhes(url)) {
|
||||
serverUrl.pathname = 'api/v3'
|
||||
} else {
|
||||
serverUrl.hostname = 'api.' + serverUrl.hostname
|
||||
}
|
||||
|
||||
if (isGhes(url)) {
|
||||
const serverUrl = getServerUrl(url)
|
||||
apiUrl = new URL(`${serverUrl.origin}/api/v3`).toString()
|
||||
return pruneSuffix(serverUrl.toString(), '/')
|
||||
}
|
||||
|
||||
return apiUrl
|
||||
return process.env['GITHUB_API_URL'] || 'https://api.github.com'
|
||||
}
|
||||
|
||||
export function isGhes(url?: string): boolean {
|
||||
const ghUrl = getServerUrl(url)
|
||||
const ghUrl = new URL(
|
||||
url || process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
)
|
||||
|
||||
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
|
||||
const hostname = ghUrl.hostname.trimEnd().toUpperCase()
|
||||
const isGitHubHost = hostname === 'GITHUB.COM'
|
||||
const isGitHubEnterpriseCloudHost = hostname.endsWith('.GHE.COM')
|
||||
const isLocalHost = hostname.endsWith('.LOCALHOST')
|
||||
|
||||
return !isGitHubHost && !isGitHubEnterpriseCloudHost && !isLocalHost
|
||||
}
|
||||
|
||||
function pruneSuffix(text: string, suffix: string) {
|
||||
if (hasContent(suffix, WhitespaceMode.Preserve) && text?.endsWith(suffix)) {
|
||||
return text.substring(0, text.length - suffix.length)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
enum WhitespaceMode {
|
||||
Trim,
|
||||
Preserve
|
||||
}
|
||||
|
||||
function hasContent(
|
||||
text: string | undefined,
|
||||
whitespaceMode: WhitespaceMode
|
||||
): boolean {
|
||||
let refinedText = text ?? ''
|
||||
if (whitespaceMode == WhitespaceMode.Trim) {
|
||||
refinedText = refinedText.trim()
|
||||
}
|
||||
return refinedText.length > 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user