Compare commits

..
117 changed files with 6144 additions and 35426 deletions
+7 -2
View File
@@ -1,6 +1,6 @@
{ {
"plugins": ["jest", "@typescript-eslint"], "plugins": ["jest", "@typescript-eslint"],
"extends": ["plugin:github/recommended"], "extends": ["plugin:github/es6"],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"ecmaVersion": 9, "ecmaVersion": 9,
@@ -16,19 +16,23 @@
"@typescript-eslint/no-require-imports": "error", "@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error", "@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error", "@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-ignore": "error",
"camelcase": "off", "camelcase": "off",
"@typescript-eslint/camelcase": "error",
"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
"@typescript-eslint/func-call-spacing": ["error", "never"], "@typescript-eslint/func-call-spacing": ["error", "never"],
"@typescript-eslint/generic-type-naming": ["error", "^[A-Z][A-Za-z]*$"],
"@typescript-eslint/no-array-constructor": "error", "@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error", "@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extraneous-class": "error", "@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error", "@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error", "@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error", "@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error", "@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "warn", "@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-object-literal-type-assertion": "error",
"@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error", "@typescript-eslint/no-useless-constructor": "error",
@@ -36,6 +40,7 @@
"@typescript-eslint/prefer-for-of": "warn", "@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn", "@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error", "@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-interface": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error", "@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error", "@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error", "@typescript-eslint/require-array-sort-compare": "error",
-1
View File
@@ -1 +0,0 @@
.licenses/** -diff linguist-generated=true
-51
View File
@@ -1,51 +0,0 @@
# `dist/index.js` is a special file in Actions.
# When you reference an action with `uses:` in a workflow,
# `index.js` is the code that will run.
# For our project, we generate this file through a build process
# from other source files.
# We need to make sure the checked-in `index.js` actually matches what we expect it to be.
name: Check dist
on:
push:
branches:
- main
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:
jobs:
check-dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install dependencies
run: npm ci
- name: Rebuild the index.js file
run: npm run build
- name: Compare the expected and actual dist/ directories
run: |
if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then
echo "Detected uncommitted changes after build. See status below:"
git diff
exit 1
fi
# If dist/ was different than expected, upload the expected version as an artifact
- uses: actions/upload-artifact@v4
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
with:
name: dist
path: dist/
-58
View File
@@ -1,58 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '28 9 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- run: npm ci
- run: npm run build
- run: rm -rf dist # We want code scanning to analyze lib instead (individual .js files)
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
-14
View File
@@ -1,14 +0,0 @@
name: Licensed
on:
push: {branches: main}
pull_request: {branches: main}
jobs:
test:
runs-on: ubuntu-latest
name: Check licenses
steps:
- uses: actions/checkout@v2
- run: npm ci
- run: npm run licensed-check
+2 -40
View File
@@ -4,7 +4,7 @@ on:
pull_request: pull_request:
push: push:
branches: branches:
- main - master
- releases/* - releases/*
jobs: jobs:
@@ -142,7 +142,7 @@ jobs:
options: --dns 127.0.0.1 options: --dns 127.0.0.1
services: services:
squid-proxy: squid-proxy:
image: ubuntu/squid:latest image: datadog/squid:latest
ports: ports:
- 3128:3128 - 3128:3128
env: env:
@@ -205,41 +205,3 @@ jobs:
path: basic path: basic
- name: Verify basic - name: Verify basic
run: __test__/verify-basic.sh --archive run: __test__/verify-basic.sh --archive
test-git-container:
runs-on: ubuntu-latest
container: bitnami/git:latest
steps:
# Clone this repo
- name: Checkout
uses: actions/checkout@v3
with:
path: v3
# Basic checkout using git
- name: Checkout basic
uses: ./v3
with:
ref: test-data/v2/basic
- name: Verify basic
run: |
if [ ! -f "./basic-file.txt" ]; then
echo "Expected basic file does not exist"
exit 1
fi
# Verify .git folder
if [ ! -d "./.git" ]; then
echo "Expected ./.git folder to exist"
exit 1
fi
# Verify auth token
git config --global --add safe.directory "*"
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
with:
path: v3
-1
View File
@@ -1,4 +1,3 @@
__test__/_temp __test__/_temp
_temp/
lib/ lib/
node_modules/ node_modules/
-14
View File
@@ -1,14 +0,0 @@
sources:
npm: true
allowed:
- apache-2.0
- bsd-2-clause
- bsd-3-clause
- isc
- mit
- cc0-1.0
- unlicense
reviewed:
npm:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-44
View File
@@ -1,49 +1,5 @@
# Changelog # Changelog
## v2.5.0
- [Bump @actions/core to v1.10.0](https://github.com/actions/checkout/pull/962)
## v2.4.2
- [Add input `set-safe-directory`](https://github.com/actions/checkout/pull/776)
## v2.4.1
- [Set the safe directory option on git to prevent git commands failing when running in containers](https://github.com/actions/checkout/pull/762)
## v2.3.1
- [Fix default branch resolution for .wiki and when using SSH](https://github.com/actions/checkout/pull/284)
## v2.3.0
- [Fallback to the default branch](https://github.com/actions/checkout/pull/278)
## v2.2.0
- [Fetch all history for all tags and branches when fetch-depth=0](https://github.com/actions/checkout/pull/258)
## v2.1.1
- Changes to support GHES ([here](https://github.com/actions/checkout/pull/236) and [here](https://github.com/actions/checkout/pull/248))
## v2.1.0
- [Group output](https://github.com/actions/checkout/pull/191)
- [Changes to support GHES alpha release](https://github.com/actions/checkout/pull/199)
- [Persist core.sshCommand for submodules](https://github.com/actions/checkout/pull/184)
- [Add support ssh](https://github.com/actions/checkout/pull/163)
- [Convert submodule SSH URL to HTTPS, when not using SSH](https://github.com/actions/checkout/pull/179)
- [Add submodule support](https://github.com/actions/checkout/pull/157)
- [Follow proxy settings](https://github.com/actions/checkout/pull/144)
- [Fix ref for pr closed event when a pr is merged](https://github.com/actions/checkout/pull/141)
- [Fix issue checking detached when git less than 2.22](https://github.com/actions/checkout/pull/128)
## v2.0.0
- [Do not pass cred on command line](https://github.com/actions/checkout/pull/108)
- [Add input persist-credentials](https://github.com/actions/checkout/pull/107)
- [Fallback to REST API to download repo](https://github.com/actions/checkout/pull/104)
## v2 (beta) ## v2 (beta)
- Improved fetch performance - Improved fetch performance
-1
View File
@@ -1 +0,0 @@
* @actions/actions-runtime
+43 -44
View File
@@ -6,7 +6,7 @@
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it. This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
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` to fetch more history. Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events.
The auth token is persisted in the local git config. This enables your scripts to run authenticated git commands. The token is removed during post-job cleanup. Set `persist-credentials: false` to opt-out. 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.
@@ -18,7 +18,6 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
- Fetches only a single commit by default - Fetches only a single commit by default
- Script authenticated git commands - Script authenticated git commands
- Auth token persisted in the local git config - Auth token persisted in the local git config
- Supports SSH
- Creates a local branch - Creates a local branch
- No longer detached HEAD when checking out a branch - No longer detached HEAD when checking out a branch
- Improved layout - Improved layout
@@ -27,6 +26,7 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
- Fallback to REST API download - Fallback to REST API download
- When Git 2.18 or higher is not in the PATH, the REST API will be used to download the files - When Git 2.18 or higher is not in the PATH, the REST API will be used to download the files
- When using a job container, the container's PATH is used - When using a job container, the container's PATH is used
- Removed input `submodules`
Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous versions. Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous versions.
@@ -42,7 +42,7 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
# The branch, tag or SHA to checkout. When checking out the repository that # The branch, tag or SHA to checkout. When checking out the repository that
# triggered a workflow, this defaults to the reference or SHA for that event. # triggered a workflow, this defaults to the reference or SHA for that event.
# Otherwise, uses the default branch. # Otherwise, defaults to `master`.
ref: '' ref: ''
# Personal access token (PAT) used to fetch the repository. The PAT is configured # Personal access token (PAT) used to fetch the repository. The PAT is configured
@@ -89,7 +89,7 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
# Default: true # Default: true
clean: '' clean: ''
# Number of commits to fetch. 0 indicates all history for all branches and tags. # Number of commits to fetch. 0 indicates all history.
# Default: 1 # Default: 1
fetch-depth: '' fetch-depth: ''
@@ -105,26 +105,11 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
# #
# Default: false # Default: false
submodules: '' submodules: ''
# Add repository path as safe.directory for Git global config by running `git
# config --global --add safe.directory <path>`
# Default: true
set-safe-directory: ''
# 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 --> <!-- end usage -->
# Scenarios # Scenarios
- [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 a different branch](#Checkout-a-different-branch)
- [Checkout HEAD^](#Checkout-HEAD) - [Checkout HEAD^](#Checkout-HEAD)
- [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side) - [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side)
@@ -132,15 +117,10 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
- [Checkout multiple repos (private)](#Checkout-multiple-repos-private) - [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 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) - [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 submodules](#Checkout-submodules)
- [Fetch all tags](#Fetch-all-tags)
## Fetch all history for all tags and branches - [Fetch all branches](#Fetch-all-branches)
- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches)
```yaml
- uses: actions/checkout@v2
with:
fetch-depth: 0
```
## Checkout a different branch ## Checkout a different branch
@@ -199,7 +179,7 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
repository: my-org/my-private-tools repository: my-org/my-private-tools
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT token: ${{ secrets.GitHub_PAT }} # `GitHub_PAT` is a secret that contains your PAT
path: my-tools path: my-tools
``` ```
@@ -219,7 +199,7 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
```yaml ```yaml
on: on:
pull_request: pull_request:
branches: [main] branches: [master]
types: [opened, synchronize, closed] types: [opened, synchronize, closed]
jobs: jobs:
build: build:
@@ -228,22 +208,41 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
``` ```
## Push a commit using the built-in token ## Checkout submodules
```yaml ```yaml
on: push - uses: actions/checkout@v2
jobs: - name: Checkout submodules
build: shell: bash
runs-on: ubuntu-latest run: |
steps: # If your submodules are configured to use SSH instead of HTTPS please uncomment the following line
- uses: actions/checkout@v2 # git config --global url."https://github.com/".insteadOf "git@github.com:"
- run: | auth_header="$(git config --local --get http.https://github.com/.extraheader)"
date > generated.txt git submodule sync --recursive
git config user.name github-actions git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
git config user.email github-actions@github.com ```
git add .
git commit -m "generated" ## Fetch all tags
git push
```yaml
- uses: actions/checkout@v2
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
```
## Fetch all branches
```yaml
- uses: actions/checkout@v2
- run: |
git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
```
## Fetch all history for all tags and branches
```yaml
- uses: actions/checkout@v2
- run: |
git fetch --prune --unshallow
``` ```
# License # License
+12 -24
View File
@@ -417,7 +417,7 @@ describe('git-auth-helper tests', () => {
`Did not expect file to exist: '${globalGitConfigPath}'` `Did not expect file to exist: '${globalGitConfigPath}'`
) )
} catch (err) { } catch (err) {
if ((err as any)?.code !== 'ENOENT') { if (err.code !== 'ENOENT') {
throw err throw err
} }
} }
@@ -518,17 +518,12 @@ describe('git-auth-helper tests', () => {
await authHelper.configureSubmoduleAuth() await authHelper.configureSubmoduleAuth()
// Assert // Assert
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4) expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/ /unset-all.*insteadOf/
) )
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch( expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/url.*insteadOf/)
/url.*insteadOf.*git@github.com:/
)
expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
/url.*insteadOf.*org-123456@github.com:/
)
} }
) )
@@ -606,7 +601,7 @@ describe('git-auth-helper tests', () => {
await fs.promises.stat(actualKeyPath) await fs.promises.stat(actualKeyPath)
throw new Error('SSH key should have been deleted') throw new Error('SSH key should have been deleted')
} catch (err) { } catch (err) {
if ((err as any)?.code !== 'ENOENT') { if (err.code !== 'ENOENT') {
throw err throw err
} }
} }
@@ -616,7 +611,7 @@ describe('git-auth-helper tests', () => {
await fs.promises.stat(actualKnownHostsPath) await fs.promises.stat(actualKnownHostsPath)
throw new Error('SSH known hosts should have been deleted') throw new Error('SSH known hosts should have been deleted')
} catch (err) { } catch (err) {
if ((err as any)?.code !== 'ENOENT') { if (err.code !== 'ENOENT') {
throw err throw err
} }
} }
@@ -643,11 +638,10 @@ describe('git-auth-helper tests', () => {
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0) expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
}) })
const removeGlobalConfig_removesOverride = const removeGlobalAuth_removesOverride = 'removeGlobalAuth removes override'
'removeGlobalConfig removes override' it(removeGlobalAuth_removesOverride, async () => {
it(removeGlobalConfig_removesOverride, async () => {
// Arrange // Arrange
await setup(removeGlobalConfig_removesOverride) await setup(removeGlobalAuth_removesOverride)
const authHelper = gitAuthHelper.createAuthHelper(git, settings) const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth() await authHelper.configureAuth()
await authHelper.configureGlobalAuth() await authHelper.configureGlobalAuth()
@@ -656,7 +650,7 @@ describe('git-auth-helper tests', () => {
await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig')) await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig'))
// Act // Act
await authHelper.removeGlobalConfig() await authHelper.removeGlobalAuth()
// Assert // Assert
expect(git.env['HOME']).toBeUndefined() expect(git.env['HOME']).toBeUndefined()
@@ -664,7 +658,7 @@ describe('git-auth-helper tests', () => {
await fs.promises.stat(homeOverride) await fs.promises.stat(homeOverride)
throw new Error(`Should have been deleted '${homeOverride}'`) throw new Error(`Should have been deleted '${homeOverride}'`)
} catch (err) { } catch (err) {
if ((err as any)?.code !== 'ENOENT') { if (err.code !== 'ENOENT') {
throw err throw err
} }
} }
@@ -720,7 +714,6 @@ async function setup(testName: string): Promise<void> {
), ),
env: {}, env: {},
fetch: jest.fn(), fetch: jest.fn(),
getDefaultBranch: jest.fn(),
getWorkingDirectory: jest.fn(() => workspace), getWorkingDirectory: jest.fn(() => workspace),
init: jest.fn(), init: jest.fn(),
isDetached: jest.fn(), isDetached: jest.fn(),
@@ -729,11 +722,9 @@ async function setup(testName: string): Promise<void> {
log1: jest.fn(), log1: jest.fn(),
remoteAdd: jest.fn(), remoteAdd: jest.fn(),
removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]), removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
revParse: jest.fn(),
setEnvironmentVariable: jest.fn((name: string, value: string) => { setEnvironmentVariable: jest.fn((name: string, value: string) => {
git.env[name] = value git.env[name] = value
}), }),
shaExists: jest.fn(),
submoduleForeach: jest.fn(async () => { submoduleForeach: jest.fn(async () => {
return '' return ''
}), }),
@@ -770,16 +761,13 @@ async function setup(testName: string): Promise<void> {
submodules: false, submodules: false,
nestedSubmodules: false, nestedSubmodules: false,
persistCredentials: true, persistCredentials: true,
ref: 'refs/heads/main', ref: 'refs/heads/master',
repositoryName: 'my-repo', repositoryName: 'my-repo',
repositoryOwner: 'my-org', repositoryOwner: 'my-org',
repositoryPath: '', repositoryPath: '',
sshKey: sshPath ? 'some ssh private key' : '', sshKey: sshPath ? 'some ssh private key' : '',
sshKnownHosts: '', sshKnownHosts: '',
sshStrict: true, sshStrict: true
workflowOrganizationId: 123456,
setSafeDirectory: true,
allowUnsafePrCheckout: false
} }
} }
+18 -73
View File
@@ -9,7 +9,6 @@ const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper')
let repositoryPath: string let repositoryPath: string
let repositoryUrl: string let repositoryUrl: string
let clean: boolean let clean: boolean
let ref: string
let git: IGitCommandManager let git: IGitCommandManager
describe('git-directory-helper tests', () => { describe('git-directory-helper tests', () => {
@@ -42,8 +41,7 @@ describe('git-directory-helper tests', () => {
git, git,
repositoryPath, repositoryPath,
repositoryUrl, repositoryUrl,
clean, clean
ref
) )
// Assert // Assert
@@ -65,8 +63,7 @@ describe('git-directory-helper tests', () => {
git, git,
repositoryPath, repositoryPath,
repositoryUrl, repositoryUrl,
clean, clean
ref
) )
// Assert // Assert
@@ -91,8 +88,7 @@ describe('git-directory-helper tests', () => {
git, git,
repositoryPath, repositoryPath,
repositoryUrl, repositoryUrl,
clean, clean
ref
) )
// Assert // Assert
@@ -113,8 +109,7 @@ describe('git-directory-helper tests', () => {
git, git,
repositoryPath, repositoryPath,
repositoryUrl, repositoryUrl,
clean, clean
ref
) )
// Assert // Assert
@@ -142,8 +137,7 @@ describe('git-directory-helper tests', () => {
git, git,
repositoryPath, repositoryPath,
repositoryUrl, repositoryUrl,
clean, clean
ref
) )
// Assert // Assert
@@ -169,8 +163,7 @@ describe('git-directory-helper tests', () => {
git, git,
repositoryPath, repositoryPath,
differentRepositoryUrl, differentRepositoryUrl,
clean, clean
ref
) )
// Assert // Assert
@@ -194,8 +187,7 @@ describe('git-directory-helper tests', () => {
git, git,
repositoryPath, repositoryPath,
repositoryUrl, repositoryUrl,
clean, clean
ref
) )
// Assert // Assert
@@ -220,8 +212,7 @@ describe('git-directory-helper tests', () => {
git, git,
repositoryPath, repositoryPath,
repositoryUrl, repositoryUrl,
clean, clean
ref
) )
// Assert // Assert
@@ -245,8 +236,7 @@ describe('git-directory-helper tests', () => {
undefined, undefined,
repositoryPath, repositoryPath,
repositoryUrl, repositoryUrl,
clean, clean
ref
) )
// Assert // Assert
@@ -270,8 +260,7 @@ describe('git-directory-helper tests', () => {
git, git,
repositoryPath, repositoryPath,
repositoryUrl, repositoryUrl,
clean, clean
ref
) )
// Assert // Assert
@@ -301,8 +290,7 @@ describe('git-directory-helper tests', () => {
git, git,
repositoryPath, repositoryPath,
repositoryUrl, repositoryUrl,
clean, clean
ref
) )
// Assert // Assert
@@ -317,66 +305,29 @@ describe('git-directory-helper tests', () => {
expect(git.tryReset).not.toHaveBeenCalled() expect(git.tryReset).not.toHaveBeenCalled()
}) })
const removesAncestorRemoteBranch = 'removes ancestor remote branch' const removesRemoteBranches = 'removes local branches'
it(removesAncestorRemoteBranch, async () => { it(removesRemoteBranches, async () => {
// Arrange // Arrange
await setup(removesAncestorRemoteBranch) await setup(removesRemoteBranches)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
const mockBranchList = git.branchList as jest.Mock<any, any> const mockBranchList = git.branchList as jest.Mock<any, any>
mockBranchList.mockImplementation(async (remote: boolean) => { mockBranchList.mockImplementation(async (remote: boolean) => {
return remote ? ['origin/remote-branch-1', 'origin/remote-branch-2'] : [] return remote ? ['remote-branch-1', 'remote-branch-2'] : []
}) })
ref = 'remote-branch-1/conflict'
// Act // Act
await gitDirectoryHelper.prepareExistingDirectory( await gitDirectoryHelper.prepareExistingDirectory(
git, git,
repositoryPath, repositoryPath,
repositoryUrl, repositoryUrl,
clean, clean
ref
) )
// Assert // Assert
const files = await fs.promises.readdir(repositoryPath) const files = await fs.promises.readdir(repositoryPath)
expect(files.sort()).toEqual(['.git', 'my-file']) expect(files.sort()).toEqual(['.git', 'my-file'])
expect(git.branchDelete).toHaveBeenCalledTimes(1) expect(git.branchDelete).toHaveBeenCalledWith(true, 'remote-branch-1')
expect(git.branchDelete).toHaveBeenCalledWith( expect(git.branchDelete).toHaveBeenCalledWith(true, 'remote-branch-2')
true,
'origin/remote-branch-1'
)
})
const removesDescendantRemoteBranches = 'removes descendant remote branch'
it(removesDescendantRemoteBranches, async () => {
// Arrange
await setup(removesDescendantRemoteBranches)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
const mockBranchList = git.branchList as jest.Mock<any, any>
mockBranchList.mockImplementation(async (remote: boolean) => {
return remote
? ['origin/remote-branch-1/conflict', 'origin/remote-branch-2']
: []
})
ref = 'remote-branch-1'
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files.sort()).toEqual(['.git', 'my-file'])
expect(git.branchDelete).toHaveBeenCalledTimes(1)
expect(git.branchDelete).toHaveBeenCalledWith(
true,
'origin/remote-branch-1/conflict'
)
}) })
}) })
@@ -393,9 +344,6 @@ async function setup(testName: string): Promise<void> {
// Clean // Clean
clean = true clean = true
// Ref
ref = ''
// Git command manager // Git command manager
git = { git = {
branchDelete: jest.fn(), branchDelete: jest.fn(),
@@ -408,7 +356,6 @@ async function setup(testName: string): Promise<void> {
config: jest.fn(), config: jest.fn(),
configExists: jest.fn(), configExists: jest.fn(),
fetch: jest.fn(), fetch: jest.fn(),
getDefaultBranch: jest.fn(),
getWorkingDirectory: jest.fn(() => repositoryPath), getWorkingDirectory: jest.fn(() => repositoryPath),
init: jest.fn(), init: jest.fn(),
isDetached: jest.fn(), isDetached: jest.fn(),
@@ -417,9 +364,7 @@ async function setup(testName: string): Promise<void> {
log1: jest.fn(), log1: jest.fn(),
remoteAdd: jest.fn(), remoteAdd: jest.fn(),
removeEnvironmentVariable: jest.fn(), removeEnvironmentVariable: jest.fn(),
revParse: jest.fn(),
setEnvironmentVariable: jest.fn(), setEnvironmentVariable: jest.fn(),
shaExists: jest.fn(),
submoduleForeach: jest.fn(), submoduleForeach: jest.fn(),
submoduleSync: jest.fn(), submoduleSync: jest.fn(),
submoduleUpdate: jest.fn(), submoduleUpdate: jest.fn(),
+25 -112
View File
@@ -1,9 +1,9 @@
import * as assert from 'assert'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as fsHelper from '../lib/fs-helper' import * as fsHelper from '../lib/fs-helper'
import * as github from '@actions/github' import * as github from '@actions/github'
import * as inputHelper from '../lib/input-helper' import * as inputHelper from '../lib/input-helper'
import * as path from 'path' import * as path from 'path'
import * as workflowContextHelper from '../lib/workflow-context-helper'
import {IGitSourceSettings} from '../lib/git-source-settings' import {IGitSourceSettings} from '../lib/git-source-settings'
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE'] const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
@@ -12,24 +12,15 @@ const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
// Inputs for mock @actions/core // Inputs for mock @actions/core
let inputs = {} as any let inputs = {} as any
// Replicate @actions/core getInput behavior: it trims whitespace by default
// (String.prototype.trim(), which strips characters such as a leading U+FEFF BOM)
// unless trimWhitespace is explicitly set to false.
const getInputImpl = (name: string, options?: {trimWhitespace?: boolean}) => {
const val = inputs[name] ?? ''
if (options && options.trimWhitespace === false) {
return val
}
return typeof val === 'string' ? val.trim() : val
}
// Shallow clone original @actions/github context // Shallow clone original @actions/github context
let originalContext = {...github.context} let originalContext = {...github.context}
describe('input-helper tests', () => { describe('input-helper tests', () => {
beforeAll(() => { beforeAll(() => {
// Mock getInput // Mock getInput
jest.spyOn(core, 'getInput').mockImplementation(getInputImpl as any) jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
return inputs[name]
})
// Mock error/warning/info/debug // Mock error/warning/info/debug
jest.spyOn(core, 'error').mockImplementation(jest.fn()) jest.spyOn(core, 'error').mockImplementation(jest.fn())
@@ -52,11 +43,6 @@ describe('input-helper tests', () => {
.spyOn(fsHelper, 'directoryExistsSync') .spyOn(fsHelper, 'directoryExistsSync')
.mockImplementation((path: string) => path == gitHubWorkspace) .mockImplementation((path: string) => path == gitHubWorkspace)
// Mock ./workflowContextHelper getOrganizationId()
jest
.spyOn(workflowContextHelper, 'getOrganizationId')
.mockImplementation(() => Promise.resolve(123456))
// GitHub workspace // GitHub workspace
process.env['GITHUB_WORKSPACE'] = gitHubWorkspace process.env['GITHUB_WORKSPACE'] = gitHubWorkspace
}) })
@@ -81,8 +67,8 @@ describe('input-helper tests', () => {
jest.restoreAllMocks() jest.restoreAllMocks()
}) })
it('sets defaults', async () => { it('sets defaults', () => {
const settings: IGitSourceSettings = await inputHelper.getInputs() const settings: IGitSourceSettings = inputHelper.getInputs()
expect(settings).toBeTruthy() expect(settings).toBeTruthy()
expect(settings.authToken).toBeFalsy() expect(settings.authToken).toBeFalsy()
expect(settings.clean).toBe(true) expect(settings.clean).toBe(true)
@@ -94,15 +80,13 @@ describe('input-helper tests', () => {
expect(settings.repositoryName).toBe('some-repo') expect(settings.repositoryName).toBe('some-repo')
expect(settings.repositoryOwner).toBe('some-owner') expect(settings.repositoryOwner).toBe('some-owner')
expect(settings.repositoryPath).toBe(gitHubWorkspace) expect(settings.repositoryPath).toBe(gitHubWorkspace)
expect(settings.setSafeDirectory).toBe(true)
expect(settings.allowUnsafePrCheckout).toBe(false)
}) })
it('qualifies ref', async () => { it('qualifies ref', () => {
let originalRef = github.context.ref let originalRef = github.context.ref
try { try {
github.context.ref = 'some-unqualified-ref' github.context.ref = 'some-unqualified-ref'
const settings: IGitSourceSettings = await inputHelper.getInputs() const settings: IGitSourceSettings = inputHelper.getInputs()
expect(settings).toBeTruthy() expect(settings).toBeTruthy()
expect(settings.commit).toBe('1234567890123456789012345678901234567890') expect(settings.commit).toBe('1234567890123456789012345678901234567890')
expect(settings.ref).toBe('refs/heads/some-unqualified-ref') expect(settings.ref).toBe('refs/heads/some-unqualified-ref')
@@ -111,110 +95,39 @@ describe('input-helper tests', () => {
} }
}) })
it('requires qualified repo', async () => { it('requires qualified repo', () => {
inputs.repository = 'some-unqualified-repo' inputs.repository = 'some-unqualified-repo'
try { assert.throws(() => {
await inputHelper.getInputs() inputHelper.getInputs()
throw 'should not reach here' }, /Invalid repository 'some-unqualified-repo'/)
} catch (err) {
expect(`(${(err as any).message}`).toMatch(
"Invalid repository 'some-unqualified-repo'"
)
}
}) })
it('roots path', async () => { it('roots path', () => {
inputs.path = 'some-directory/some-subdirectory' inputs.path = 'some-directory/some-subdirectory'
const settings: IGitSourceSettings = await inputHelper.getInputs() const settings: IGitSourceSettings = inputHelper.getInputs()
expect(settings.repositoryPath).toBe( expect(settings.repositoryPath).toBe(
path.join(gitHubWorkspace, 'some-directory', 'some-subdirectory') path.join(gitHubWorkspace, 'some-directory', 'some-subdirectory')
) )
}) })
it('sets ref to empty when explicit sha', async () => { it('sets correct default ref/sha for other repo', () => {
inputs.repository = 'some-owner/some-other-repo'
const settings: IGitSourceSettings = inputHelper.getInputs()
expect(settings.ref).toBe('refs/heads/master')
expect(settings.commit).toBeFalsy()
})
it('sets ref to empty when explicit sha', () => {
inputs.ref = '1111111111222222222233333333334444444444' inputs.ref = '1111111111222222222233333333334444444444'
const settings: IGitSourceSettings = await inputHelper.getInputs() const settings: IGitSourceSettings = inputHelper.getInputs()
expect(settings.ref).toBeFalsy() expect(settings.ref).toBeFalsy()
expect(settings.commit).toBe('1111111111222222222233333333334444444444') expect(settings.commit).toBe('1111111111222222222233333333334444444444')
}) })
it('sets sha to empty when explicit ref', async () => { it('sets sha to empty when explicit ref', () => {
inputs.ref = 'refs/heads/some-other-ref' inputs.ref = 'refs/heads/some-other-ref'
const settings: IGitSourceSettings = await inputHelper.getInputs() const settings: IGitSourceSettings = inputHelper.getInputs()
expect(settings.ref).toBe('refs/heads/some-other-ref') expect(settings.ref).toBe('refs/heads/some-other-ref')
expect(settings.commit).toBeFalsy() expect(settings.commit).toBeFalsy()
}) })
it('does not reclassify a ref as sha when a BOM is prefixed', async () => {
// A fork branch named "<U+FEFF>" + 40 hex chars. core.getInput trims the
// BOM by default, which previously collapsed this into a bare SHA and
// bypassed the unsafe fork PR checkout guard.
inputs.ref = '\uFEFF522d932fae5296da51fdf431934425ecf891c6a2'
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.commit).toBeFalsy()
expect(settings.ref).toBe('522d932fae5296da51fdf431934425ecf891c6a2')
})
it('treats a sha surrounded by ascii whitespace as a commit', async () => {
// ASCII whitespace can only come from the workflow author's YAML (git ref
// names cannot contain it), so trimming it and treating the value as a
// commit is safe.
inputs.ref = ' 1111111111222222222233333333334444444444 '
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.ref).toBeFalsy()
expect(settings.commit).toBe('1111111111222222222233333333334444444444')
})
it('sets workflow organization ID', async () => {
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.workflowOrganizationId).toBe(123456)
})
describe('unsafe PR checkout guard', () => {
const forkPayload = {
repository: {id: 100},
pull_request: {
head: {
sha: '1234567890123456789012345678901234567890',
repo: {id: 200, full_name: 'attacker/fork'}
},
merge_commit_sha: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
}
}
it('allows the default self-checkout on a fork pull_request_target', async () => {
const originalEvent = github.context.eventName
const originalPayload = github.context.payload
const originalSha = github.context.sha
try {
github.context.eventName = 'pull_request_target'
github.context.payload = forkPayload as any
// Simulate a rebase/fast-forward merge where the base tip (event SHA)
// equals the PR head SHA. The default self-checkout must still succeed.
github.context.sha = '1234567890123456789012345678901234567890'
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
} finally {
github.context.eventName = originalEvent
github.context.payload = originalPayload
github.context.sha = originalSha
}
})
it('refuses an explicit fork repository on pull_request_target', async () => {
const originalEvent = github.context.eventName
const originalPayload = github.context.payload
try {
github.context.eventName = 'pull_request_target'
github.context.payload = forkPayload as any
inputs.repository = 'attacker/fork'
await expect(inputHelper.getInputs()).rejects.toThrow(
/Refusing to check out fork pull request code/
)
} finally {
github.context.eventName = originalEvent
github.context.payload = originalPayload
}
})
})
}) })
+1 -1
View File
@@ -2,5 +2,5 @@
mkdir override-git-version mkdir override-git-version
cd override-git-version cd override-git-version
echo @echo override git version 1.2.3 > git.cmd echo @echo override git version 1.2.3 > git.cmd
echo "%CD%" >> $GITHUB_PATH echo ::add-path::%CD%
cd .. cd ..
+1 -1
View File
@@ -5,5 +5,5 @@ cd override-git-version
echo "#!/bin/sh" > git echo "#!/bin/sh" > git
echo "echo override git version 1.2.3" >> git echo "echo override git version 1.2.3" >> git
chmod +x git chmod +x git
echo "$(pwd)" >> $GITHUB_PATH echo "::add-path::$(pwd)"
cd .. cd ..
+3 -5
View File
@@ -16,7 +16,7 @@ describe('ref-helper tests', () => {
await refHelper.getCheckoutInfo(git, 'refs/heads/my/branch', commit) await refHelper.getCheckoutInfo(git, 'refs/heads/my/branch', commit)
throw new Error('Should not reach here') throw new Error('Should not reach here')
} catch (err) { } catch (err) {
expect((err as any)?.message).toBe('Arg git cannot be empty') expect(err.message).toBe('Arg git cannot be empty')
} }
}) })
@@ -25,9 +25,7 @@ describe('ref-helper tests', () => {
await refHelper.getCheckoutInfo(git, '', '') await refHelper.getCheckoutInfo(git, '', '')
throw new Error('Should not reach here') throw new Error('Should not reach here')
} catch (err) { } catch (err) {
expect((err as any)?.message).toBe( expect(err.message).toBe('Args ref and commit cannot both be empty')
'Args ref and commit cannot both be empty'
)
} }
}) })
@@ -104,7 +102,7 @@ describe('ref-helper tests', () => {
await refHelper.getCheckoutInfo(git, 'my-ref', '') await refHelper.getCheckoutInfo(git, 'my-ref', '')
throw new Error('Should not reach here') throw new Error('Should not reach here')
} catch (err) { } catch (err) {
expect((err as any)?.message).toBe( expect(err.message).toBe(
"A branch or tag with the name 'my-ref' could not be found" "A branch or tag with the name 'my-ref' could not be found"
) )
} }
+1 -1
View File
@@ -74,7 +74,7 @@ describe('retry-helper tests', () => {
throw new Error(`some error ${++attempts}`) throw new Error(`some error ${++attempts}`)
}) })
} catch (err) { } catch (err) {
error = err as Error error = err
} }
expect(error.message).toBe('some error 3') expect(error.message).toBe('some error 3')
expect(attempts).toBe(3) expect(attempts).toBe(3)
-267
View File
@@ -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()
})
})
+1 -1
View File
@@ -20,5 +20,5 @@ else
# Verify auth token # Verify auth token
cd basic cd basic
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main git fetch --no-tags --depth=1 origin +refs/heads/master:refs/remotes/origin/master
fi fi
+1 -1
View File
@@ -12,6 +12,6 @@ if [[ "$(git status --porcelain)" != "" ]]; then
echo ---------------------------------------- echo ----------------------------------------
echo Troubleshooting echo Troubleshooting
echo ---------------------------------------- echo ----------------------------------------
echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run format && npm run build" echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run all"
exit 1 exit 1
fi fi
+2 -14
View File
@@ -8,7 +8,7 @@ inputs:
description: > description: >
The branch, tag or SHA to checkout. When checking out the repository that The branch, tag or SHA to checkout. When checking out the repository that
triggered a workflow, this defaults to the reference or SHA for that triggered a workflow, this defaults to the reference or SHA for that
event. Otherwise, uses the default branch. event. Otherwise, defaults to `master`.
token: token:
description: > description: >
Personal access token (PAT) used to fetch the repository. The PAT is configured Personal access token (PAT) used to fetch the repository. The PAT is configured
@@ -54,7 +54,7 @@ inputs:
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching' description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
default: true default: true
fetch-depth: fetch-depth:
description: 'Number of commits to fetch. 0 indicates all history for all branches and tags.' description: 'Number of commits to fetch. 0 indicates all history.'
default: 1 default: 1
lfs: lfs:
description: 'Whether to download Git-LFS files' description: 'Whether to download Git-LFS files'
@@ -68,18 +68,6 @@ inputs:
When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are
converted to HTTPS. converted to HTTPS.
default: false default: false
set-safe-directory:
description: Add repository path as safe.directory for Git global config by running `git config --global --add safe.directory <path>`
default: true
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: runs:
using: node12 using: node12
main: dist/index.js main: dist/index.js
+9 -28
View File
@@ -24,31 +24,19 @@ We want to take this opportunity to make behavioral changes, from v1. This docum
description: > description: >
The branch, tag or SHA to checkout. When checking out the repository that The branch, tag or SHA to checkout. When checking out the repository that
triggered a workflow, this defaults to the reference or SHA for that triggered a workflow, this defaults to the reference or SHA for that
event. Otherwise, uses the default branch. event. Otherwise, defaults to `master`.
token: token:
description: > description: >
Personal access token (PAT) used to fetch the repository. The PAT is configured Personal access token (PAT) used to fetch the repository. The PAT is configured
with the local git config, which enables your scripts to run authenticated git with the local git config, which enables your scripts to run authenticated git
commands. The post-job step removes the PAT. commands. The post-job step removes the PAT. [Learn more about creating and using
encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
We recommend using a service account with the least permissions necessary.
Also when generating a new PAT, select the least scopes necessary.
[Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
default: ${{ github.token }} default: ${{ github.token }}
ssh-key: ssh-key:
description: > description: >
SSH key used to fetch the repository. The SSH key is configured with the local SSH key used to fetch the repository. SSH key is configured with the local
git config, which enables your scripts to run authenticated git commands. git config, which enables your scripts to run authenticated git commands.
The post-job step removes the SSH key. The post-job step removes the SSH key. [Learn more about creating and using
We recommend using a service account with the least permissions necessary.
[Learn more about creating and using
encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
ssh-known-hosts: ssh-known-hosts:
description: > description: >
@@ -56,10 +44,7 @@ We want to take this opportunity to make behavioral changes, from v1. This docum
SSH keys for a host may be obtained using the utility `ssh-keyscan`. For example, SSH keys for a host may be obtained using the utility `ssh-keyscan`. For example,
`ssh-keyscan github.com`. The public key for github.com is always implicitly added. `ssh-keyscan github.com`. The public key for github.com is always implicitly added.
ssh-strict: ssh-strict:
description: > description: 'Whether to perform strict host key checking'
Whether to perform strict host key checking. When true, adds the options `StrictHostKeyChecking=yes`
and `CheckHostIP=no` to the SSH command line. Use the input `ssh-known-hosts` to
configure additional hosts.
default: true default: true
persist-credentials: persist-credentials:
description: 'Whether to configure the token or SSH key with the local git config' description: 'Whether to configure the token or SSH key with the local git config'
@@ -70,7 +55,7 @@ We want to take this opportunity to make behavioral changes, from v1. This docum
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching' description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
default: true default: true
fetch-depth: fetch-depth:
description: 'Number of commits to fetch. 0 indicates all history for all tags and branches.' description: 'Number of commits to fetch. 0 indicates all history.'
default: 1 default: 1
lfs: lfs:
description: 'Whether to download Git-LFS files' description: 'Whether to download Git-LFS files'
@@ -79,11 +64,7 @@ We want to take this opportunity to make behavioral changes, from v1. This docum
description: > description: >
Whether to checkout submodules: `true` to checkout submodules or `recursive` to Whether to checkout submodules: `true` to checkout submodules or `recursive` to
recursively checkout submodules. recursively checkout submodules.
default: 'false'
When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are
converted to HTTPS.
default: false
``` ```
Note: Note:
@@ -277,7 +258,7 @@ Note:
### Branching strategy and release tags ### Branching strategy and release tags
- Create a servicing branch for V1: `releases/v1` - Create a servicing branch for V1: `releases/v1`
- Merge the changes into the default branch - Merge the changes into `master`
- Release using a new tag `preview` - Release using a new tag `preview`
- When stable, release using a new tag `v2` - When stable, release using a new tag `v2`
+1131 -21981
View File
File diff suppressed because one or more lines are too long
+4672 -11681
View File
File diff suppressed because it is too large Load Diff
+16 -19
View File
@@ -1,16 +1,14 @@
{ {
"name": "checkout", "name": "checkout",
"version": "2.6.0", "version": "2.0.2",
"description": "checkout action", "description": "checkout action",
"main": "lib/main.js", "main": "lib/main.js",
"scripts": { "scripts": {
"build": "tsc && ncc build && node lib/misc/generate-docs.js", "build": "tsc && ncc build && node lib/misc/generate-docs.js",
"format": "prettier --write '**/*.ts'", "format": "prettier --write **/*.ts",
"format-check": "prettier --check '**/*.ts'", "format-check": "prettier --check **/*.ts",
"lint": "eslint src/**/*.ts", "lint": "eslint src/**/*.ts",
"test": "jest", "test": "jest"
"licensed-check": "src/misc/licensed-check.sh",
"licensed-generate": "src/misc/licensed-generate.sh"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -28,28 +26,27 @@
}, },
"homepage": "https://github.com/actions/checkout#readme", "homepage": "https://github.com/actions/checkout#readme",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.1.3",
"@actions/exec": "^1.0.1", "@actions/exec": "^1.0.1",
"@actions/github": "^2.2.0", "@actions/github": "^2.0.2",
"@actions/io": "^1.1.2", "@actions/io": "^1.0.1",
"@actions/tool-cache": "^1.1.2", "@actions/tool-cache": "^1.1.2",
"uuid": "^3.3.3" "uuid": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^27.0.2", "@types/jest": "^24.0.23",
"@types/node": "^12.7.12", "@types/node": "^12.7.12",
"@types/uuid": "^3.4.6", "@types/uuid": "^3.4.6",
"@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^2.8.0",
"@typescript-eslint/parser": "^5.45.0",
"@zeit/ncc": "^0.20.5", "@zeit/ncc": "^0.20.5",
"eslint": "^7.32.0", "eslint": "^5.16.0",
"eslint-plugin-github": "^4.3.2", "eslint-plugin-github": "^2.0.0",
"eslint-plugin-jest": "^25.7.0", "eslint-plugin-jest": "^22.21.0",
"jest": "^27.3.0", "jest": "^24.9.0",
"jest-circus": "^27.3.0", "jest-circus": "^24.9.0",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"ts-jest": "^27.0.7", "ts-jest": "^24.2.0",
"typescript": "^4.4.4" "typescript": "^3.6.4"
} }
} }
+6 -9
View File
@@ -9,7 +9,7 @@ export function directoryExistsSync(path: string, required?: boolean): boolean {
try { try {
stats = fs.statSync(path) stats = fs.statSync(path)
} catch (error) { } catch (error) {
if ((error as any)?.code === 'ENOENT') { if (error.code === 'ENOENT') {
if (!required) { if (!required) {
return false return false
} }
@@ -18,8 +18,7 @@ export function directoryExistsSync(path: string, required?: boolean): boolean {
} }
throw new Error( throw new Error(
`Encountered an error when checking whether path '${path}' exists: ${(error as any) `Encountered an error when checking whether path '${path}' exists: ${error.message}`
?.message ?? error}`
) )
} }
@@ -40,13 +39,12 @@ export function existsSync(path: string): boolean {
try { try {
fs.statSync(path) fs.statSync(path)
} catch (error) { } catch (error) {
if ((error as any)?.code === 'ENOENT') { if (error.code === 'ENOENT') {
return false return false
} }
throw new Error( throw new Error(
`Encountered an error when checking whether path '${path}' exists: ${(error as any) `Encountered an error when checking whether path '${path}' exists: ${error.message}`
?.message ?? error}`
) )
} }
@@ -62,13 +60,12 @@ export function fileExistsSync(path: string): boolean {
try { try {
stats = fs.statSync(path) stats = fs.statSync(path)
} catch (error) { } catch (error) {
if ((error as any)?.code === 'ENOENT') { if (error.code === 'ENOENT') {
return false return false
} }
throw new Error( throw new Error(
`Encountered an error when checking whether path '${path}' exists: ${(error as any) `Encountered an error when checking whether path '${path}' exists: ${error.message}`
?.message ?? error}`
) )
} }
+30 -60
View File
@@ -7,21 +7,20 @@ import * as os from 'os'
import * as path from 'path' import * as path from 'path'
import * as regexpHelper from './regexp-helper' import * as regexpHelper from './regexp-helper'
import * as stateHelper from './state-helper' import * as stateHelper from './state-helper'
import * as urlHelper from './url-helper'
import {default as uuid} from 'uuid/v4' import {default as uuid} from 'uuid/v4'
import {IGitCommandManager} from './git-command-manager' import {IGitCommandManager} from './git-command-manager'
import {IGitSourceSettings} from './git-source-settings' import {IGitSourceSettings} from './git-source-settings'
const IS_WINDOWS = process.platform === 'win32' const IS_WINDOWS = process.platform === 'win32'
const HOSTNAME = 'github.com'
const SSH_COMMAND_KEY = 'core.sshCommand' const SSH_COMMAND_KEY = 'core.sshCommand'
export interface IGitAuthHelper { export interface IGitAuthHelper {
configureAuth(): Promise<void> configureAuth(): Promise<void>
configureGlobalAuth(): Promise<void> configureGlobalAuth(): Promise<void>
configureSubmoduleAuth(): Promise<void> configureSubmoduleAuth(): Promise<void>
configureTempGlobalConfig(): Promise<string>
removeAuth(): Promise<void> removeAuth(): Promise<void>
removeGlobalConfig(): Promise<void> removeGlobalAuth(): Promise<void>
} }
export function createAuthHelper( export function createAuthHelper(
@@ -34,26 +33,24 @@ export function createAuthHelper(
class GitAuthHelper { class GitAuthHelper {
private readonly git: IGitCommandManager private readonly git: IGitCommandManager
private readonly settings: IGitSourceSettings private readonly settings: IGitSourceSettings
private readonly tokenConfigKey: string private readonly tokenConfigKey: string = `http.https://${HOSTNAME}/.extraheader`
private readonly tokenConfigValue: string
private readonly tokenPlaceholderConfigValue: string private readonly tokenPlaceholderConfigValue: string
private readonly insteadOfKey: string private readonly insteadOfKey: string = `url.https://${HOSTNAME}/.insteadOf`
private readonly insteadOfValues: string[] = [] private readonly insteadOfValue: string = `git@${HOSTNAME}:`
private sshCommand = '' private sshCommand = ''
private sshKeyPath = '' private sshKeyPath = ''
private sshKnownHostsPath = '' private sshKnownHostsPath = ''
private temporaryHomePath = '' private temporaryHomePath = ''
private tokenConfigValue: string
constructor( constructor(
gitCommandManager: IGitCommandManager, gitCommandManager: IGitCommandManager,
gitSourceSettings: IGitSourceSettings | undefined gitSourceSettings?: IGitSourceSettings
) { ) {
this.git = gitCommandManager this.git = gitCommandManager
this.settings = gitSourceSettings || (({} as unknown) as IGitSourceSettings) this.settings = gitSourceSettings || (({} as unknown) as IGitSourceSettings)
// Token auth header // Token auth header
const serverUrl = urlHelper.getServerUrl()
this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader` // "origin" is SCHEME://HOSTNAME[:PORT]
const basicCredential = Buffer.from( const basicCredential = Buffer.from(
`x-access-token:${this.settings.authToken}`, `x-access-token:${this.settings.authToken}`,
'utf8' 'utf8'
@@ -61,15 +58,6 @@ class GitAuthHelper {
core.setSecret(basicCredential) core.setSecret(basicCredential)
this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***` this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***`
this.tokenConfigValue = `AUTHORIZATION: basic ${basicCredential}` this.tokenConfigValue = `AUTHORIZATION: basic ${basicCredential}`
// Instead of SSH URL
this.insteadOfKey = `url.${serverUrl.origin}/.insteadOf` // "origin" is SCHEME://HOSTNAME[:PORT]
this.insteadOfValues.push(`git@${serverUrl.hostname}:`)
if (this.settings.workflowOrganizationId) {
this.insteadOfValues.push(
`org-${this.settings.workflowOrganizationId}@github.com:`
)
}
} }
async configureAuth(): Promise<void> { async configureAuth(): Promise<void> {
@@ -81,11 +69,7 @@ class GitAuthHelper {
await this.configureToken() await this.configureToken()
} }
async configureTempGlobalConfig(): Promise<string> { async configureGlobalAuth(): Promise<void> {
// Already setup global config
if (this.temporaryHomePath?.length > 0) {
return path.join(this.temporaryHomePath, '.gitconfig')
}
// Create a temp home directory // Create a temp home directory
const runnerTemp = process.env['RUNNER_TEMP'] || '' const runnerTemp = process.env['RUNNER_TEMP'] || ''
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
@@ -104,7 +88,7 @@ class GitAuthHelper {
await fs.promises.stat(gitConfigPath) await fs.promises.stat(gitConfigPath)
configExists = true configExists = true
} catch (err) { } catch (err) {
if ((err as any)?.code !== 'ENOENT') { if (err.code !== 'ENOENT') {
throw err throw err
} }
} }
@@ -115,28 +99,20 @@ class GitAuthHelper {
await fs.promises.writeFile(newGitConfigPath, '') await fs.promises.writeFile(newGitConfigPath, '')
} }
// Override HOME
core.info(
`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
)
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
return newGitConfigPath
}
async configureGlobalAuth(): Promise<void> {
// 'configureTempGlobalConfig' noops if already set, just returns the path
const newGitConfigPath = await this.configureTempGlobalConfig()
try { try {
// Override HOME
core.info(
`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
)
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
// Configure the token // Configure the token
await this.configureToken(newGitConfigPath, true) await this.configureToken(newGitConfigPath, true)
// Configure HTTPS instead of SSH // Configure HTTPS instead of SSH
await this.git.tryConfigUnset(this.insteadOfKey, true) await this.git.tryConfigUnset(this.insteadOfKey, true)
if (!this.settings.sshKey) { if (!this.settings.sshKey) {
for (const insteadOfValue of this.insteadOfValues) { await this.git.config(this.insteadOfKey, this.insteadOfValue, true)
await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
}
} }
} catch (err) { } catch (err) {
// Unset in case somehow written to the real global config // Unset in case somehow written to the real global config
@@ -157,8 +133,7 @@ class GitAuthHelper {
// by process creation audit events, which are commonly logged. For more information, // 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 // 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( 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 `git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url`,
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
this.settings.nestedSubmodules this.settings.nestedSubmodules
) )
@@ -167,7 +142,7 @@ class GitAuthHelper {
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [] output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
for (const configPath of configPaths) { for (const configPath of configPaths) {
core.debug(`Replacing token placeholder in '${configPath}'`) core.debug(`Replacing token placeholder in '${configPath}'`)
await this.replaceTokenPlaceholder(configPath) this.replaceTokenPlaceholder(configPath)
} }
if (this.settings.sshKey) { if (this.settings.sshKey) {
@@ -178,12 +153,10 @@ class GitAuthHelper {
) )
} else { } else {
// Configure HTTPS instead of SSH // Configure HTTPS instead of SSH
for (const insteadOfValue of this.insteadOfValues) { await this.git.submoduleForeach(
await this.git.submoduleForeach( `git config --local '${this.insteadOfKey}' '${this.insteadOfValue}'`,
`git config --local --add '${this.insteadOfKey}' '${insteadOfValue}'`, this.settings.nestedSubmodules
this.settings.nestedSubmodules )
)
}
} }
} }
} }
@@ -193,12 +166,10 @@ class GitAuthHelper {
await this.removeToken() await this.removeToken()
} }
async removeGlobalConfig(): Promise<void> { async removeGlobalAuth(): Promise<void> {
if (this.temporaryHomePath?.length > 0) { core.info(`Unsetting HOME override`)
core.debug(`Unsetting HOME override`) this.git.removeEnvironmentVariable('HOME')
this.git.removeEnvironmentVariable('HOME') await io.rmRF(this.temporaryHomePath)
await io.rmRF(this.temporaryHomePath)
}
} }
private async configureSsh(): Promise<void> { private async configureSsh(): Promise<void> {
@@ -236,7 +207,7 @@ class GitAuthHelper {
await fs.promises.readFile(userKnownHostsPath) await fs.promises.readFile(userKnownHostsPath)
).toString() ).toString()
} catch (err) { } catch (err) {
if ((err as any)?.code !== 'ENOENT') { if (err.code !== 'ENOENT') {
throw err throw err
} }
} }
@@ -247,7 +218,7 @@ class GitAuthHelper {
if (this.settings.sshKnownHosts) { if (this.settings.sshKnownHosts) {
knownHosts += `# Begin from input known hosts\n${this.settings.sshKnownHosts}\n# end from input known hosts\n` knownHosts += `# Begin from input known hosts\n${this.settings.sshKnownHosts}\n# end from input known hosts\n`
} }
knownHosts += `# Begin implicitly added github.com\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=\n# End implicitly added github.com\n` knownHosts += `# Begin implicitly added github.com\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n# End implicitly added github.com\n`
this.sshKnownHostsPath = path.join(runnerTemp, `${uniqueId}_known_hosts`) this.sshKnownHostsPath = path.join(runnerTemp, `${uniqueId}_known_hosts`)
stateHelper.setSshKnownHostsPath(this.sshKnownHostsPath) stateHelper.setSshKnownHostsPath(this.sshKnownHostsPath)
await fs.promises.writeFile(this.sshKnownHostsPath, knownHosts) await fs.promises.writeFile(this.sshKnownHostsPath, knownHosts)
@@ -325,7 +296,7 @@ class GitAuthHelper {
try { try {
await io.rmRF(keyPath) await io.rmRF(keyPath)
} catch (err) { } catch (err) {
core.debug(`${(err as any)?.message ?? err}`) core.debug(err.message)
core.warning(`Failed to remove SSH key '${keyPath}'`) core.warning(`Failed to remove SSH key '${keyPath}'`)
} }
} }
@@ -366,8 +337,7 @@ class GitAuthHelper {
const pattern = regexpHelper.escape(configKey) const pattern = regexpHelper.escape(configKey)
await this.git.submoduleForeach( await this.git.submoduleForeach(
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline `git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :`,
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
true true
) )
} }
+24 -77
View File
@@ -3,7 +3,6 @@ import * as exec from '@actions/exec'
import * as fshelper from './fs-helper' import * as fshelper from './fs-helper'
import * as io from '@actions/io' import * as io from '@actions/io'
import * as path from 'path' import * as path from 'path'
import * as refHelper from './ref-helper'
import * as regexpHelper from './regexp-helper' import * as regexpHelper from './regexp-helper'
import * as retryHelper from './retry-helper' import * as retryHelper from './retry-helper'
import {GitVersion} from './git-version' import {GitVersion} from './git-version'
@@ -21,23 +20,19 @@ export interface IGitCommandManager {
config( config(
configKey: string, configKey: string,
configValue: string, configValue: string,
globalConfig?: boolean, globalConfig?: boolean
add?: boolean
): Promise<void> ): Promise<void>
configExists(configKey: string, globalConfig?: boolean): Promise<boolean> configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
fetch(refSpec: string[], fetchDepth?: number): Promise<void> fetch(fetchDepth: number, refSpec: string[]): Promise<void>
getDefaultBranch(repositoryUrl: string): Promise<string>
getWorkingDirectory(): string getWorkingDirectory(): string
init(): Promise<void> init(): Promise<void>
isDetached(): Promise<boolean> isDetached(): Promise<boolean>
lfsFetch(ref: string): Promise<void> lfsFetch(ref: string): Promise<void>
lfsInstall(): Promise<void> lfsInstall(): Promise<void>
log1(format?: string): Promise<string> log1(): Promise<void>
remoteAdd(remoteName: string, remoteUrl: string): Promise<void> remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
removeEnvironmentVariable(name: string): void removeEnvironmentVariable(name: string): void
revParse(ref: string): Promise<string>
setEnvironmentVariable(name: string, value: string): void setEnvironmentVariable(name: string, value: string): void
shaExists(sha: string): Promise<boolean>
submoduleForeach(command: string, recursive: boolean): Promise<string> submoduleForeach(command: string, recursive: boolean): Promise<string>
submoduleSync(recursive: boolean): Promise<void> submoduleSync(recursive: boolean): Promise<void>
submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void> submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void>
@@ -141,15 +136,14 @@ class GitCommandManager {
async config( async config(
configKey: string, configKey: string,
configValue: string, configValue: string,
globalConfig?: boolean, globalConfig?: boolean
add?: boolean
): Promise<void> { ): Promise<void> {
const args: string[] = ['config', globalConfig ? '--global' : '--local'] await this.execGit([
if (add) { 'config',
args.push('--add') globalConfig ? '--global' : '--local',
} configKey,
args.push(...[configKey, configValue]) configValue
await this.execGit(args) ])
} }
async configExists( async configExists(
@@ -170,14 +164,17 @@ class GitCommandManager {
return output.exitCode === 0 return output.exitCode === 0
} }
async fetch(refSpec: string[], fetchDepth?: number): Promise<void> { async fetch(fetchDepth: number, refSpec: string[]): Promise<void> {
const args = ['-c', 'protocol.version=2', 'fetch'] const args = [
if (!refSpec.some(x => x === refHelper.tagsRefSpec)) { '-c',
args.push('--no-tags') 'protocol.version=2',
} 'fetch',
'--no-tags',
args.push('--prune', '--progress', '--no-recurse-submodules') '--prune',
if (fetchDepth && fetchDepth > 0) { '--progress',
'--no-recurse-submodules'
]
if (fetchDepth > 0) {
args.push(`--depth=${fetchDepth}`) args.push(`--depth=${fetchDepth}`)
} else if ( } else if (
fshelper.fileExistsSync( fshelper.fileExistsSync(
@@ -198,34 +195,6 @@ class GitCommandManager {
}) })
} }
async getDefaultBranch(repositoryUrl: string): Promise<string> {
let output: GitOutput | undefined
await retryHelper.execute(async () => {
output = await this.execGit([
'ls-remote',
'--quiet',
'--exit-code',
'--symref',
repositoryUrl,
'HEAD'
])
})
if (output) {
// Satisfy compiler, will always be set
for (let line of output.stdout.trim().split('\n')) {
line = line.trim()
if (line.startsWith('ref:') || line.endsWith('HEAD')) {
return line
.substr('ref:'.length, line.length - 'ref:'.length - 'HEAD'.length)
.trim()
}
}
}
throw new Error('Unexpected output when retrieving default branch')
}
getWorkingDirectory(): string { getWorkingDirectory(): string {
return this.workingDirectory return this.workingDirectory
} }
@@ -256,11 +225,8 @@ class GitCommandManager {
await this.execGit(['lfs', 'install', '--local']) await this.execGit(['lfs', 'install', '--local'])
} }
async log1(format?: string): Promise<string> { async log1(): Promise<void> {
var args = format ? ['log', '-1', format] : ['log', '-1'] await this.execGit(['log', '-1'])
var silent = format ? false : true
const output = await this.execGit(args, false, silent)
return output.stdout
} }
async remoteAdd(remoteName: string, remoteUrl: string): Promise<void> { async remoteAdd(remoteName: string, remoteUrl: string): Promise<void> {
@@ -271,27 +237,10 @@ class GitCommandManager {
delete this.gitEnv[name] delete this.gitEnv[name]
} }
/**
* Resolves a ref to a SHA. For a branch or lightweight tag, the commit SHA is returned.
* For an annotated tag, the tag SHA is returned.
* @param {string} ref For example: 'refs/heads/main' or '/refs/tags/v1'
* @returns {Promise<string>}
*/
async revParse(ref: string): Promise<string> {
const output = await this.execGit(['rev-parse', ref])
return output.stdout.trim()
}
setEnvironmentVariable(name: string, value: string): void { setEnvironmentVariable(name: string, value: string): void {
this.gitEnv[name] = value this.gitEnv[name] = value
} }
async shaExists(sha: string): Promise<boolean> {
const args = ['rev-parse', '--verify', '--quiet', `${sha}^{object}`]
const output = await this.execGit(args, true)
return output.exitCode === 0
}
async submoduleForeach(command: string, recursive: boolean): Promise<string> { async submoduleForeach(command: string, recursive: boolean): Promise<string> {
const args = ['submodule', 'foreach'] const args = ['submodule', 'foreach']
if (recursive) { if (recursive) {
@@ -394,8 +343,7 @@ class GitCommandManager {
private async execGit( private async execGit(
args: string[], args: string[],
allowAllExitCodes = false, allowAllExitCodes = false
silent = false
): Promise<GitOutput> { ): Promise<GitOutput> {
fshelper.directoryExistsSync(this.workingDirectory, true) fshelper.directoryExistsSync(this.workingDirectory, true)
@@ -414,7 +362,6 @@ class GitCommandManager {
const options = { const options = {
cwd: this.workingDirectory, cwd: this.workingDirectory,
env, env,
silent,
ignoreReturnCode: allowAllExitCodes, ignoreReturnCode: allowAllExitCodes,
listeners: { listeners: {
stdout: (data: Buffer) => { stdout: (data: Buffer) => {
+6 -34
View File
@@ -1,4 +1,3 @@
import * as assert from 'assert'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as fs from 'fs' import * as fs from 'fs'
import * as fsHelper from './fs-helper' import * as fsHelper from './fs-helper'
@@ -10,13 +9,8 @@ export async function prepareExistingDirectory(
git: IGitCommandManager | undefined, git: IGitCommandManager | undefined,
repositoryPath: string, repositoryPath: string,
repositoryUrl: string, repositoryUrl: string,
clean: boolean, clean: boolean
ref: string
): Promise<void> { ): Promise<void> {
assert.ok(repositoryPath, 'Expected repositoryPath to be defined')
assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined')
// Indicates whether to delete the directory contents
let remove = false let remove = false
// Check whether using git or REST API // Check whether using git or REST API
@@ -39,14 +33,11 @@ export async function prepareExistingDirectory(
try { try {
await io.rmRF(lockPath) await io.rmRF(lockPath)
} catch (error) { } catch (error) {
core.debug( core.debug(`Unable to delete '${lockPath}'. ${error.message}`)
`Unable to delete '${lockPath}'. ${(error as any)?.message ?? error}`
)
} }
} }
try { try {
core.startGroup('Removing previously created refs, to avoid conflicts')
// Checkout detached HEAD // Checkout detached HEAD
if (!(await git.isDetached())) { if (!(await git.isDetached())) {
await git.checkoutDetach() await git.checkoutDetach()
@@ -58,32 +49,14 @@ export async function prepareExistingDirectory(
await git.branchDelete(false, branch) await git.branchDelete(false, branch)
} }
// Remove any conflicting refs/remotes/origin/* // Remove all refs/remotes/origin/* to avoid conflicts
// Example 1: Consider ref is refs/heads/foo and previously fetched refs/remotes/origin/foo/bar branches = await git.branchList(true)
// Example 2: Consider ref is refs/heads/foo/bar and previously fetched refs/remotes/origin/foo for (const branch of branches) {
if (ref) { await git.branchDelete(true, branch)
ref = ref.startsWith('refs/') ? ref : `refs/heads/${ref}`
if (ref.startsWith('refs/heads/')) {
const upperName1 = ref.toUpperCase().substr('REFS/HEADS/'.length)
const upperName1Slash = `${upperName1}/`
branches = await git.branchList(true)
for (const branch of branches) {
const upperName2 = branch.substr('origin/'.length).toUpperCase()
const upperName2Slash = `${upperName2}/`
if (
upperName1.startsWith(upperName2Slash) ||
upperName2.startsWith(upperName1Slash)
) {
await git.branchDelete(true, branch)
}
}
}
} }
core.endGroup()
// Clean // Clean
if (clean) { if (clean) {
core.startGroup('Cleaning the repository')
if (!(await git.tryClean())) { if (!(await git.tryClean())) {
core.debug( core.debug(
`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.` `The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`
@@ -92,7 +65,6 @@ export async function prepareExistingDirectory(
} else if (!(await git.tryReset())) { } else if (!(await git.tryReset())) {
remove = true remove = true
} }
core.endGroup()
if (remove) { if (remove) {
core.warning( core.warning(
+92 -194
View File
@@ -8,16 +8,23 @@ import * as io from '@actions/io'
import * as path from 'path' import * as path from 'path'
import * as refHelper from './ref-helper' import * as refHelper from './ref-helper'
import * as stateHelper from './state-helper' import * as stateHelper from './state-helper'
import * as urlHelper from './url-helper'
import {IGitCommandManager} from './git-command-manager' import {IGitCommandManager} from './git-command-manager'
import {IGitSourceSettings} from './git-source-settings' import {IGitSourceSettings} from './git-source-settings'
const hostname = 'github.com'
export async function getSource(settings: IGitSourceSettings): Promise<void> { export async function getSource(settings: IGitSourceSettings): Promise<void> {
// Repository URL // Repository URL
core.info( core.info(
`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}` `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
) )
const repositoryUrl = urlHelper.getFetchUrl(settings) const repositoryUrl = settings.sshKey
? `git@${hostname}:${encodeURIComponent(
settings.repositoryOwner
)}/${encodeURIComponent(settings.repositoryName)}.git`
: `https://${hostname}/${encodeURIComponent(
settings.repositoryOwner
)}/${encodeURIComponent(settings.repositoryName)}`
// Remove conflicting file path // Remove conflicting file path
if (fsHelper.fileExistsSync(settings.repositoryPath)) { if (fsHelper.fileExistsSync(settings.repositoryPath)) {
@@ -32,117 +39,67 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
} }
// Git command manager // Git command manager
core.startGroup('Getting Git version info')
const git = await getGitCommandManager(settings) const git = await getGitCommandManager(settings)
core.endGroup()
let authHelper: gitAuthHelper.IGitAuthHelper | null = null // Prepare existing directory, otherwise recreate
if (isExisting) {
await gitDirectoryHelper.prepareExistingDirectory(
git,
settings.repositoryPath,
repositoryUrl,
settings.clean
)
}
if (!git) {
// Downloading using REST API
core.info(`The repository will be downloaded using the GitHub REST API`)
core.info(
`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
)
if (settings.submodules) {
throw new Error(
`Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
)
} else if (settings.sshKey) {
throw new Error(
`Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
)
}
await githubApiHelper.downloadRepository(
settings.authToken,
settings.repositoryOwner,
settings.repositoryName,
settings.ref,
settings.commit,
settings.repositoryPath
)
return
}
// Save state for POST action
stateHelper.setRepositoryPath(settings.repositoryPath)
// Initialize the repository
if (
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
) {
await git.init()
await git.remoteAdd('origin', repositoryUrl)
}
// Disable automatic garbage collection
if (!(await git.tryDisableAutomaticGarbageCollection())) {
core.warning(
`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
)
}
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
try { try {
if (git) {
authHelper = gitAuthHelper.createAuthHelper(git, settings)
if (settings.setSafeDirectory) {
// Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail
// Otherwise all git commands we run in a container fail
await authHelper.configureTempGlobalConfig()
core.info(
`Adding repository directory to the temporary git global config as a safe directory`
)
await git
.config('safe.directory', settings.repositoryPath, true, true)
.catch(error => {
core.info(
`Failed to initialize safe directory with error: ${error}`
)
})
stateHelper.setSafeDirectory()
}
}
// Prepare existing directory, otherwise recreate
if (isExisting) {
await gitDirectoryHelper.prepareExistingDirectory(
git,
settings.repositoryPath,
repositoryUrl,
settings.clean,
settings.ref
)
}
if (!git) {
// Downloading using REST API
core.info(`The repository will be downloaded using the GitHub REST API`)
core.info(
`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
)
if (settings.submodules) {
throw new Error(
`Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
)
} else if (settings.sshKey) {
throw new Error(
`Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
)
}
await githubApiHelper.downloadRepository(
settings.authToken,
settings.repositoryOwner,
settings.repositoryName,
settings.ref,
settings.commit,
settings.repositoryPath
)
return
}
// Save state for POST action
stateHelper.setRepositoryPath(settings.repositoryPath)
// Initialize the repository
if (
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
) {
core.startGroup('Initializing the repository')
await git.init()
await git.remoteAdd('origin', repositoryUrl)
core.endGroup()
}
// Disable automatic garbage collection
core.startGroup('Disabling automatic garbage collection')
if (!(await git.tryDisableAutomaticGarbageCollection())) {
core.warning(
`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
)
}
core.endGroup()
// If we didn't initialize it above, do it now
if (!authHelper) {
authHelper = gitAuthHelper.createAuthHelper(git, settings)
}
// Configure auth // Configure auth
core.startGroup('Setting up auth')
await authHelper.configureAuth() await authHelper.configureAuth()
core.endGroup()
// Determine the default branch
if (!settings.ref && !settings.commit) {
core.startGroup('Determining the default branch')
if (settings.sshKey) {
settings.ref = await git.getDefaultBranch(repositoryUrl)
} else {
settings.ref = await githubApiHelper.getDefaultBranch(
settings.authToken,
settings.repositoryOwner,
settings.repositoryName
)
}
core.endGroup()
}
// LFS install // LFS install
if (settings.lfs) { if (settings.lfs) {
@@ -150,99 +107,59 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
} }
// Fetch // Fetch
core.startGroup('Fetching the repository') const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
if (settings.fetchDepth <= 0) { await git.fetch(settings.fetchDepth, refSpec)
// Fetch all branches and tags
let refSpec = refHelper.getRefSpecForAllHistory(
settings.ref,
settings.commit
)
await git.fetch(refSpec)
// When all history is fetched, the ref we're interested in may have moved to a different
// commit (push or force push). If so, fetch again with a targeted refspec.
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
await git.fetch(refSpec)
}
} else {
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
await git.fetch(refSpec, settings.fetchDepth)
}
core.endGroup()
// Checkout info // Checkout info
core.startGroup('Determining the checkout info')
const checkoutInfo = await refHelper.getCheckoutInfo( const checkoutInfo = await refHelper.getCheckoutInfo(
git, git,
settings.ref, settings.ref,
settings.commit settings.commit
) )
core.endGroup()
// LFS fetch // LFS fetch
// Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time). // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
// Explicit lfs fetch will fetch lfs objects in parallel. // Explicit lfs fetch will fetch lfs objects in parallel.
if (settings.lfs) { if (settings.lfs) {
core.startGroup('Fetching LFS objects')
await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref) await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
core.endGroup()
} }
// Checkout // Checkout
core.startGroup('Checking out the ref')
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint) await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
core.endGroup()
// Submodules // Submodules
if (settings.submodules) { if (settings.submodules) {
// Temporarily override global config try {
core.startGroup('Setting up auth for fetching submodules') // Temporarily override global config
await authHelper.configureGlobalAuth() await authHelper.configureGlobalAuth()
core.endGroup()
// Checkout submodules // Checkout submodules
core.startGroup('Fetching submodules') await git.submoduleSync(settings.nestedSubmodules)
await git.submoduleSync(settings.nestedSubmodules) await git.submoduleUpdate(
await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules) settings.fetchDepth,
await git.submoduleForeach( settings.nestedSubmodules
'git config --local gc.auto 0', )
settings.nestedSubmodules await git.submoduleForeach(
) 'git config --local gc.auto 0',
core.endGroup() settings.nestedSubmodules
)
// Persist credentials // Persist credentials
if (settings.persistCredentials) { if (settings.persistCredentials) {
core.startGroup('Persisting credentials for submodules') await authHelper.configureSubmoduleAuth()
await authHelper.configureSubmoduleAuth() }
core.endGroup() } finally {
// Remove temporary global config override
await authHelper.removeGlobalAuth()
} }
} }
// Get commit information // Dump some info about the checked out commit
const commitInfo = await git.log1() await git.log1()
// Log commit sha
await git.log1("--format='%H'")
// Check for incorrect pull request merge commit
await refHelper.checkCommitInfo(
settings.authToken,
commitInfo,
settings.repositoryOwner,
settings.repositoryName,
settings.ref,
settings.commit
)
} finally { } finally {
// Remove auth // Remove auth
if (authHelper) { if (!settings.persistCredentials) {
if (!settings.persistCredentials) { await authHelper.removeAuth()
core.startGroup('Removing auth')
await authHelper.removeAuth()
core.endGroup()
}
authHelper.removeGlobalConfig()
} }
} }
} }
@@ -265,26 +182,7 @@ export async function cleanup(repositoryPath: string): Promise<void> {
// Remove auth // Remove auth
const authHelper = gitAuthHelper.createAuthHelper(git) const authHelper = gitAuthHelper.createAuthHelper(git)
try { await authHelper.removeAuth()
if (stateHelper.PostSetSafeDirectory) {
// Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail
// Otherwise all git commands we run in a container fail
await authHelper.configureTempGlobalConfig()
core.info(
`Adding repository directory to the temporary git global config as a safe directory`
)
await git
.config('safe.directory', repositoryPath, true, true)
.catch(error => {
core.info(`Failed to initialize safe directory with error: ${error}`)
})
}
await authHelper.removeAuth()
} finally {
await authHelper.removeGlobalConfig()
}
} }
async function getGitCommandManager( async function getGitCommandManager(
-75
View File
@@ -1,92 +1,17 @@
export interface IGitSourceSettings { export interface IGitSourceSettings {
/**
* The location on disk where the repository will be placed
*/
repositoryPath: string repositoryPath: string
/**
* The repository owner
*/
repositoryOwner: string repositoryOwner: string
/**
* The repository name
*/
repositoryName: string repositoryName: string
/**
* The ref to fetch
*/
ref: string ref: string
/**
* The commit to checkout
*/
commit: string commit: string
/**
* Indicates whether to clean the repository
*/
clean: boolean clean: boolean
/**
* The depth when fetching
*/
fetchDepth: number fetchDepth: number
/**
* Indicates whether to fetch LFS objects
*/
lfs: boolean lfs: boolean
/**
* Indicates whether to checkout submodules
*/
submodules: boolean submodules: boolean
/**
* Indicates whether to recursively checkout submodules
*/
nestedSubmodules: boolean nestedSubmodules: boolean
/**
* The auth token to use when fetching the repository
*/
authToken: string authToken: string
/**
* The SSH key to configure
*/
sshKey: string sshKey: string
/**
* Additional SSH known hosts
*/
sshKnownHosts: string sshKnownHosts: string
/**
* Indicates whether the server must be a known host
*/
sshStrict: boolean sshStrict: boolean
/**
* Indicates whether to persist the credentials on disk to enable scripting authenticated git commands
*/
persistCredentials: boolean persistCredentials: boolean
/**
* Organization ID for the currently running workflow (used for auth settings)
*/
workflowOrganizationId: number | undefined
/**
* Indicates whether to add repositoryPath as safe.directory in git global config
*/
setSafeDirectory: boolean
/**
* Opt-in to allow checking out fork pull request code from a workflow
* triggered by pull_request_target or workflow_run.
*/
allowUnsafePrCheckout: boolean
} }
+4 -53
View File
@@ -7,7 +7,7 @@ import * as path from 'path'
import * as retryHelper from './retry-helper' import * as retryHelper from './retry-helper'
import * as toolCache from '@actions/tool-cache' import * as toolCache from '@actions/tool-cache'
import {default as uuid} from 'uuid/v4' import {default as uuid} from 'uuid/v4'
import {Octokit} from '@octokit/rest' import {ReposGetArchiveLinkParams} from '@octokit/rest'
const IS_WINDOWS = process.platform === 'win32' const IS_WINDOWS = process.platform === 'win32'
@@ -19,12 +19,6 @@ export async function downloadRepository(
commit: string, commit: string,
repositoryPath: string repositoryPath: string
): Promise<void> { ): Promise<void> {
// Determine the default branch
if (!ref && !commit) {
core.info('Determining the default branch')
ref = await getDefaultBranch(authToken, owner, repo)
}
// Download the archive // Download the archive
let archiveData = await retryHelper.execute(async () => { let archiveData = await retryHelper.execute(async () => {
core.info('Downloading the archive') core.info('Downloading the archive')
@@ -47,7 +41,7 @@ export async function downloadRepository(
} else { } else {
await toolCache.extractTar(archivePath, extractPath) await toolCache.extractTar(archivePath, extractPath)
} }
await io.rmRF(archivePath) io.rmRF(archivePath)
// Determine the path of the repository content. The archive contains // Determine the path of the repository content. The archive contains
// a top-level folder and the repository content is inside. // a top-level folder and the repository content is inside.
@@ -70,50 +64,7 @@ export async function downloadRepository(
await io.mv(sourcePath, targetPath) await io.mv(sourcePath, targetPath)
} }
} }
await io.rmRF(extractPath) io.rmRF(extractPath)
}
/**
* Looks up the default branch name
*/
export async function getDefaultBranch(
authToken: string,
owner: string,
repo: string
): Promise<string> {
return await retryHelper.execute(async () => {
core.info('Retrieving the default branch name')
const octokit = new github.GitHub(authToken)
let result: string
try {
// Get the default branch from the repo info
const response = await octokit.repos.get({owner, repo})
result = response.data.default_branch
assert.ok(result, 'default_branch cannot be empty')
} catch (err) {
// Handle .wiki repo
if (
(err as any)?.status === 404 &&
repo.toUpperCase().endsWith('.WIKI')
) {
result = 'master'
}
// Otherwise error
else {
throw err
}
}
// Print the default branch
core.info(`Default branch '${result}'`)
// Prefix with 'refs/heads'
if (!result.startsWith('refs/')) {
result = `refs/heads/${result}`
}
return result
})
} }
async function downloadArchive( async function downloadArchive(
@@ -124,7 +75,7 @@ async function downloadArchive(
commit: string commit: string
): Promise<Buffer> { ): Promise<Buffer> {
const octokit = new github.GitHub(authToken) const octokit = new github.GitHub(authToken)
const params: Octokit.ReposGetArchiveLinkParams = { const params: ReposGetArchiveLinkParams = {
owner: owner, owner: owner,
repo: repo, repo: repo,
archive_format: IS_WINDOWS ? 'zipball' : 'tarball', archive_format: IS_WINDOWS ? 'zipball' : 'tarball',
+9 -51
View File
@@ -2,11 +2,9 @@ import * as core from '@actions/core'
import * as fsHelper from './fs-helper' import * as fsHelper from './fs-helper'
import * as github from '@actions/github' import * as github from '@actions/github'
import * as path from 'path' import * as path from 'path'
import * as unsafePrCheckoutHelper from './unsafe-pr-checkout-helper'
import * as workflowContextHelper from './workflow-context-helper'
import {IGitSourceSettings} from './git-source-settings' import {IGitSourceSettings} from './git-source-settings'
export async function getInputs(): Promise<IGitSourceSettings> { export function getInputs(): IGitSourceSettings {
const result = ({} as unknown) as IGitSourceSettings const result = ({} as unknown) as IGitSourceSettings
// GitHub workspace // GitHub workspace
@@ -59,38 +57,25 @@ export async function getInputs(): Promise<IGitSourceSettings> {
// Source branch, source version // Source branch, source version
result.ref = core.getInput('ref') result.ref = core.getInput('ref')
// core.getInput()'s default trim strips a range of Unicode characters such as a
// leading BOM (U+FEFF) or NBSP (U+00A0). Those are valid in a git ref name, so
// a fork branch named "<BOM>" + 40 hex chars would trim down to a bare SHA and
// be silently reclassified as a commit, bypassing the unsafe fork PR checkout
// guard.
//
// The trim below strips only the ASCII whitespace characters which are all forbidden
// in a git branch name.
// \t U+0009 horizontal tab - ASCII control, forbidden in ref names
// \n U+000A line feed - ASCII control, forbidden in ref names
// \v U+000B vertical tab - ASCII control, forbidden in ref names
// \f U+000C form feed - ASCII control, forbidden in ref names
// \r U+000D carriage return - ASCII control, forbidden in ref names
// ' ' U+0020 space - forbidden in ref names
const asciiTrimmedRef = core
.getInput('ref', {trimWhitespace: false})
.replace(/^[\t\n\v\f\r ]+|[\t\n\v\f\r ]+$/g, '')
if (!result.ref) { if (!result.ref) {
if (isWorkflowRepository) { if (isWorkflowRepository) {
result.ref = github.context.ref result.ref = github.context.ref
result.commit = github.context.sha result.commit = github.context.sha
// Some events have an unqualifed ref. For example when a PR is merged (pull_request closed event), // Some events have an unqualifed ref. For example when a PR is merged (pull_request closed event),
// the ref is unqualifed like "main" instead of "refs/heads/main". // the ref is unqualifed like "master" instead of "refs/heads/master".
if (result.commit && result.ref && !result.ref.startsWith('refs/')) { if (result.commit && result.ref && !result.ref.startsWith('refs/')) {
result.ref = `refs/heads/${result.ref}` result.ref = `refs/heads/${result.ref}`
} }
} }
if (!result.ref && !result.commit) {
result.ref = 'refs/heads/master'
}
} }
// SHA? // SHA?
else if (asciiTrimmedRef.match(/^[0-9a-fA-F]{40}$/)) { else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) {
result.commit = asciiTrimmedRef result.commit = result.ref
result.ref = '' result.ref = ''
} }
core.debug(`ref = '${result.ref}'`) core.debug(`ref = '${result.ref}'`)
@@ -125,7 +110,7 @@ export async function getInputs(): Promise<IGitSourceSettings> {
core.debug(`recursive submodules = ${result.nestedSubmodules}`) core.debug(`recursive submodules = ${result.nestedSubmodules}`)
// Auth token // Auth token
result.authToken = core.getInput('token', {required: true}) result.authToken = core.getInput('token')
// SSH // SSH
result.sshKey = core.getInput('ssh-key') result.sshKey = core.getInput('ssh-key')
@@ -137,32 +122,5 @@ export async function getInputs(): Promise<IGitSourceSettings> {
result.persistCredentials = result.persistCredentials =
(core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE' (core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE'
// Workflow organization ID
result.workflowOrganizationId = await workflowContextHelper.getOrganizationId()
// Set safe.directory in git global config.
result.setSafeDirectory =
(core.getInput('set-safe-directory') || 'true').toUpperCase() === 'TRUE'
// 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}`)
// The default self-checkout (this repository with no explicit ref) always
// resolves to the trusted ref/commit GitHub set for the triggering event, so
// the fork-checkout guard only needs to run when the caller customized the
// repository or ref.
const isDefaultCheckout = isWorkflowRepository && !core.getInput('ref')
if (!isDefaultCheckout) {
unsafePrCheckoutHelper.assertSafePrCheckout({
qualifiedRepository,
ref: result.ref,
commit: result.commit,
allowUnsafePrCheckout: result.allowUnsafePrCheckout
})
}
return result return result
} }
+3 -3
View File
@@ -7,7 +7,7 @@ import * as stateHelper from './state-helper'
async function run(): Promise<void> { async function run(): Promise<void> {
try { try {
const sourceSettings = await inputHelper.getInputs() const sourceSettings = inputHelper.getInputs()
try { try {
// Register problem matcher // Register problem matcher
@@ -24,7 +24,7 @@ async function run(): Promise<void> {
coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '') coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '')
} }
} catch (error) { } catch (error) {
core.setFailed(`${(error as any)?.message ?? error}`) core.setFailed(error.message)
} }
} }
@@ -32,7 +32,7 @@ async function cleanup(): Promise<void> {
try { try {
await gitSourceProvider.cleanup(stateHelper.RepositoryPath) await gitSourceProvider.cleanup(stateHelper.RepositoryPath)
} catch (error) { } catch (error) {
core.warning(`${(error as any)?.message ?? error}`) core.warning(error.message)
} }
} }
+4 -4
View File
@@ -10,10 +10,10 @@ import * as yaml from 'js-yaml'
function updateUsage( function updateUsage(
actionReference: string, actionReference: string,
actionYamlPath = 'action.yml', actionYamlPath: string = 'action.yml',
readmePath = 'README.md', readmePath: string = 'README.md',
startToken = '<!-- start usage -->', startToken: string = '<!-- start usage -->',
endToken = '<!-- end usage -->' endToken: string = '<!-- end usage -->'
): void { ): void {
if (!actionReference) { if (!actionReference) {
throw new Error('Parameter actionReference must not be empty') throw new Error('Parameter actionReference must not be empty')
-8
View File
@@ -1,8 +0,0 @@
#!/bin/bash
set -e
src/misc/licensed-download.sh
echo 'Running: licensed cached'
_temp/licensed-3.6.0/licensed status
-24
View File
@@ -1,24 +0,0 @@
#!/bin/bash
set -e
if [ ! -f _temp/licensed-3.6.0.done ]; then
echo 'Clearing temp'
rm -rf _temp/licensed-3.6.0 || true
echo 'Downloading licensed'
mkdir -p _temp/licensed-3.6.0
pushd _temp/licensed-3.6.0
if [[ "$OSTYPE" == "darwin"* ]]; then
curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.6.0/licensed-3.6.0-darwin-x64.tar.gz
else
curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.6.0/licensed-3.6.0-linux-x64.tar.gz
fi
echo 'Extracting licenesed'
tar -xzf licensed.tar.gz
popd
touch _temp/licensed-3.6.0.done
else
echo 'Licensed already downloaded'
fi
-8
View File
@@ -1,8 +0,0 @@
#!/bin/bash
set -e
src/misc/licensed-download.sh
echo 'Running: licensed cached'
_temp/licensed-3.6.0/licensed cache
-176
View File
@@ -1,9 +1,4 @@
import {URL} from 'url'
import {IGitCommandManager} from './git-command-manager' import {IGitCommandManager} from './git-command-manager'
import * as core from '@actions/core'
import * as github from '@actions/github'
export const tagsRefSpec = '+refs/tags/*:refs/tags/*'
export interface ICheckoutInfo { export interface ICheckoutInfo {
ref: string ref: string
@@ -62,16 +57,6 @@ export async function getCheckoutInfo(
return result return result
} }
export function getRefSpecForAllHistory(ref: string, commit: string): string[] {
const result = ['+refs/heads/*:refs/remotes/origin/*', tagsRefSpec]
if (ref && ref.toUpperCase().startsWith('REFS/PULL/')) {
const branch = ref.substring('refs/pull/'.length)
result.push(`+${commit || ref}:refs/remotes/pull/${branch}`)
}
return result
}
export function getRefSpec(ref: string, commit: string): string[] { export function getRefSpec(ref: string, commit: string): string[] {
if (!ref && !commit) { if (!ref && !commit) {
throw new Error('Args ref and commit cannot both be empty') throw new Error('Args ref and commit cannot both be empty')
@@ -122,164 +107,3 @@ export function getRefSpec(ref: string, commit: string): string[] {
return [`+${ref}:${ref}`] return [`+${ref}:${ref}`]
} }
} }
/**
* Tests whether the initial fetch created the ref at the expected commit
*/
export async function testRef(
git: IGitCommandManager,
ref: string,
commit: string
): Promise<boolean> {
if (!git) {
throw new Error('Arg git cannot be empty')
}
if (!ref && !commit) {
throw new Error('Args ref and commit cannot both be empty')
}
// No SHA? Nothing to test
if (!commit) {
return true
}
// SHA only?
else if (!ref) {
return await git.shaExists(commit)
}
const upperRef = ref.toUpperCase()
// refs/heads/
if (upperRef.startsWith('REFS/HEADS/')) {
const branch = ref.substring('refs/heads/'.length)
return (
(await git.branchExists(true, `origin/${branch}`)) &&
commit === (await git.revParse(`refs/remotes/origin/${branch}`))
)
}
// refs/pull/
else if (upperRef.startsWith('REFS/PULL/')) {
// Assume matches because fetched using the commit
return true
}
// refs/tags/
else if (upperRef.startsWith('REFS/TAGS/')) {
const tagName = ref.substring('refs/tags/'.length)
return (
(await git.tagExists(tagName)) && commit === (await git.revParse(ref))
)
}
// Unexpected
else {
core.debug(`Unexpected ref format '${ref}' when testing ref info`)
return true
}
}
export async function checkCommitInfo(
token: string,
commitInfo: string,
repositoryOwner: string,
repositoryName: string,
ref: string,
commit: string
): Promise<void> {
try {
// GHES?
if (isGhes()) {
return
}
// Auth token?
if (!token) {
return
}
// Public PR synchronize, for workflow repo?
if (
fromPayload('repository.private') !== false ||
github.context.eventName !== 'pull_request' ||
fromPayload('action') !== 'synchronize' ||
repositoryOwner !== github.context.repo.owner ||
repositoryName !== github.context.repo.repo ||
ref !== github.context.ref ||
!ref.startsWith('refs/pull/') ||
commit !== github.context.sha
) {
return
}
// Head SHA
const expectedHeadSha = fromPayload('after')
if (!expectedHeadSha) {
core.debug('Unable to determine head sha')
return
}
// Base SHA
const expectedBaseSha = fromPayload('pull_request.base.sha')
if (!expectedBaseSha) {
core.debug('Unable to determine base sha')
return
}
// Expected message?
const expectedMessage = `Merge ${expectedHeadSha} into ${expectedBaseSha}`
if (commitInfo.indexOf(expectedMessage) >= 0) {
return
}
// Extract details from message
const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/)
if (!match) {
core.debug('Unexpected message format')
return
}
// Post telemetry
const actualHeadSha = match[1]
if (actualHeadSha !== expectedHeadSha) {
core.debug(
`Expected head sha ${expectedHeadSha}; actual head sha ${actualHeadSha}`
)
const octokit = new github.GitHub(token, {
userAgent: `actions-checkout-tracepoint/1.0 (code=STALE_MERGE;owner=${repositoryOwner};repo=${repositoryName};pr=${fromPayload(
'number'
)};run_id=${
process.env['GITHUB_RUN_ID']
};expected_head_sha=${expectedHeadSha};actual_head_sha=${actualHeadSha})`
})
await octokit.repos.get({owner: repositoryOwner, repo: repositoryName})
}
} catch (err) {
core.debug(
`Error when validating commit info: ${(err as any)?.stack ?? err}`
)
}
}
export function fromPayload(path: string): any {
return select(github.context.payload, path)
}
function select(obj: any, path: string): any {
if (!obj) {
return undefined
}
const i = path.indexOf('.')
if (i < 0) {
return obj[path]
}
const key = path.substr(0, i)
return select(obj[key], path.substr(i + 1))
}
function isGhes(): boolean {
const ghUrl = new URL(
process.env['GITHUB_SERVER_URL'] || 'https://github.com'
)
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
}
+1 -1
View File
@@ -29,7 +29,7 @@ export class RetryHelper {
try { try {
return await action() return await action()
} catch (err) { } catch (err) {
core.info((err as any)?.message) core.info(err.message)
} }
// Sleep // Sleep
+19 -21
View File
@@ -1,60 +1,58 @@
import * as core from '@actions/core' import * as coreCommand from '@actions/core/lib/command'
/** /**
* Indicates whether the POST action is running * Indicates whether the POST action is running
*/ */
export const IsPost = !!core.getState('isPost') export const IsPost = !!process.env['STATE_isPost']
/** /**
* The repository path for the POST action. The value is empty during the MAIN action. * The repository path for the POST action. The value is empty during the MAIN action.
*/ */
export const RepositoryPath = core.getState('repositoryPath') export const RepositoryPath =
(process.env['STATE_repositoryPath'] as string) || ''
/**
* The set-safe-directory for the POST action. The value is set if input: 'safe-directory' is set during the MAIN action.
*/
export const PostSetSafeDirectory = core.getState('setSafeDirectory') === 'true'
/** /**
* The SSH key path for the POST action. The value is empty during the MAIN action. * The SSH key path for the POST action. The value is empty during the MAIN action.
*/ */
export const SshKeyPath = core.getState('sshKeyPath') export const SshKeyPath = (process.env['STATE_sshKeyPath'] as string) || ''
/** /**
* The SSH known hosts path for the POST action. The value is empty during the MAIN action. * The SSH known hosts path for the POST action. The value is empty during the MAIN action.
*/ */
export const SshKnownHostsPath = core.getState('sshKnownHostsPath') export const SshKnownHostsPath =
(process.env['STATE_sshKnownHostsPath'] as string) || ''
/** /**
* Save the repository path so the POST action can retrieve the value. * Save the repository path so the POST action can retrieve the value.
*/ */
export function setRepositoryPath(repositoryPath: string) { export function setRepositoryPath(repositoryPath: string) {
core.saveState('repositoryPath', repositoryPath) coreCommand.issueCommand(
'save-state',
{name: 'repositoryPath'},
repositoryPath
)
} }
/** /**
* Save the SSH key path so the POST action can retrieve the value. * Save the SSH key path so the POST action can retrieve the value.
*/ */
export function setSshKeyPath(sshKeyPath: string) { export function setSshKeyPath(sshKeyPath: string) {
core.saveState('sshKeyPath', sshKeyPath) coreCommand.issueCommand('save-state', {name: 'sshKeyPath'}, sshKeyPath)
} }
/** /**
* Save the SSH known hosts path so the POST action can retrieve the value. * Save the SSH known hosts path so the POST action can retrieve the value.
*/ */
export function setSshKnownHostsPath(sshKnownHostsPath: string) { export function setSshKnownHostsPath(sshKnownHostsPath: string) {
core.saveState('sshKnownHostsPath', sshKnownHostsPath) coreCommand.issueCommand(
} 'save-state',
{name: 'sshKnownHostsPath'},
/** sshKnownHostsPath
* Save the sef-safe-directory input so the POST action can retrieve the value. )
*/
export function setSafeDirectory() {
core.saveState('setSafeDirectory', 'true')
} }
// Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic. // Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic.
// This is necessary since we don't have a separate entry point. // This is necessary since we don't have a separate entry point.
if (!IsPost) { if (!IsPost) {
core.saveState('isPost', 'true') coreCommand.issueCommand('save-state', {name: 'isPost'}, 'true')
} }
-88
View File
@@ -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())
}
}
-29
View File
@@ -1,29 +0,0 @@
import * as assert from 'assert'
import {IGitSourceSettings} from './git-source-settings'
import {URL} from 'url'
export function getFetchUrl(settings: IGitSourceSettings): string {
assert.ok(
settings.repositoryOwner,
'settings.repositoryOwner must be defined'
)
assert.ok(settings.repositoryName, 'settings.repositoryName must be defined')
const serviceUrl = getServerUrl()
const encodedOwner = encodeURIComponent(settings.repositoryOwner)
const encodedName = encodeURIComponent(settings.repositoryName)
if (settings.sshKey) {
return `git@${serviceUrl.hostname}:${encodedOwner}/${encodedName}.git`
}
// "origin" is SCHEME://HOSTNAME[:PORT]
return `${serviceUrl.origin}/${encodedOwner}/${encodedName}`
}
export function getServerUrl(): URL {
// todo: remove GITHUB_URL after support for GHES Alpha is no longer needed
return new URL(
process.env['GITHUB_SERVER_URL'] ||
process.env['GITHUB_URL'] ||
'https://github.com'
)
}
-30
View File
@@ -1,30 +0,0 @@
import * as core from '@actions/core'
import * as fs from 'fs'
/**
* Gets the organization ID of the running workflow or undefined if the value cannot be loaded from the GITHUB_EVENT_PATH
*/
export async function getOrganizationId(): Promise<number | undefined> {
try {
const eventPath = process.env.GITHUB_EVENT_PATH
if (!eventPath) {
core.debug(`GITHUB_EVENT_PATH is not defined`)
return
}
const content = await fs.promises.readFile(eventPath, {encoding: 'utf8'})
const event = JSON.parse(content)
const id = event?.repository?.owner?.id
if (typeof id !== 'number') {
core.debug('Repository owner ID not found within GITHUB event info')
return
}
return id as number
} catch (err) {
core.debug(
`Unable to load organization ID from GITHUB_EVENT_PATH: ${(err as any)
.message || err}`
)
}
}
+1 -2
View File
@@ -10,8 +10,7 @@
"declaration": true, "declaration": true,
"strict": true, "strict": true,
"noImplicitAny": false, "noImplicitAny": false,
"esModuleInterop": true, "esModuleInterop": true
"skipLibCheck": true
}, },
"exclude": ["__test__", "lib", "node_modules"] "exclude": ["__test__", "lib", "node_modules"]
} }