mirror of
https://github.com/actions/checkout
synced 2026-07-06 22:17:29 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bb56186c3 | |||
| 8eb1f6a495 | |||
| 556e4c3cb0 | |||
| b32f140b0c | |||
| 2650dbd060 | |||
| aadec89964 | |||
| df0bcddf6d | |||
| 473055ba18 | |||
| b4ffde65f4 | |||
| 8530928916 | |||
| 7cdaf2fbc0 | |||
| 8ade135a41 | |||
| c533a0a4cf | |||
| 72f2cec99f | |||
| 3df4ab11eb | |||
| 8b5e8b7687 | |||
| 97a652b800 |
@@ -24,10 +24,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set Node.js 16.x
|
||||
- name: Set Node.js 20.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# If dist/ was different than expected, upload the expected version as an artifact
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
|
||||
with:
|
||||
name: dist
|
||||
|
||||
@@ -7,13 +7,18 @@ on:
|
||||
- main
|
||||
- releases/*
|
||||
|
||||
|
||||
# Note that when you see patterns like "ref: test-data/v2/basic" within this workflow,
|
||||
# these refer to "test-data" branches on this actions/checkout repo.
|
||||
# (For example, test-data/v2/basic -> https://github.com/actions/checkout/tree/test-data/v2/basic)
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 20.x
|
||||
- uses: actions/checkout@v3
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
@@ -72,6 +77,16 @@ jobs:
|
||||
shell: bash
|
||||
run: __test__/verify-side-by-side.sh
|
||||
|
||||
# Filter
|
||||
- name: Fetch filter
|
||||
uses: ./
|
||||
with:
|
||||
filter: 'blob:none'
|
||||
path: fetch-filter
|
||||
|
||||
- name: Verify fetch filter
|
||||
run: __test__/verify-fetch-filter.sh
|
||||
|
||||
# Sparse checkout
|
||||
- name: Sparse checkout
|
||||
uses: ./
|
||||
@@ -85,6 +100,16 @@ jobs:
|
||||
- name: Verify sparse checkout
|
||||
run: __test__/verify-sparse-checkout.sh
|
||||
|
||||
# Disabled sparse checkout in existing checkout
|
||||
- name: Disabled sparse checkout
|
||||
uses: ./
|
||||
with:
|
||||
path: sparse-checkout
|
||||
|
||||
- name: Verify disabled sparse checkout
|
||||
shell: bash
|
||||
run: set -x && ls -l sparse-checkout/src/git-command-manager.ts
|
||||
|
||||
# Sparse checkout (non-cone mode)
|
||||
- name: Sparse checkout (non-cone mode)
|
||||
uses: ./
|
||||
@@ -165,7 +190,7 @@ jobs:
|
||||
test-proxy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: alpine/git:latest
|
||||
image: ghcr.io/actions/test-ubuntu-git:main.20240221.114913.703z
|
||||
options: --dns 127.0.0.1
|
||||
services:
|
||||
squid-proxy:
|
||||
@@ -269,4 +294,4 @@ jobs:
|
||||
- name: Fix Checkout v3
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: v3
|
||||
path: v3
|
||||
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
type: choice
|
||||
description: The major version to update
|
||||
options:
|
||||
- v4
|
||||
- v3
|
||||
- v2
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Publish test-ubuntu-git Container
|
||||
|
||||
on:
|
||||
# Use an on demand workflow trigger.
|
||||
# (Forked copies of actions/checkout won't have permission to update GHCR.io/actions,
|
||||
# so avoid trigger events that run automatically.)
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
description: 'Publish to ghcr.io? (main branch only)'
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: actions/test-ubuntu-git
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# 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.0.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Format Timestamp
|
||||
id: timestamp
|
||||
# Use `date` with a custom format to achieve the key=value format GITHUB_OUTPUT expects.
|
||||
run: date -u "+now=%Y%m%d.%H%M%S.%3NZ" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Issue Image Publish Warning
|
||||
if: ${{ inputs.publish && github.ref_name != 'main' }}
|
||||
run: echo "::warning::test-ubuntu-git images can only be published from the actions/checkout 'main' branch. Workflow will continue with push/publish disabled."
|
||||
|
||||
# Use `docker/build-push-action` to build (and optionally publish) the image.
|
||||
- name: Build Docker Image (with optional Push)
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: images/test-ubuntu-git.Dockerfile
|
||||
# For now, attempts to push to ghcr.io must target the `main` branch.
|
||||
# In the future, consider also allowing attempts from `releases/*` branches.
|
||||
push: ${{ inputs.publish && github.ref_name == 'main' }}
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}.${{ steps.timestamp.outputs.now }}
|
||||
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## v4.1.2
|
||||
- Fix: Disable sparse checkout whenever `sparse-checkout` option is not present @dscho in https://github.com/actions/checkout/pull/1598
|
||||
|
||||
## v4.1.1
|
||||
- Correct link to GitHub Docs by @peterbe in https://github.com/actions/checkout/pull/1511
|
||||
- Link to release page from what's new section by @cory-miller in https://github.com/actions/checkout/pull/1514
|
||||
|
||||
## v4.1.0
|
||||
- [Add support for partial checkout filters](https://github.com/actions/checkout/pull/1396)
|
||||
|
||||
## v4.0.0
|
||||
- [Support fetching without the --progress option](https://github.com/actions/checkout/pull/1067)
|
||||
- [Update to node20](https://github.com/actions/checkout/pull/1436)
|
||||
|
||||
## v3.6.0
|
||||
- [Fix: Mark test scripts with Bash'isms to be run via Bash](https://github.com/actions/checkout/pull/1377)
|
||||
- [Add option to fetch tags even if fetch-depth > 0](https://github.com/actions/checkout/pull/579)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
* @actions/actions-runtime
|
||||
* @actions/actions-launch
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[](https://github.com/actions/checkout/actions/workflows/test.yml)
|
||||
|
||||
# Checkout V3
|
||||
# Checkout V4
|
||||
|
||||
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
|
||||
|
||||
Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. Set `fetch-depth: 0` to fetch all history for all branches and tags. Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events.
|
||||
Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. Set `fetch-depth: 0` to fetch all history for all branches and tags. Refer [here](https://docs.github.com/actions/using-workflows/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events.
|
||||
|
||||
The auth token is persisted in the local git config. This enables your scripts to run authenticated git commands. The token is removed during post-job cleanup. Set `persist-credentials: false` to opt-out.
|
||||
|
||||
@@ -12,14 +12,13 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
|
||||
# What's new
|
||||
|
||||
- Updated to the node16 runtime by default
|
||||
- This requires a minimum [Actions Runner](https://github.com/actions/runner/releases/tag/v2.285.0) version of v2.285.0 to run, which is by default available in GHES 3.4 or later.
|
||||
Please refer to the [release page](https://github.com/actions/checkout/releases/latest) for the latest release notes.
|
||||
|
||||
# Usage
|
||||
|
||||
<!-- start usage -->
|
||||
```yaml
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Repository name with owner. For example, actions/checkout
|
||||
# Default: ${{ github.repository }}
|
||||
@@ -74,8 +73,12 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
# Default: true
|
||||
clean: ''
|
||||
|
||||
# Partially clone against a given filter. Overrides sparse-checkout if set.
|
||||
# Default: null
|
||||
filter: ''
|
||||
|
||||
# Do a sparse checkout on given patterns. Each pattern should be separated with
|
||||
# new lines
|
||||
# new lines.
|
||||
# Default: null
|
||||
sparse-checkout: ''
|
||||
|
||||
@@ -91,6 +94,10 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
# Default: false
|
||||
fetch-tags: ''
|
||||
|
||||
# Whether to show progress status output when fetching.
|
||||
# Default: true
|
||||
show-progress: ''
|
||||
|
||||
# Whether to download Git-LFS files
|
||||
# Default: false
|
||||
lfs: ''
|
||||
@@ -114,15 +121,6 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
# running from unless specified. Example URLs are https://github.com or
|
||||
# https://my-ghes-server.example.com
|
||||
github-server-url: ''
|
||||
|
||||
# Required to check out fork pull request code from a workflow triggered by
|
||||
# `pull_request_target` or `workflow_run`. These workflows run with the base
|
||||
# repository's GITHUB_TOKEN, secrets, default-branch cache scope, and runner
|
||||
# access; fetching and executing a fork's code in that trusted context commonly
|
||||
# leads to "pwn request" vulnerabilities. Set to `true` only after reviewing the
|
||||
# risks at https://gh.io/securely-using-pull_request_target.
|
||||
# Default: false
|
||||
allow-unsafe-pr-checkout: ''
|
||||
```
|
||||
<!-- end usage -->
|
||||
|
||||
@@ -144,7 +142,7 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
## Fetch only the root files
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .
|
||||
```
|
||||
@@ -152,7 +150,7 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
## Fetch only the root files and `.github` and `src` folder
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github
|
||||
@@ -162,7 +160,7 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
## Fetch only a single file
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
README.md
|
||||
@@ -172,7 +170,7 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
## Fetch all history for all tags and branches
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
```
|
||||
@@ -180,7 +178,7 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
## Checkout a different branch
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: my-branch
|
||||
```
|
||||
@@ -188,7 +186,7 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
## Checkout HEAD^
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- run: git checkout HEAD^
|
||||
@@ -198,12 +196,12 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: main
|
||||
|
||||
- name: Checkout tools repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: my-org/my-tools
|
||||
path: my-tools
|
||||
@@ -214,10 +212,10 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout tools repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: my-org/my-tools
|
||||
path: my-tools
|
||||
@@ -228,12 +226,12 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: main
|
||||
|
||||
- name: Checkout private tools
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: my-org/my-private-tools
|
||||
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
|
||||
@@ -246,7 +244,7 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
|
||||
## Checkout pull request HEAD commit instead of merge commit
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
```
|
||||
@@ -262,7 +260,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
```
|
||||
|
||||
## Push a commit using the built-in token
|
||||
@@ -273,7 +271,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
date > generated.txt
|
||||
git config user.name github-actions
|
||||
|
||||
@@ -727,6 +727,7 @@ async function setup(testName: string): Promise<void> {
|
||||
branchDelete: jest.fn(),
|
||||
branchExists: jest.fn(),
|
||||
branchList: jest.fn(),
|
||||
disableSparseCheckout: jest.fn(),
|
||||
sparseCheckout: jest.fn(),
|
||||
sparseCheckoutNonConeMode: jest.fn(),
|
||||
checkout: jest.fn(),
|
||||
@@ -802,10 +803,12 @@ async function setup(testName: string): Promise<void> {
|
||||
authToken: 'some auth token',
|
||||
clean: true,
|
||||
commit: '',
|
||||
filter: undefined,
|
||||
sparseCheckout: [],
|
||||
sparseCheckoutConeMode: true,
|
||||
fetchDepth: 1,
|
||||
fetchTags: false,
|
||||
showProgress: true,
|
||||
lfs: false,
|
||||
submodules: false,
|
||||
nestedSubmodules: false,
|
||||
@@ -819,8 +822,7 @@ async function setup(testName: string): Promise<void> {
|
||||
sshStrict: true,
|
||||
workflowOrganizationId: 123456,
|
||||
setSafeDirectory: true,
|
||||
githubServerUrl: githubServerUrl,
|
||||
allowUnsafePrCheckout: false
|
||||
githubServerUrl: githubServerUrl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -135,7 +135,6 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
'protocol.version=2',
|
||||
'fetch',
|
||||
'--prune',
|
||||
'--progress',
|
||||
'--no-recurse-submodules',
|
||||
'--filter=filterValue',
|
||||
'origin',
|
||||
@@ -174,7 +173,6 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
'fetch',
|
||||
'--no-tags',
|
||||
'--prune',
|
||||
'--progress',
|
||||
'--no-recurse-submodules',
|
||||
'--filter=filterValue',
|
||||
'origin',
|
||||
@@ -213,7 +211,6 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
'fetch',
|
||||
'--no-tags',
|
||||
'--prune',
|
||||
'--progress',
|
||||
'--no-recurse-submodules',
|
||||
'--filter=filterValue',
|
||||
'--depth=1',
|
||||
@@ -252,7 +249,6 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
'protocol.version=2',
|
||||
'fetch',
|
||||
'--prune',
|
||||
'--progress',
|
||||
'--no-recurse-submodules',
|
||||
'--filter=filterValue',
|
||||
'--depth=1',
|
||||
@@ -263,4 +259,120 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when showProgress is true', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
const doSparseCheckout = false
|
||||
git = await commandManager.createCommandManager(
|
||||
workingDirectory,
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
const refSpec = ['refspec1', 'refspec2']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
showProgress: true
|
||||
}
|
||||
|
||||
await git.fetch(refSpec, options)
|
||||
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[
|
||||
'-c',
|
||||
'protocol.version=2',
|
||||
'fetch',
|
||||
'--no-tags',
|
||||
'--prune',
|
||||
'--no-recurse-submodules',
|
||||
'--progress',
|
||||
'--filter=filterValue',
|
||||
'origin',
|
||||
'refspec1',
|
||||
'refspec2'
|
||||
],
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when fetchDepth is 42 and showProgress is true', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
const doSparseCheckout = false
|
||||
git = await commandManager.createCommandManager(
|
||||
workingDirectory,
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
const refSpec = ['refspec1', 'refspec2']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
fetchDepth: 42,
|
||||
showProgress: true
|
||||
}
|
||||
|
||||
await git.fetch(refSpec, options)
|
||||
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[
|
||||
'-c',
|
||||
'protocol.version=2',
|
||||
'fetch',
|
||||
'--no-tags',
|
||||
'--prune',
|
||||
'--no-recurse-submodules',
|
||||
'--progress',
|
||||
'--filter=filterValue',
|
||||
'--depth=42',
|
||||
'origin',
|
||||
'refspec1',
|
||||
'refspec2'
|
||||
],
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('should call execGit with the correct arguments when fetchTags is true and showProgress is true', async () => {
|
||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||
|
||||
const workingDirectory = 'test'
|
||||
const lfs = false
|
||||
const doSparseCheckout = false
|
||||
git = await commandManager.createCommandManager(
|
||||
workingDirectory,
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
const refSpec = ['refspec1', 'refspec2']
|
||||
const options = {
|
||||
filter: 'filterValue',
|
||||
fetchTags: true,
|
||||
showProgress: true
|
||||
}
|
||||
|
||||
await git.fetch(refSpec, options)
|
||||
|
||||
expect(mockExec).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[
|
||||
'-c',
|
||||
'protocol.version=2',
|
||||
'fetch',
|
||||
'--prune',
|
||||
'--no-recurse-submodules',
|
||||
'--progress',
|
||||
'--filter=filterValue',
|
||||
'origin',
|
||||
'refspec1',
|
||||
'refspec2'
|
||||
],
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -462,6 +462,7 @@ async function setup(testName: string): Promise<void> {
|
||||
branchList: jest.fn(async () => {
|
||||
return []
|
||||
}),
|
||||
disableSparseCheckout: jest.fn(),
|
||||
sparseCheckout: jest.fn(),
|
||||
sparseCheckoutNonConeMode: jest.fn(),
|
||||
checkout: jest.fn(),
|
||||
|
||||
@@ -79,17 +79,18 @@ describe('input-helper tests', () => {
|
||||
expect(settings.clean).toBe(true)
|
||||
expect(settings.commit).toBeTruthy()
|
||||
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
|
||||
expect(settings.filter).toBe(undefined)
|
||||
expect(settings.sparseCheckout).toBe(undefined)
|
||||
expect(settings.sparseCheckoutConeMode).toBe(true)
|
||||
expect(settings.fetchDepth).toBe(1)
|
||||
expect(settings.fetchTags).toBe(false)
|
||||
expect(settings.showProgress).toBe(true)
|
||||
expect(settings.lfs).toBe(false)
|
||||
expect(settings.ref).toBe('refs/heads/some-ref')
|
||||
expect(settings.repositoryName).toBe('some-repo')
|
||||
expect(settings.repositoryOwner).toBe('some-owner')
|
||||
expect(settings.repositoryPath).toBe(gitHubWorkspace)
|
||||
expect(settings.setSafeDirectory).toBe(true)
|
||||
expect(settings.allowUnsafePrCheckout).toBe(false)
|
||||
})
|
||||
|
||||
it('qualifies ref', async () => {
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
import * as github from '@actions/github'
|
||||
import {assertSafePrCheckout} from '../lib/unsafe-pr-checkout-helper'
|
||||
|
||||
// Shallow clone original @actions/github context
|
||||
const originalContext = {...github.context}
|
||||
const originalEventName = github.context.eventName
|
||||
const originalPayload = github.context.payload
|
||||
|
||||
const BASE_REPO_ID = 100
|
||||
const FORK_REPO_ID = 200
|
||||
const PR_HEAD_SHA = '1111111111111111111111111111111111111111'
|
||||
const PR_MERGE_SHA = '2222222222222222222222222222222222222222'
|
||||
const SAFE_BASE_SHA = '3333333333333333333333333333333333333333'
|
||||
const WORKFLOW_RUN_HEAD_COMMIT_SHA = '4444444444444444444444444444444444444444'
|
||||
const BASE_QUALIFIED_REPO = 'some-owner/some-repo'
|
||||
const FORK_QUALIFIED_REPO = 'another-repo/fork'
|
||||
|
||||
function setContext(eventName: string, payload: object): void {
|
||||
;(github.context as {eventName: string}).eventName = eventName
|
||||
;(github.context as {payload: object}).payload = payload
|
||||
}
|
||||
|
||||
function forkPullRequestTargetPayload(): object {
|
||||
return {
|
||||
repository: {id: BASE_REPO_ID},
|
||||
pull_request: {
|
||||
head: {
|
||||
sha: PR_HEAD_SHA,
|
||||
repo: {id: FORK_REPO_ID, full_name: FORK_QUALIFIED_REPO}
|
||||
},
|
||||
merge_commit_sha: PR_MERGE_SHA
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sameRepoPullRequestTargetPayload(): object {
|
||||
return {
|
||||
repository: {id: BASE_REPO_ID},
|
||||
pull_request: {
|
||||
head: {
|
||||
sha: PR_HEAD_SHA,
|
||||
repo: {id: BASE_REPO_ID, full_name: BASE_QUALIFIED_REPO}
|
||||
},
|
||||
merge_commit_sha: PR_MERGE_SHA
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function forkWorkflowRunPayload(): object {
|
||||
return {
|
||||
repository: {id: BASE_REPO_ID},
|
||||
workflow_run: {
|
||||
event: 'pull_request',
|
||||
head_commit: {id: WORKFLOW_RUN_HEAD_COMMIT_SHA},
|
||||
head_repository: {id: FORK_REPO_ID, full_name: FORK_QUALIFIED_REPO}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('unsafe-pr-checkout-helper', () => {
|
||||
beforeAll(() => {
|
||||
jest.spyOn(github.context, 'repo', 'get').mockReturnValue({
|
||||
owner: 'some-owner',
|
||||
repo: 'some-repo'
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
;(github.context as {eventName: string}).eventName = originalEventName
|
||||
;(github.context as {payload: object}).payload = originalPayload
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
;(github.context as {eventName: string}).eventName =
|
||||
originalContext.eventName
|
||||
;(github.context as {payload: object}).payload = originalContext.payload
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('allows pull_request events untouched', () => {
|
||||
setContext('pull_request', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: 'attacker/fork',
|
||||
ref: 'refs/pull/1/merge',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('allows pull_request_target default checkout (base branch)', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: 'refs/heads/main',
|
||||
commit: SAFE_BASE_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('allows same-repo pull_request_target checkout of PR head', () => {
|
||||
setContext('pull_request_target', sameRepoPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: PR_HEAD_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('refuses pull_request_target fork PR head SHA checkout', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: PR_HEAD_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow(/Refusing to check out fork pull request code/)
|
||||
})
|
||||
|
||||
it('refuses pull_request_target fork PR merge_commit_sha checkout', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: PR_MERGE_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow(/allow-unsafe-pr-checkout/)
|
||||
})
|
||||
|
||||
it('refuses pull_request_target fork PR ref pattern (head)', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: 'refs/pull/42/head',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('refuses pull_request_target fork PR ref pattern (merge)', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: 'refs/pull/42/merge',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('refuses pull_request_target when repository points at the fork', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: FORK_QUALIFIED_REPO,
|
||||
ref: 'refs/heads/main',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('allows pull_request_target checkout of an unrelated third-party repo', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: 'some-other/unrelated',
|
||||
ref: 'refs/heads/main',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('refuses pull_request_target ignoring repository case differences', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: FORK_QUALIFIED_REPO.toUpperCase(),
|
||||
ref: '',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('refuses pull_request_target ignoring commit SHA case differences', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: PR_HEAD_SHA.toUpperCase(),
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('allows pull_request_target fork PR checkout when opted in', () => {
|
||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: 'refs/pull/42/merge',
|
||||
commit: '',
|
||||
allowUnsafePrCheckout: true
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('refuses workflow_run fork PR head_commit.id checkout', () => {
|
||||
setContext('workflow_run', forkWorkflowRunPayload())
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: WORKFLOW_RUN_HEAD_COMMIT_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('refuses workflow_run with pull_request_target underlying event', () => {
|
||||
const payload = forkWorkflowRunPayload() as {
|
||||
workflow_run: {event: string}
|
||||
}
|
||||
payload.workflow_run.event = 'pull_request_target'
|
||||
setContext('workflow_run', payload)
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: WORKFLOW_RUN_HEAD_COMMIT_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('allows workflow_run same-repo PR (head_repository.id matches base)', () => {
|
||||
const payload = forkWorkflowRunPayload() as {
|
||||
workflow_run: {head_repository: {id: number}}
|
||||
}
|
||||
payload.workflow_run.head_repository.id = BASE_REPO_ID
|
||||
setContext('workflow_run', payload)
|
||||
expect(() =>
|
||||
assertSafePrCheckout({
|
||||
qualifiedRepository: BASE_QUALIFIED_REPO,
|
||||
ref: '',
|
||||
commit: WORKFLOW_RUN_HEAD_COMMIT_SHA,
|
||||
allowUnsafePrCheckout: false
|
||||
})
|
||||
).not.toThrow()
|
||||
})
|
||||
})
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Verify .git folder
|
||||
if [ ! -d "./fetch-filter/.git" ]; then
|
||||
echo "Expected ./fetch-filter/.git folder to exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify .git/config contains partialclonefilter
|
||||
|
||||
CLONE_FILTER=$(git -C fetch-filter config --local --get remote.origin.partialclonefilter)
|
||||
|
||||
if [ "$CLONE_FILTER" != "blob:none" ]; then
|
||||
echo "Expected ./fetch-filter/.git/config to have 'remote.origin.partialclonefilter' set to 'blob:none'"
|
||||
exit 1
|
||||
fi
|
||||
+10
-11
@@ -53,10 +53,15 @@ inputs:
|
||||
clean:
|
||||
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
|
||||
default: true
|
||||
filter:
|
||||
description: >
|
||||
Partially clone against a given filter.
|
||||
Overrides sparse-checkout if set.
|
||||
default: null
|
||||
sparse-checkout:
|
||||
description: >
|
||||
Do a sparse checkout on given patterns.
|
||||
Each pattern should be separated with new lines
|
||||
Each pattern should be separated with new lines.
|
||||
default: null
|
||||
sparse-checkout-cone-mode:
|
||||
description: >
|
||||
@@ -68,6 +73,9 @@ inputs:
|
||||
fetch-tags:
|
||||
description: 'Whether to fetch tags, even if fetch-depth > 0.'
|
||||
default: false
|
||||
show-progress:
|
||||
description: 'Whether to show progress status output when fetching.'
|
||||
default: true
|
||||
lfs:
|
||||
description: 'Whether to download Git-LFS files'
|
||||
default: false
|
||||
@@ -86,16 +94,7 @@ inputs:
|
||||
github-server-url:
|
||||
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
|
||||
required: false
|
||||
allow-unsafe-pr-checkout:
|
||||
description: >
|
||||
Required to check out fork pull request code from a workflow triggered by
|
||||
`pull_request_target` or `workflow_run`. These workflows run with the
|
||||
base repository's GITHUB_TOKEN, secrets, default-branch cache scope, and
|
||||
runner access; fetching and executing a fork's code in that trusted
|
||||
context commonly leads to "pwn request" vulnerabilities. Set to `true`
|
||||
only after reviewing the risks at https://gh.io/securely-using-pull_request_target.
|
||||
default: false
|
||||
runs:
|
||||
using: node16
|
||||
using: node20
|
||||
main: dist/index.js
|
||||
post: dist/index.js
|
||||
|
||||
Vendored
+29
-113
@@ -576,6 +576,11 @@ class GitCommandManager {
|
||||
return result;
|
||||
});
|
||||
}
|
||||
disableSparseCheckout() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
yield this.execGit(['sparse-checkout', 'disable']);
|
||||
});
|
||||
}
|
||||
sparseCheckout(sparseCheckout) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
yield this.execGit(['sparse-checkout', 'set', ...sparseCheckout]);
|
||||
@@ -640,7 +645,10 @@ class GitCommandManager {
|
||||
if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
|
||||
args.push('--no-tags');
|
||||
}
|
||||
args.push('--prune', '--progress', '--no-recurse-submodules');
|
||||
args.push('--prune', '--no-recurse-submodules');
|
||||
if (options.showProgress) {
|
||||
args.push('--progress');
|
||||
}
|
||||
if (options.filter) {
|
||||
args.push(`--filter=${options.filter}`);
|
||||
}
|
||||
@@ -1241,8 +1249,12 @@ function getSource(settings) {
|
||||
// Fetch
|
||||
core.startGroup('Fetching the repository');
|
||||
const fetchOptions = {};
|
||||
if (settings.sparseCheckout)
|
||||
if (settings.filter) {
|
||||
fetchOptions.filter = settings.filter;
|
||||
}
|
||||
else if (settings.sparseCheckout) {
|
||||
fetchOptions.filter = 'blob:none';
|
||||
}
|
||||
if (settings.fetchDepth <= 0) {
|
||||
// Fetch all branches and tags
|
||||
let refSpec = refHelper.getRefSpecForAllHistory(settings.ref, settings.commit);
|
||||
@@ -1275,7 +1287,10 @@ function getSource(settings) {
|
||||
core.endGroup();
|
||||
}
|
||||
// Sparse checkout
|
||||
if (settings.sparseCheckout) {
|
||||
if (!settings.sparseCheckout) {
|
||||
yield git.disableSparseCheckout();
|
||||
}
|
||||
else {
|
||||
core.startGroup('Setting up sparse checkout');
|
||||
if (settings.sparseCheckoutConeMode) {
|
||||
yield git.sparseCheckout(settings.sparseCheckout);
|
||||
@@ -1664,7 +1679,6 @@ const core = __importStar(__nccwpck_require__(2186));
|
||||
const fsHelper = __importStar(__nccwpck_require__(7219));
|
||||
const github = __importStar(__nccwpck_require__(5438));
|
||||
const path = __importStar(__nccwpck_require__(1017));
|
||||
const unsafePrCheckoutHelper = __importStar(__nccwpck_require__(843));
|
||||
const workflowContextHelper = __importStar(__nccwpck_require__(9568));
|
||||
function getInputs() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
@@ -1721,6 +1735,12 @@ function getInputs() {
|
||||
// Clean
|
||||
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE';
|
||||
core.debug(`clean = ${result.clean}`);
|
||||
// Filter
|
||||
const filter = core.getInput('filter');
|
||||
if (filter) {
|
||||
result.filter = filter;
|
||||
}
|
||||
core.debug(`filter = ${result.filter}`);
|
||||
// Sparse checkout
|
||||
const sparseCheckout = core.getMultilineInput('sparse-checkout');
|
||||
if (sparseCheckout.length) {
|
||||
@@ -1740,6 +1760,10 @@ function getInputs() {
|
||||
result.fetchTags =
|
||||
(core.getInput('fetch-tags') || 'false').toUpperCase() === 'TRUE';
|
||||
core.debug(`fetch tags = ${result.fetchTags}`);
|
||||
// Show fetch progress
|
||||
result.showProgress =
|
||||
(core.getInput('show-progress') || 'true').toUpperCase() === 'TRUE';
|
||||
core.debug(`show progress = ${result.showProgress}`);
|
||||
// LFS
|
||||
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE';
|
||||
core.debug(`lfs = ${result.lfs}`);
|
||||
@@ -1774,17 +1798,6 @@ function getInputs() {
|
||||
// Determine the GitHub URL that the repository is being hosted from
|
||||
result.githubServerUrl = core.getInput('github-server-url');
|
||||
core.debug(`GitHub Host URL = ${result.githubServerUrl}`);
|
||||
// Allow unsafe PR checkout (opt-in for pull_request_target / workflow_run fork PRs)
|
||||
result.allowUnsafePrCheckout =
|
||||
(core.getInput('allow-unsafe-pr-checkout') || 'false').toUpperCase() ===
|
||||
'TRUE';
|
||||
core.debug(`allow unsafe PR checkout = ${result.allowUnsafePrCheckout}`);
|
||||
unsafePrCheckoutHelper.assertSafePrCheckout({
|
||||
qualifiedRepository,
|
||||
ref: result.ref,
|
||||
commit: result.commit,
|
||||
allowUnsafePrCheckout: result.allowUnsafePrCheckout
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
@@ -1911,7 +1924,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.fromPayload = exports.checkCommitInfo = exports.testRef = exports.getRefSpec = exports.getRefSpecForAllHistory = exports.getCheckoutInfo = exports.tagsRefSpec = void 0;
|
||||
exports.checkCommitInfo = exports.testRef = exports.getRefSpec = exports.getRefSpecForAllHistory = exports.getCheckoutInfo = exports.tagsRefSpec = void 0;
|
||||
const core = __importStar(__nccwpck_require__(2186));
|
||||
const github = __importStar(__nccwpck_require__(5438));
|
||||
const url_helper_1 = __nccwpck_require__(9437);
|
||||
@@ -2133,7 +2146,6 @@ exports.checkCommitInfo = checkCommitInfo;
|
||||
function fromPayload(path) {
|
||||
return select(github.context.payload, path);
|
||||
}
|
||||
exports.fromPayload = fromPayload;
|
||||
function select(obj, path) {
|
||||
if (!obj) {
|
||||
return undefined;
|
||||
@@ -2340,102 +2352,6 @@ if (!exports.IsPost) {
|
||||
}
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 843:
|
||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.assertSafePrCheckout = void 0;
|
||||
const github = __importStar(__nccwpck_require__(5438));
|
||||
const ref_helper_1 = __nccwpck_require__(8601);
|
||||
const PR_REF_PATTERN = /^refs\/pull\/[0-9]+\/(?:head|merge)$/;
|
||||
function assertSafePrCheckout(input) {
|
||||
if (input.allowUnsafePrCheckout) {
|
||||
return;
|
||||
}
|
||||
const eventName = github.context.eventName;
|
||||
if (eventName !== 'pull_request_target' && eventName !== 'workflow_run') {
|
||||
return;
|
||||
}
|
||||
const baseRepoId = (0, ref_helper_1.fromPayload)('repository.id');
|
||||
if (typeof baseRepoId !== 'number') {
|
||||
return;
|
||||
}
|
||||
let prHeadRepoId;
|
||||
let prHeadRepoFullName;
|
||||
const prShas = [];
|
||||
if (eventName === 'pull_request_target') {
|
||||
prHeadRepoId = (0, ref_helper_1.fromPayload)('pull_request.head.repo.id');
|
||||
prHeadRepoFullName = (0, ref_helper_1.fromPayload)('pull_request.head.repo.full_name');
|
||||
pushIfSha(prShas, (0, ref_helper_1.fromPayload)('pull_request.head.sha'));
|
||||
pushIfSha(prShas, (0, ref_helper_1.fromPayload)('pull_request.merge_commit_sha'));
|
||||
}
|
||||
else {
|
||||
const wrEvent = (0, ref_helper_1.fromPayload)('workflow_run.event');
|
||||
if (typeof wrEvent !== 'string' || !wrEvent.startsWith('pull_request')) {
|
||||
return;
|
||||
}
|
||||
prHeadRepoId = (0, ref_helper_1.fromPayload)('workflow_run.head_repository.id');
|
||||
prHeadRepoFullName = (0, ref_helper_1.fromPayload)('workflow_run.head_repository.full_name');
|
||||
pushIfSha(prShas, (0, ref_helper_1.fromPayload)('workflow_run.head_commit.id'));
|
||||
// For `pull_request_target`-triggered workflow_run, `head_sha` is the base
|
||||
// default branch SHA (not the PR head)
|
||||
if (wrEvent !== 'pull_request_target') {
|
||||
pushIfSha(prShas, (0, ref_helper_1.fromPayload)('workflow_run.head_sha'));
|
||||
}
|
||||
}
|
||||
// (A) Fork PR?
|
||||
if (typeof prHeadRepoId !== 'number' || prHeadRepoId === baseRepoId) {
|
||||
return;
|
||||
}
|
||||
// (B) We cannot check for all fork PR refs so check to see
|
||||
// if the resolved input points to the fork PR sha we have in the payload
|
||||
const repositoryMatchesPrHead = typeof prHeadRepoFullName === 'string' &&
|
||||
input.qualifiedRepository.toLowerCase() === prHeadRepoFullName.toLowerCase();
|
||||
const refMatchesPullPattern = PR_REF_PATTERN.test(input.ref);
|
||||
const commitMatchesPrHeadSha = !!input.commit && prShas.includes(input.commit.toLowerCase());
|
||||
if (!repositoryMatchesPrHead &&
|
||||
!refMatchesPullPattern &&
|
||||
!commitMatchesPrHeadSha) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`Refusing to check out fork pull request code from a '${eventName}' workflow. ` +
|
||||
`This workflow runs with the base repository's GITHUB_TOKEN, secrets, default-branch ` +
|
||||
`cache scope, and runner access. Fetching and executing a fork's code in that trusted ` +
|
||||
`context commonly leads to "pwn request" vulnerabilities. To opt in, review the risks ` +
|
||||
`at https://gh.io/securely-using-pull_request_target and set 'allow-unsafe-pr-checkout: true' ` +
|
||||
`on the actions/checkout step.`);
|
||||
}
|
||||
exports.assertSafePrCheckout = assertSafePrCheckout;
|
||||
function pushIfSha(target, value) {
|
||||
if (typeof value === 'string' && value.length > 0) {
|
||||
target.push(value.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 9437:
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Defines the test-ubuntu-git Container Image.
|
||||
# Consumed by actions/checkout CI/CD validation workflows.
|
||||
|
||||
FROM ubuntu:latest
|
||||
|
||||
RUN apt update
|
||||
RUN apt install -y git
|
||||
|
||||
LABEL org.opencontainers.image.title="Ubuntu + git (validation image)"
|
||||
LABEL org.opencontainers.image.description="Ubuntu image with git pre-installed. Intended primarily for testing `actions/checkout` during CI/CD workflows."
|
||||
LABEL org.opencontainers.image.documentation="https://github.com/actions/checkout/tree/main/images/test-ubuntu-git.md"
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
||||
@@ -0,0 +1,15 @@
|
||||
# `test-ubuntu-git` Container Image
|
||||
|
||||
[](https://github.com/actions/checkout/actions/workflows/update-test-ubuntu-git.yml)
|
||||
|
||||
## Purpose
|
||||
|
||||
`test-ubuntu-git` is a container image hosted on the GitHub Container Registry, `ghcr.io`.
|
||||
|
||||
It is intended primarily for testing the [`actions/checkout` repository](https://github.com/actions/checkout) as part of `actions/checkout`'s CI/CD workflows.
|
||||
|
||||
The composition of `test-ubuntu-git` is intentionally minimal. It is comprised of [git](https://git-scm.com/) installed on top of a [base-level ubuntu image](https://hub.docker.com/_/ubuntu/tags).
|
||||
|
||||
# License
|
||||
|
||||
`test-ubuntu-git` is released under the [MIT License](/LICENSE).
|
||||
Generated
+195
-16046
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "checkout",
|
||||
"version": "3.6.0",
|
||||
"version": "4.1.2",
|
||||
"description": "checkout action",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/node": "^12.7.12",
|
||||
"@types/node": "^20.5.3",
|
||||
"@types/uuid": "^3.4.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
@@ -52,4 +52,4 @@
|
||||
"ts-jest": "^27.0.7",
|
||||
"typescript": "^4.4.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface IGitCommandManager {
|
||||
branchDelete(remote: boolean, branch: string): Promise<void>
|
||||
branchExists(remote: boolean, pattern: string): Promise<boolean>
|
||||
branchList(remote: boolean): Promise<string[]>
|
||||
disableSparseCheckout(): Promise<void>
|
||||
sparseCheckout(sparseCheckout: string[]): Promise<void>
|
||||
sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void>
|
||||
checkout(ref: string, startPoint: string): Promise<void>
|
||||
@@ -34,6 +35,7 @@ export interface IGitCommandManager {
|
||||
filter?: string
|
||||
fetchDepth?: number
|
||||
fetchTags?: boolean
|
||||
showProgress?: boolean
|
||||
}
|
||||
): Promise<void>
|
||||
getDefaultBranch(repositoryUrl: string): Promise<string>
|
||||
@@ -170,6 +172,10 @@ class GitCommandManager {
|
||||
return result
|
||||
}
|
||||
|
||||
async disableSparseCheckout(): Promise<void> {
|
||||
await this.execGit(['sparse-checkout', 'disable'])
|
||||
}
|
||||
|
||||
async sparseCheckout(sparseCheckout: string[]): Promise<void> {
|
||||
await this.execGit(['sparse-checkout', 'set', ...sparseCheckout])
|
||||
}
|
||||
@@ -241,14 +247,22 @@ class GitCommandManager {
|
||||
|
||||
async fetch(
|
||||
refSpec: string[],
|
||||
options: {filter?: string; fetchDepth?: number; fetchTags?: boolean}
|
||||
options: {
|
||||
filter?: string
|
||||
fetchDepth?: number
|
||||
fetchTags?: boolean
|
||||
showProgress?: boolean
|
||||
}
|
||||
): Promise<void> {
|
||||
const args = ['-c', 'protocol.version=2', 'fetch']
|
||||
if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
|
||||
args.push('--no-tags')
|
||||
}
|
||||
|
||||
args.push('--prune', '--progress', '--no-recurse-submodules')
|
||||
args.push('--prune', '--no-recurse-submodules')
|
||||
if (options.showProgress) {
|
||||
args.push('--progress')
|
||||
}
|
||||
|
||||
if (options.filter) {
|
||||
args.push(`--filter=${options.filter}`)
|
||||
|
||||
@@ -157,8 +157,15 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
filter?: string
|
||||
fetchDepth?: number
|
||||
fetchTags?: boolean
|
||||
showProgress?: boolean
|
||||
} = {}
|
||||
if (settings.sparseCheckout) fetchOptions.filter = 'blob:none'
|
||||
|
||||
if (settings.filter) {
|
||||
fetchOptions.filter = settings.filter
|
||||
} else if (settings.sparseCheckout) {
|
||||
fetchOptions.filter = 'blob:none'
|
||||
}
|
||||
|
||||
if (settings.fetchDepth <= 0) {
|
||||
// Fetch all branches and tags
|
||||
let refSpec = refHelper.getRefSpecForAllHistory(
|
||||
@@ -201,7 +208,9 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
}
|
||||
|
||||
// Sparse checkout
|
||||
if (settings.sparseCheckout) {
|
||||
if (!settings.sparseCheckout) {
|
||||
await git.disableSparseCheckout()
|
||||
} else {
|
||||
core.startGroup('Setting up sparse checkout')
|
||||
if (settings.sparseCheckoutConeMode) {
|
||||
await git.sparseCheckout(settings.sparseCheckout)
|
||||
|
||||
@@ -29,6 +29,11 @@ export interface IGitSourceSettings {
|
||||
*/
|
||||
clean: boolean
|
||||
|
||||
/**
|
||||
* The filter determining which objects to include
|
||||
*/
|
||||
filter: string | undefined
|
||||
|
||||
/**
|
||||
* The array of folders to make the sparse checkout
|
||||
*/
|
||||
@@ -49,6 +54,11 @@ export interface IGitSourceSettings {
|
||||
*/
|
||||
fetchTags: boolean
|
||||
|
||||
/**
|
||||
* Indicates whether to use the --progress option when fetching
|
||||
*/
|
||||
showProgress: boolean
|
||||
|
||||
/**
|
||||
* Indicates whether to fetch LFS objects
|
||||
*/
|
||||
@@ -103,10 +113,4 @@ export interface IGitSourceSettings {
|
||||
* User override on the GitHub Server/Host URL that hosts the repository to be cloned
|
||||
*/
|
||||
githubServerUrl: string | undefined
|
||||
|
||||
/**
|
||||
* Opt-in to allow checking out fork pull request code from a workflow
|
||||
* triggered by pull_request_target or workflow_run.
|
||||
*/
|
||||
allowUnsafePrCheckout: boolean
|
||||
}
|
||||
|
||||
+13
-14
@@ -2,7 +2,6 @@ import * as core from '@actions/core'
|
||||
import * as fsHelper from './fs-helper'
|
||||
import * as github from '@actions/github'
|
||||
import * as path from 'path'
|
||||
import * as unsafePrCheckoutHelper from './unsafe-pr-checkout-helper'
|
||||
import * as workflowContextHelper from './workflow-context-helper'
|
||||
import {IGitSourceSettings} from './git-source-settings'
|
||||
|
||||
@@ -83,6 +82,14 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
|
||||
core.debug(`clean = ${result.clean}`)
|
||||
|
||||
// Filter
|
||||
const filter = core.getInput('filter')
|
||||
if (filter) {
|
||||
result.filter = filter
|
||||
}
|
||||
|
||||
core.debug(`filter = ${result.filter}`)
|
||||
|
||||
// Sparse checkout
|
||||
const sparseCheckout = core.getMultilineInput('sparse-checkout')
|
||||
if (sparseCheckout.length) {
|
||||
@@ -106,6 +113,11 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
(core.getInput('fetch-tags') || 'false').toUpperCase() === 'TRUE'
|
||||
core.debug(`fetch tags = ${result.fetchTags}`)
|
||||
|
||||
// Show fetch progress
|
||||
result.showProgress =
|
||||
(core.getInput('show-progress') || 'true').toUpperCase() === 'TRUE'
|
||||
core.debug(`show progress = ${result.showProgress}`)
|
||||
|
||||
// LFS
|
||||
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'
|
||||
core.debug(`lfs = ${result.lfs}`)
|
||||
@@ -147,18 +159,5 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
result.githubServerUrl = core.getInput('github-server-url')
|
||||
core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
|
||||
|
||||
// Allow unsafe PR checkout (opt-in for pull_request_target / workflow_run fork PRs)
|
||||
result.allowUnsafePrCheckout =
|
||||
(core.getInput('allow-unsafe-pr-checkout') || 'false').toUpperCase() ===
|
||||
'TRUE'
|
||||
core.debug(`allow unsafe PR checkout = ${result.allowUnsafePrCheckout}`)
|
||||
|
||||
unsafePrCheckoutHelper.assertSafePrCheckout({
|
||||
qualifiedRepository,
|
||||
ref: result.ref,
|
||||
commit: result.commit,
|
||||
allowUnsafePrCheckout: result.allowUnsafePrCheckout
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ function updateUsage(
|
||||
}
|
||||
|
||||
updateUsage(
|
||||
'actions/checkout@v3',
|
||||
'actions/checkout@v4',
|
||||
path.join(__dirname, '..', '..', 'action.yml'),
|
||||
path.join(__dirname, '..', '..', 'README.md')
|
||||
)
|
||||
|
||||
+1
-1
@@ -264,7 +264,7 @@ export async function checkCommitInfo(
|
||||
}
|
||||
}
|
||||
|
||||
export function fromPayload(path: string): any {
|
||||
function fromPayload(path: string): any {
|
||||
return select(github.context.payload, path)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import * as github from '@actions/github'
|
||||
import {fromPayload} from './ref-helper'
|
||||
|
||||
const PR_REF_PATTERN = /^refs\/pull\/[0-9]+\/(?:head|merge)$/
|
||||
|
||||
export interface IUnsafePrCheckoutInput {
|
||||
qualifiedRepository: string
|
||||
ref: string
|
||||
commit: string | undefined
|
||||
allowUnsafePrCheckout: boolean
|
||||
}
|
||||
|
||||
export function assertSafePrCheckout(input: IUnsafePrCheckoutInput): void {
|
||||
if (input.allowUnsafePrCheckout) {
|
||||
return
|
||||
}
|
||||
|
||||
const eventName = github.context.eventName
|
||||
if (eventName !== 'pull_request_target' && eventName !== 'workflow_run') {
|
||||
return
|
||||
}
|
||||
|
||||
const baseRepoId = fromPayload('repository.id')
|
||||
if (typeof baseRepoId !== 'number') {
|
||||
return
|
||||
}
|
||||
|
||||
let prHeadRepoId: unknown
|
||||
let prHeadRepoFullName: unknown
|
||||
const prShas: string[] = []
|
||||
|
||||
if (eventName === 'pull_request_target') {
|
||||
prHeadRepoId = fromPayload('pull_request.head.repo.id')
|
||||
prHeadRepoFullName = fromPayload('pull_request.head.repo.full_name')
|
||||
pushIfSha(prShas, fromPayload('pull_request.head.sha'))
|
||||
pushIfSha(prShas, fromPayload('pull_request.merge_commit_sha'))
|
||||
} else {
|
||||
const wrEvent = fromPayload('workflow_run.event')
|
||||
if (typeof wrEvent !== 'string' || !wrEvent.startsWith('pull_request')) {
|
||||
return
|
||||
}
|
||||
prHeadRepoId = fromPayload('workflow_run.head_repository.id')
|
||||
prHeadRepoFullName = fromPayload('workflow_run.head_repository.full_name')
|
||||
pushIfSha(prShas, fromPayload('workflow_run.head_commit.id'))
|
||||
// For `pull_request_target`-triggered workflow_run, `head_sha` is the base
|
||||
// default branch SHA (not the PR head)
|
||||
if (wrEvent !== 'pull_request_target') {
|
||||
pushIfSha(prShas, fromPayload('workflow_run.head_sha'))
|
||||
}
|
||||
}
|
||||
|
||||
// (A) Fork PR?
|
||||
if (typeof prHeadRepoId !== 'number' || prHeadRepoId === baseRepoId) {
|
||||
return
|
||||
}
|
||||
|
||||
// (B) We cannot check for all fork PR refs so check to see
|
||||
// if the resolved input points to the fork PR sha we have in the payload
|
||||
const repositoryMatchesPrHead =
|
||||
typeof prHeadRepoFullName === 'string' &&
|
||||
input.qualifiedRepository.toLowerCase() === prHeadRepoFullName.toLowerCase()
|
||||
const refMatchesPullPattern = PR_REF_PATTERN.test(input.ref)
|
||||
const commitMatchesPrHeadSha =
|
||||
!!input.commit && prShas.includes(input.commit.toLowerCase())
|
||||
|
||||
if (
|
||||
!repositoryMatchesPrHead &&
|
||||
!refMatchesPullPattern &&
|
||||
!commitMatchesPrHeadSha
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Refusing to check out fork pull request code from a '${eventName}' workflow. ` +
|
||||
`This workflow runs with the base repository's GITHUB_TOKEN, secrets, default-branch ` +
|
||||
`cache scope, and runner access. Fetching and executing a fork's code in that trusted ` +
|
||||
`context commonly leads to "pwn request" vulnerabilities. To opt in, review the risks ` +
|
||||
`at https://gh.io/securely-using-pull_request_target and set 'allow-unsafe-pr-checkout: true' ` +
|
||||
`on the actions/checkout step.`
|
||||
)
|
||||
}
|
||||
|
||||
function pushIfSha(target: string[], value: unknown): void {
|
||||
if (typeof value === 'string' && value.length > 0) {
|
||||
target.push(value.toLowerCase())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user