Compare commits
6 Commits
main
...
users/eric
Author | SHA1 | Date | |
---|---|---|---|
|
43c99f2ebc | ||
|
4a3a4ebf11 | ||
|
a5ba5cb63a | ||
|
31b1047b1f | ||
|
89cbb18acd | ||
|
1e6a918852 |
@ -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
.gitattributes
vendored
1
.gitattributes
vendored
@ -1 +0,0 @@
|
|||||||
.licenses/** -diff linguist-generated=true
|
|
20
.github/dependabot.yml
vendored
20
.github/dependabot.yml
vendored
@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
groups:
|
|
||||||
minor-npm-dependencies:
|
|
||||||
# NPM: Only group minor and patch updates (we want to carefully review major updates)
|
|
||||||
update-types: [minor, patch]
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
groups:
|
|
||||||
minor-actions-dependencies:
|
|
||||||
# GitHub Actions: Only group minor and patch updates (we want to carefully review major updates)
|
|
||||||
update-types: [minor, patch]
|
|
51
.github/workflows/check-dist.yml
vendored
51
.github/workflows/check-dist.yml
vendored
@ -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@v3
|
|
||||||
|
|
||||||
- name: Set Node.js 20.x
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20.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
.github/workflows/codeql-analysis.yml
vendored
58
.github/workflows/codeql-analysis.yml
vendored
@ -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@v3
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v3
|
|
||||||
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@v3
|
|
14
.github/workflows/licensed.yml
vendored
14
.github/workflows/licensed.yml
vendored
@ -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@v3
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run licensed-check
|
|
304
.github/workflows/test.yml
vendored
304
.github/workflows/test.yml
vendored
@ -1,297 +1,31 @@
|
|||||||
name: Build and Test
|
name: Build and Test
|
||||||
|
|
||||||
on:
|
on: push
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- releases/*
|
|
||||||
|
|
||||||
|
|
||||||
# Note that when you see patterns like "ref: test-data/v2/basic" within this workflow,
|
|
||||||
# these refer to "test-data" branches on this actions/checkout repo.
|
|
||||||
# (For example, test-data/v2/basic -> https://github.com/actions/checkout/tree/test-data/v2/basic)
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
test-archive:
|
||||||
runs-on: ubuntu-latest
|
runs-on: windows-latest
|
||||||
steps:
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20.x
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run build
|
|
||||||
- run: npm run format-check
|
|
||||||
- run: npm run lint
|
|
||||||
- run: npm test
|
|
||||||
- name: Verify no unstaged changes
|
|
||||||
run: __test__/verify-no-unstaged-changes.sh
|
|
||||||
|
|
||||||
test:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
runs-on: [ubuntu-latest, macos-latest, windows-latest]
|
|
||||||
runs-on: ${{ matrix.runs-on }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Clone this repo
|
# Clone this repo
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.1.1
|
shell: bash
|
||||||
|
run: |
|
||||||
|
curl --location --user token:${{ github.token }} --output checkout.tar.gz https://api.github.com/repos/actions/checkout/tarball/${{ github.sha }}
|
||||||
|
tar -xzf checkout.tar.gz
|
||||||
|
mv */* ./
|
||||||
|
|
||||||
# Basic checkout
|
# Basic checkout
|
||||||
- name: Checkout basic
|
- shell: cmd
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/basic
|
|
||||||
path: basic
|
|
||||||
- name: Verify basic
|
|
||||||
shell: bash
|
|
||||||
run: __test__/verify-basic.sh
|
|
||||||
|
|
||||||
# Clean
|
|
||||||
- name: Modify work tree
|
|
||||||
shell: bash
|
|
||||||
run: __test__/modify-work-tree.sh
|
|
||||||
- name: Checkout clean
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/basic
|
|
||||||
path: basic
|
|
||||||
- name: Verify clean
|
|
||||||
shell: bash
|
|
||||||
run: __test__/verify-clean.sh
|
|
||||||
|
|
||||||
# Side by side
|
|
||||||
- name: Checkout side by side 1
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/side-by-side-1
|
|
||||||
path: side-by-side-1
|
|
||||||
- name: Checkout side by side 2
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/side-by-side-2
|
|
||||||
path: side-by-side-2
|
|
||||||
- name: Verify side by side
|
|
||||||
shell: bash
|
|
||||||
run: __test__/verify-side-by-side.sh
|
|
||||||
|
|
||||||
# Filter
|
|
||||||
- name: Fetch filter
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
filter: 'blob:none'
|
|
||||||
path: fetch-filter
|
|
||||||
|
|
||||||
- name: Verify fetch filter
|
|
||||||
run: __test__/verify-fetch-filter.sh
|
|
||||||
|
|
||||||
# Sparse checkout
|
|
||||||
- name: Sparse checkout
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
sparse-checkout: |
|
|
||||||
__test__
|
|
||||||
.github
|
|
||||||
dist
|
|
||||||
path: sparse-checkout
|
|
||||||
|
|
||||||
- name: Verify sparse checkout
|
|
||||||
run: __test__/verify-sparse-checkout.sh
|
|
||||||
|
|
||||||
# Disabled sparse checkout in existing checkout
|
|
||||||
- name: Disabled sparse checkout
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
path: sparse-checkout
|
|
||||||
|
|
||||||
- name: Verify disabled sparse checkout
|
|
||||||
shell: bash
|
|
||||||
run: set -x && ls -l sparse-checkout/src/git-command-manager.ts
|
|
||||||
|
|
||||||
# Sparse checkout (non-cone mode)
|
|
||||||
- name: Sparse checkout (non-cone mode)
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
sparse-checkout: |
|
|
||||||
/__test__/
|
|
||||||
/.github/
|
|
||||||
/dist/
|
|
||||||
sparse-checkout-cone-mode: false
|
|
||||||
path: sparse-checkout-non-cone-mode
|
|
||||||
|
|
||||||
- name: Verify sparse checkout (non-cone mode)
|
|
||||||
run: __test__/verify-sparse-checkout-non-cone-mode.sh
|
|
||||||
|
|
||||||
# LFS
|
|
||||||
- name: Checkout LFS
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
repository: actions/checkout # hardcoded, otherwise doesn't work from a fork
|
|
||||||
ref: test-data/v2/lfs
|
|
||||||
path: lfs
|
|
||||||
lfs: true
|
|
||||||
- name: Verify LFS
|
|
||||||
shell: bash
|
|
||||||
run: __test__/verify-lfs.sh
|
|
||||||
|
|
||||||
# Submodules false
|
|
||||||
- name: Checkout submodules false
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/submodule-ssh-url
|
|
||||||
path: submodules-false
|
|
||||||
- name: Verify submodules false
|
|
||||||
run: __test__/verify-submodules-false.sh
|
|
||||||
|
|
||||||
# Submodules one level
|
|
||||||
- name: Checkout submodules true
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/submodule-ssh-url
|
|
||||||
path: submodules-true
|
|
||||||
submodules: true
|
|
||||||
- name: Verify submodules true
|
|
||||||
run: __test__/verify-submodules-true.sh
|
|
||||||
|
|
||||||
# Submodules recursive
|
|
||||||
- name: Checkout submodules recursive
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/submodule-ssh-url
|
|
||||||
path: submodules-recursive
|
|
||||||
submodules: recursive
|
|
||||||
- name: Verify submodules recursive
|
|
||||||
run: __test__/verify-submodules-recursive.sh
|
|
||||||
|
|
||||||
# Basic checkout using REST API
|
|
||||||
- name: Remove basic
|
|
||||||
if: runner.os != 'windows'
|
|
||||||
run: rm -rf basic
|
|
||||||
- name: Remove basic (Windows)
|
|
||||||
if: runner.os == 'windows'
|
|
||||||
shell: cmd
|
|
||||||
run: rmdir /s /q basic
|
|
||||||
- name: Override git version
|
|
||||||
if: runner.os != 'windows'
|
|
||||||
run: __test__/override-git-version.sh
|
|
||||||
- name: Override git version (Windows)
|
|
||||||
if: runner.os == 'windows'
|
|
||||||
run: __test__\\override-git-version.cmd
|
|
||||||
- name: Checkout basic using REST API
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/basic
|
|
||||||
path: basic
|
|
||||||
- name: Verify basic
|
|
||||||
run: __test__/verify-basic.sh --archive
|
|
||||||
|
|
||||||
test-proxy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: ghcr.io/actions/test-ubuntu-git:main.20240221.114913.703z
|
|
||||||
options: --dns 127.0.0.1
|
|
||||||
services:
|
|
||||||
squid-proxy:
|
|
||||||
image: ubuntu/squid:latest
|
|
||||||
ports:
|
|
||||||
- 3128:3128
|
|
||||||
env:
|
|
||||||
https_proxy: http://squid-proxy:3128
|
|
||||||
steps:
|
|
||||||
# Clone this repo
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# Basic checkout using git
|
|
||||||
- name: Checkout basic
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/basic
|
|
||||||
path: basic
|
|
||||||
- name: Verify basic
|
|
||||||
run: __test__/verify-basic.sh
|
|
||||||
|
|
||||||
# Basic checkout using REST API
|
|
||||||
- name: Remove basic
|
|
||||||
run: rm -rf basic
|
|
||||||
- name: Override git version
|
|
||||||
run: __test__/override-git-version.sh
|
|
||||||
- name: Basic checkout using REST API
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/basic
|
|
||||||
path: basic
|
|
||||||
- name: Verify basic
|
|
||||||
run: __test__/verify-basic.sh --archive
|
|
||||||
|
|
||||||
test-bypass-proxy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
https_proxy: http://no-such-proxy:3128
|
|
||||||
no_proxy: api.github.com,github.com
|
|
||||||
steps:
|
|
||||||
# Clone this repo
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# Basic checkout using git
|
|
||||||
- name: Checkout basic
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/basic
|
|
||||||
path: basic
|
|
||||||
- name: Verify basic
|
|
||||||
run: __test__/verify-basic.sh
|
|
||||||
- name: Remove basic
|
|
||||||
run: rm -rf basic
|
|
||||||
|
|
||||||
# Basic checkout using REST API
|
|
||||||
- name: Override git version
|
|
||||||
run: __test__/override-git-version.sh
|
|
||||||
- name: Checkout basic using REST API
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/basic
|
|
||||||
path: basic
|
|
||||||
- name: Verify basic
|
|
||||||
run: __test__/verify-basic.sh --archive
|
|
||||||
|
|
||||||
test-git-container:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: bitnami/git:latest
|
|
||||||
steps:
|
|
||||||
# Clone this repo
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
path: v3
|
|
||||||
|
|
||||||
# Basic checkout using git
|
|
||||||
- name: Checkout basic
|
|
||||||
uses: ./v3
|
|
||||||
with:
|
|
||||||
ref: test-data/v2/basic
|
|
||||||
- name: Verify basic
|
|
||||||
run: |
|
run: |
|
||||||
if [ ! -f "./basic-file.txt" ]; then
|
echo echo hello > git.cmd
|
||||||
echo "Expected basic file does not exist"
|
echo ::add-path::%CD%
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify .git folder
|
- name: Basic checkout
|
||||||
if [ ! -d "./.git" ]; then
|
uses: ./
|
||||||
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:
|
with:
|
||||||
path: v3
|
ref: test-data/v2/basic
|
||||||
|
path: basic
|
||||||
|
- name: Verify basic
|
||||||
|
shell: bash
|
||||||
|
run: __test__/verify-basic.sh container
|
||||||
|
|
||||||
|
35
.github/workflows/update-main-version.yml
vendored
35
.github/workflows/update-main-version.yml
vendored
@ -1,35 +0,0 @@
|
|||||||
name: Update Main Version
|
|
||||||
run-name: Move ${{ github.event.inputs.major_version }} to ${{ github.event.inputs.target }}
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
target:
|
|
||||||
description: The tag or reference to use
|
|
||||||
required: true
|
|
||||||
major_version:
|
|
||||||
type: choice
|
|
||||||
description: The major version to update
|
|
||||||
options:
|
|
||||||
- v4
|
|
||||||
- v3
|
|
||||||
- v2
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tag:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# Note this update workflow can also be used as a rollback tool.
|
|
||||||
# For that reason, it's best to pin `actions/checkout` to a known, stable version
|
|
||||||
# (typically, about two releases back).
|
|
||||||
- uses: actions/checkout@v4.1.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Git config
|
|
||||||
run: |
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
- name: Tag new target
|
|
||||||
run: git tag -f ${{ github.event.inputs.major_version }} ${{ github.event.inputs.target }}
|
|
||||||
- name: Push new tag
|
|
||||||
run: git push origin ${{ github.event.inputs.major_version }} --force
|
|
59
.github/workflows/update-test-ubuntu-git.yml
vendored
59
.github/workflows/update-test-ubuntu-git.yml
vendored
@ -1,59 +0,0 @@
|
|||||||
name: Publish test-ubuntu-git Container
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Use an on demand workflow trigger.
|
|
||||||
# (Forked copies of actions/checkout won't have permission to update GHCR.io/actions,
|
|
||||||
# so avoid trigger events that run automatically.)
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
publish:
|
|
||||||
description: 'Publish to ghcr.io? (main branch only)'
|
|
||||||
type: boolean
|
|
||||||
required: true
|
|
||||||
default: false
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
IMAGE_NAME: actions/test-ubuntu-git
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push-image:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# Use `docker/login-action` to log in to GHCR.io.
|
|
||||||
# Once published, the packages are scoped to the account defined here.
|
|
||||||
- name: Log in to the ghcr.io container registry
|
|
||||||
uses: docker/login-action@v3.1.0
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Format Timestamp
|
|
||||||
id: timestamp
|
|
||||||
# Use `date` with a custom format to achieve the key=value format GITHUB_OUTPUT expects.
|
|
||||||
run: date -u "+now=%Y%m%d.%H%M%S.%3NZ" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Issue Image Publish Warning
|
|
||||||
if: ${{ inputs.publish && github.ref_name != 'main' }}
|
|
||||||
run: echo "::warning::test-ubuntu-git images can only be published from the actions/checkout 'main' branch. Workflow will continue with push/publish disabled."
|
|
||||||
|
|
||||||
# Use `docker/build-push-action` to build (and optionally publish) the image.
|
|
||||||
- name: Build Docker Image (with optional Push)
|
|
||||||
uses: docker/build-push-action@v5.3.0
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: images/test-ubuntu-git.Dockerfile
|
|
||||||
# For now, attempts to push to ghcr.io must target the `main` branch.
|
|
||||||
# In the future, consider also allowing attempts from `releases/*` branches.
|
|
||||||
push: ${{ inputs.publish && github.ref_name == 'main' }}
|
|
||||||
tags: |
|
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}.${{ steps.timestamp.outputs.now }}
|
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +1,2 @@
|
|||||||
__test__/_temp
|
|
||||||
_temp/
|
|
||||||
lib/
|
lib/
|
||||||
node_modules/
|
node_modules/
|
||||||
.vscode/
|
|
@ -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.
110
CHANGELOG.md
110
CHANGELOG.md
@ -1,115 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v4.1.4
|
|
||||||
- Disable `extensions.worktreeConfig` when disabling `sparse-checkout` by @jww3 in https://github.com/actions/checkout/pull/1692
|
|
||||||
- Add dependabot config by @cory-miller in https://github.com/actions/checkout/pull/1688
|
|
||||||
- Bump the minor-actions-dependencies group with 2 updates by @dependabot in https://github.com/actions/checkout/pull/1693
|
|
||||||
- Bump word-wrap from 1.2.3 to 1.2.5 by @dependabot in https://github.com/actions/checkout/pull/1643
|
|
||||||
|
|
||||||
## v4.1.3
|
|
||||||
- Check git version before attempting to disable `sparse-checkout` by @jww3 in https://github.com/actions/checkout/pull/1656
|
|
||||||
- Add SSH user parameter by @cory-miller in https://github.com/actions/checkout/pull/1685
|
|
||||||
- Update `actions/checkout` version in `update-main-version.yml` by @jww3 in https://github.com/actions/checkout/pull/1650
|
|
||||||
|
|
||||||
## v4.1.2
|
|
||||||
- Fix: Disable sparse checkout whenever `sparse-checkout` option is not present @dscho in https://github.com/actions/checkout/pull/1598
|
|
||||||
|
|
||||||
## v4.1.1
|
|
||||||
- Correct link to GitHub Docs by @peterbe in https://github.com/actions/checkout/pull/1511
|
|
||||||
- Link to release page from what's new section by @cory-miller in https://github.com/actions/checkout/pull/1514
|
|
||||||
|
|
||||||
## v4.1.0
|
|
||||||
- [Add support for partial checkout filters](https://github.com/actions/checkout/pull/1396)
|
|
||||||
|
|
||||||
## v4.0.0
|
|
||||||
- [Support fetching without the --progress option](https://github.com/actions/checkout/pull/1067)
|
|
||||||
- [Update to node20](https://github.com/actions/checkout/pull/1436)
|
|
||||||
|
|
||||||
## v3.6.0
|
|
||||||
- [Fix: Mark test scripts with Bash'isms to be run via Bash](https://github.com/actions/checkout/pull/1377)
|
|
||||||
- [Add option to fetch tags even if fetch-depth > 0](https://github.com/actions/checkout/pull/579)
|
|
||||||
|
|
||||||
## v3.5.3
|
|
||||||
- [Fix: Checkout fail in self-hosted runners when faulty submodule are checked-in](https://github.com/actions/checkout/pull/1196)
|
|
||||||
- [Fix typos found by codespell](https://github.com/actions/checkout/pull/1287)
|
|
||||||
- [Add support for sparse checkouts](https://github.com/actions/checkout/pull/1369)
|
|
||||||
|
|
||||||
## v3.5.2
|
|
||||||
- [Fix api endpoint for GHES](https://github.com/actions/checkout/pull/1289)
|
|
||||||
|
|
||||||
## v3.5.1
|
|
||||||
- [Fix slow checkout on Windows](https://github.com/actions/checkout/pull/1246)
|
|
||||||
|
|
||||||
## v3.5.0
|
|
||||||
* [Add new public key for known_hosts](https://github.com/actions/checkout/pull/1237)
|
|
||||||
|
|
||||||
## v3.4.0
|
|
||||||
- [Upgrade codeql actions to v2](https://github.com/actions/checkout/pull/1209)
|
|
||||||
- [Upgrade dependencies](https://github.com/actions/checkout/pull/1210)
|
|
||||||
- [Upgrade @actions/io](https://github.com/actions/checkout/pull/1225)
|
|
||||||
|
|
||||||
## v3.3.0
|
|
||||||
- [Implement branch list using callbacks from exec function](https://github.com/actions/checkout/pull/1045)
|
|
||||||
- [Add in explicit reference to private checkout options](https://github.com/actions/checkout/pull/1050)
|
|
||||||
- [Fix comment typos (that got added in #770)](https://github.com/actions/checkout/pull/1057)
|
|
||||||
|
|
||||||
## v3.2.0
|
|
||||||
- [Add GitHub Action to perform release](https://github.com/actions/checkout/pull/942)
|
|
||||||
- [Fix status badge](https://github.com/actions/checkout/pull/967)
|
|
||||||
- [Replace datadog/squid with ubuntu/squid Docker image](https://github.com/actions/checkout/pull/1002)
|
|
||||||
- [Wrap pipeline commands for submoduleForeach in quotes](https://github.com/actions/checkout/pull/964)
|
|
||||||
- [Update @actions/io to 1.1.2](https://github.com/actions/checkout/pull/1029)
|
|
||||||
- [Upgrading version to 3.2.0](https://github.com/actions/checkout/pull/1039)
|
|
||||||
|
|
||||||
## v3.1.0
|
|
||||||
- [Use @actions/core `saveState` and `getState`](https://github.com/actions/checkout/pull/939)
|
|
||||||
- [Add `github-server-url` input](https://github.com/actions/checkout/pull/922)
|
|
||||||
|
|
||||||
## v3.0.2
|
|
||||||
- [Add input `set-safe-directory`](https://github.com/actions/checkout/pull/770)
|
|
||||||
|
|
||||||
## v3.0.1
|
|
||||||
- [Fixed an issue where checkout failed to run in container jobs due to the new git setting `safe.directory`](https://github.com/actions/checkout/pull/762)
|
|
||||||
- [Bumped various npm package versions](https://github.com/actions/checkout/pull/744)
|
|
||||||
|
|
||||||
## v3.0.0
|
|
||||||
|
|
||||||
- [Update to node 16](https://github.com/actions/checkout/pull/689)
|
|
||||||
|
|
||||||
## 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 +0,0 @@
|
|||||||
* @actions/actions-launch
|
|
@ -1,28 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
## Submitting a pull request
|
|
||||||
|
|
||||||
1. Fork and clone the repository
|
|
||||||
1. Configure and install the dependencies: `npm install`
|
|
||||||
1. Create a new branch: `git checkout -b my-branch-name`
|
|
||||||
1. Make your change, add tests, and make sure the tests still pass: `npm run test`
|
|
||||||
1. Make sure your code is correctly formatted: `npm run format`
|
|
||||||
1. Update `dist/index.js` using `npm run build`. This creates a single javascript file that is used as an entrypoint for the action
|
|
||||||
1. Push to your fork and submit a pull request
|
|
||||||
1. Pat yourself on the back and wait for your pull request to be reviewed and merged
|
|
||||||
|
|
||||||
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
|
||||||
|
|
||||||
- Write tests.
|
|
||||||
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
|
||||||
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
|
||||||
- [GitHub Help](https://help.github.com)
|
|
||||||
- [Writing good commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
|
|
||||||
|
|
||||||
Thanks! :heart: :heart: :heart:
|
|
||||||
|
|
||||||
GitHub Actions Team :octocat:
|
|
271
README.md
271
README.md
@ -1,76 +1,53 @@
|
|||||||
[![Build and Test](https://github.com/actions/checkout/actions/workflows/test.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/test.yml)
|
<p align="center">
|
||||||
|
<a href="https://github.com/actions/checkout"><img alt="GitHub Actions status" src="https://github.com/actions/checkout/workflows/test-local/badge.svg"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
# Checkout V4
|
# Checkout V2 beta
|
||||||
|
|
||||||
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://docs.github.com/actions/using-workflows/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events.
|
By default, the repository that triggered the workflow is checked-out, for the ref/SHA that triggered the event.
|
||||||
|
|
||||||
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.
|
Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events.
|
||||||
|
|
||||||
When Git 2.18 or higher is not in your PATH, falls back to the REST API to download the files.
|
|
||||||
|
|
||||||
# What's new
|
# What's new
|
||||||
|
|
||||||
Please refer to the [release page](https://github.com/actions/checkout/releases/latest) for the latest release notes.
|
- Improved fetch performance
|
||||||
|
- The default behavior now fetches only the SHA being checked-out
|
||||||
|
- Script authenticated git commands
|
||||||
|
- Persists `with.token` in the local git config
|
||||||
|
- Enables your scripts to run authenticated git commands
|
||||||
|
- Post-job cleanup removes the token
|
||||||
|
- Coming soon: Opt out by setting `with.persist-credentials` to `false`
|
||||||
|
- Creates a local branch
|
||||||
|
- No longer detached HEAD when checking out a branch
|
||||||
|
- A local branch is created with the corresponding upstream branch set
|
||||||
|
- Improved layout
|
||||||
|
- `with.path` is always relative to `github.workspace`
|
||||||
|
- Aligns better with container actions, where `github.workspace` gets mapped in
|
||||||
|
- Removed input `submodules`
|
||||||
|
|
||||||
|
Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous versions.
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
<!-- start usage -->
|
<!-- start usage -->
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2-beta
|
||||||
with:
|
with:
|
||||||
# Repository name with owner. For example, actions/checkout
|
# Repository name with owner. For example, actions/checkout
|
||||||
# Default: ${{ github.repository }}
|
# Default: ${{ github.repository }}
|
||||||
repository: ''
|
repository: ''
|
||||||
|
|
||||||
# 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
|
# Access token for clone repository
|
||||||
# with the local git config, which enables your scripts to run authenticated git
|
|
||||||
# commands. The post-job step removes the PAT.
|
|
||||||
#
|
|
||||||
# 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 }}
|
||||||
token: ''
|
token: ''
|
||||||
|
|
||||||
# SSH key used to fetch the repository. The SSH key is configured with the local
|
|
||||||
# git config, which enables your scripts to run authenticated git commands. The
|
|
||||||
# post-job step removes the SSH key.
|
|
||||||
#
|
|
||||||
# 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)
|
|
||||||
ssh-key: ''
|
|
||||||
|
|
||||||
# Known hosts in addition to the user and global host key database. The public 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-known-hosts: ''
|
|
||||||
|
|
||||||
# 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
|
|
||||||
ssh-strict: ''
|
|
||||||
|
|
||||||
# The user to use when connecting to the remote SSH host. By default 'git' is
|
|
||||||
# used.
|
|
||||||
# Default: git
|
|
||||||
ssh-user: ''
|
|
||||||
|
|
||||||
# Whether to configure the token or SSH key with the local git config
|
|
||||||
# Default: true
|
|
||||||
persist-credentials: ''
|
|
||||||
|
|
||||||
# Relative path under $GITHUB_WORKSPACE to place the repository
|
# Relative path under $GITHUB_WORKSPACE to place the repository
|
||||||
path: ''
|
path: ''
|
||||||
|
|
||||||
@ -78,213 +55,41 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
# Default: true
|
# Default: true
|
||||||
clean: ''
|
clean: ''
|
||||||
|
|
||||||
# Partially clone against a given filter. Overrides sparse-checkout if set.
|
# Number of commits to fetch. 0 indicates all history.
|
||||||
# Default: null
|
|
||||||
filter: ''
|
|
||||||
|
|
||||||
# Do a sparse checkout on given patterns. Each pattern should be separated with
|
|
||||||
# new lines.
|
|
||||||
# Default: null
|
|
||||||
sparse-checkout: ''
|
|
||||||
|
|
||||||
# Specifies whether to use cone-mode when doing a sparse checkout.
|
|
||||||
# Default: true
|
|
||||||
sparse-checkout-cone-mode: ''
|
|
||||||
|
|
||||||
# Number of commits to fetch. 0 indicates all history for all branches and tags.
|
|
||||||
# Default: 1
|
# Default: 1
|
||||||
fetch-depth: ''
|
fetch-depth: ''
|
||||||
|
|
||||||
# Whether to fetch tags, even if fetch-depth > 0.
|
|
||||||
# Default: false
|
|
||||||
fetch-tags: ''
|
|
||||||
|
|
||||||
# Whether to show progress status output when fetching.
|
|
||||||
# Default: true
|
|
||||||
show-progress: ''
|
|
||||||
|
|
||||||
# Whether to download Git-LFS files
|
# Whether to download Git-LFS files
|
||||||
# Default: false
|
# Default: false
|
||||||
lfs: ''
|
lfs: ''
|
||||||
|
|
||||||
# Whether to checkout submodules: `true` to checkout submodules or `recursive` to
|
|
||||||
# recursively checkout submodules.
|
|
||||||
#
|
|
||||||
# When the `ssh-key` input is not provided, SSH URLs beginning with
|
|
||||||
# `git@github.com:` are converted to HTTPS.
|
|
||||||
#
|
|
||||||
# Default: false
|
|
||||||
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: ''
|
|
||||||
|
|
||||||
# The base URL for the GitHub instance that you are trying to clone from, will use
|
|
||||||
# environment defaults to fetch from the same instance that the workflow is
|
|
||||||
# running from unless specified. Example URLs are https://github.com or
|
|
||||||
# https://my-ghes-server.example.com
|
|
||||||
github-server-url: ''
|
|
||||||
```
|
```
|
||||||
<!-- end usage -->
|
<!-- end usage -->
|
||||||
|
|
||||||
# Scenarios
|
|
||||||
|
|
||||||
- [Fetch only the root files](#Fetch-only-the-root-files)
|
|
||||||
- [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder)
|
|
||||||
- [Fetch only a single file](#Fetch-only-a-single-file)
|
|
||||||
- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches)
|
|
||||||
- [Checkout a different branch](#Checkout-a-different-branch)
|
|
||||||
- [Checkout HEAD^](#Checkout-HEAD)
|
|
||||||
- [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side)
|
|
||||||
- [Checkout multiple repos (nested)](#Checkout-multiple-repos-nested)
|
|
||||||
- [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
|
||||||
- [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit)
|
|
||||||
- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
|
|
||||||
- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token)
|
|
||||||
|
|
||||||
## Fetch only the root files
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
sparse-checkout: .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fetch only the root files and `.github` and `src` folder
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
sparse-checkout: |
|
|
||||||
.github
|
|
||||||
src
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fetch only a single file
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
sparse-checkout: |
|
|
||||||
README.md
|
|
||||||
sparse-checkout-cone-mode: false
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fetch all history for all tags and branches
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
```
|
|
||||||
|
|
||||||
## Checkout a different branch
|
## Checkout a different branch
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2-beta
|
||||||
with:
|
with:
|
||||||
ref: my-branch
|
ref: some-branch
|
||||||
```
|
```
|
||||||
|
|
||||||
## Checkout HEAD^
|
## Checkout a different, private repository
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2-beta
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
repository: myAccount/myRepository
|
||||||
- run: git checkout HEAD^
|
ref: refs/heads/master
|
||||||
|
token: ${{ secrets.GitHub_PAT }} # `GitHub_PAT` is a secret that contains your PAT
|
||||||
```
|
```
|
||||||
|
> - `${{ github.token }}` is scoped to the current repository, so if you want to checkout another repository that is private you will need to provide your own [PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line).
|
||||||
|
|
||||||
## Checkout multiple repos (side by side)
|
## Checkout the HEAD commit of a PR, rather than the merge commit
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Checkout
|
- uses: actions/checkout@v2-beta
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
with:
|
||||||
path: main
|
ref: ${{ github.event.after }}
|
||||||
|
|
||||||
- name: Checkout tools repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: my-org/my-tools
|
|
||||||
path: my-tools
|
|
||||||
```
|
|
||||||
> - If your secondary repository is private you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
|
||||||
|
|
||||||
## Checkout multiple repos (nested)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Checkout tools repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: my-org/my-tools
|
|
||||||
path: my-tools
|
|
||||||
```
|
|
||||||
> - If your secondary repository is private you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
|
||||||
|
|
||||||
## Checkout multiple repos (private)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
path: main
|
|
||||||
|
|
||||||
- name: Checkout private tools
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: my-org/my-private-tools
|
|
||||||
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
|
|
||||||
path: my-tools
|
|
||||||
```
|
|
||||||
|
|
||||||
> - `${{ github.token }}` is scoped to the current repository, so if you want to checkout a different repository that is private you will need to provide your own [PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line).
|
|
||||||
|
|
||||||
|
|
||||||
## Checkout pull request HEAD commit instead of merge commit
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Checkout pull request on closed event
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
types: [opened, synchronize, closed]
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
```
|
|
||||||
|
|
||||||
## Push a commit using the built-in token
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- run: |
|
|
||||||
date > generated.txt
|
|
||||||
# Note: the following account information will not work on GHES
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add .
|
|
||||||
git commit -m "generated"
|
|
||||||
git push
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
@ -1,856 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
import * as gitAuthHelper from '../lib/git-auth-helper'
|
|
||||||
import * as io from '@actions/io'
|
|
||||||
import * as os from 'os'
|
|
||||||
import * as path from 'path'
|
|
||||||
import * as stateHelper from '../lib/state-helper'
|
|
||||||
import {IGitCommandManager} from '../lib/git-command-manager'
|
|
||||||
import {IGitSourceSettings} from '../lib/git-source-settings'
|
|
||||||
|
|
||||||
const isWindows = process.platform === 'win32'
|
|
||||||
const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
|
|
||||||
const originalRunnerTemp = process.env['RUNNER_TEMP']
|
|
||||||
const originalHome = process.env['HOME']
|
|
||||||
let workspace: string
|
|
||||||
let localGitConfigPath: string
|
|
||||||
let globalGitConfigPath: string
|
|
||||||
let runnerTemp: string
|
|
||||||
let tempHomedir: string
|
|
||||||
let git: IGitCommandManager & {env: {[key: string]: string}}
|
|
||||||
let settings: IGitSourceSettings
|
|
||||||
let sshPath: string
|
|
||||||
let githubServerUrl: string
|
|
||||||
|
|
||||||
describe('git-auth-helper tests', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
// SSH
|
|
||||||
sshPath = await io.which('ssh')
|
|
||||||
|
|
||||||
// Clear test workspace
|
|
||||||
await io.rmRF(testWorkspace)
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock setSecret
|
|
||||||
jest.spyOn(core, 'setSecret').mockImplementation((secret: string) => {})
|
|
||||||
|
|
||||||
// Mock error/warning/info/debug
|
|
||||||
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
|
||||||
|
|
||||||
// Mock state helper
|
|
||||||
jest.spyOn(stateHelper, 'setSshKeyPath').mockImplementation(jest.fn())
|
|
||||||
jest
|
|
||||||
.spyOn(stateHelper, 'setSshKnownHostsPath')
|
|
||||||
.mockImplementation(jest.fn())
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
// Unregister mocks
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
|
|
||||||
// Restore HOME
|
|
||||||
if (originalHome) {
|
|
||||||
process.env['HOME'] = originalHome
|
|
||||||
} else {
|
|
||||||
delete process.env['HOME']
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
// Restore RUNNER_TEMP
|
|
||||||
delete process.env['RUNNER_TEMP']
|
|
||||||
if (originalRunnerTemp) {
|
|
||||||
process.env['RUNNER_TEMP'] = originalRunnerTemp
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function testAuthHeader(
|
|
||||||
testName: string,
|
|
||||||
serverUrl: string | undefined = undefined
|
|
||||||
) {
|
|
||||||
// Arrange
|
|
||||||
let expectedServerUrl = 'https://github.com'
|
|
||||||
if (serverUrl) {
|
|
||||||
githubServerUrl = serverUrl
|
|
||||||
expectedServerUrl = githubServerUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
await setup(testName)
|
|
||||||
expect(settings.authToken).toBeTruthy() // sanity check
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
|
|
||||||
// Assert config
|
|
||||||
const configContent = (
|
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
|
||||||
).toString()
|
|
||||||
const basicCredential = Buffer.from(
|
|
||||||
`x-access-token:${settings.authToken}`,
|
|
||||||
'utf8'
|
|
||||||
).toString('base64')
|
|
||||||
expect(
|
|
||||||
configContent.indexOf(
|
|
||||||
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
|
||||||
)
|
|
||||||
).toBeGreaterThanOrEqual(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const configureAuth_configuresAuthHeader =
|
|
||||||
'configureAuth configures auth header'
|
|
||||||
it(configureAuth_configuresAuthHeader, async () => {
|
|
||||||
await testAuthHeader(configureAuth_configuresAuthHeader)
|
|
||||||
})
|
|
||||||
|
|
||||||
const configureAuth_AcceptsGitHubServerUrl =
|
|
||||||
'inject https://my-ghes-server.com as github server url'
|
|
||||||
it(configureAuth_AcceptsGitHubServerUrl, async () => {
|
|
||||||
await testAuthHeader(
|
|
||||||
configureAuth_AcceptsGitHubServerUrl,
|
|
||||||
'https://my-ghes-server.com'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const configureAuth_AcceptsGitHubServerUrlSetToGHEC =
|
|
||||||
'inject https://github.com as github server url'
|
|
||||||
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
|
|
||||||
await testAuthHeader(
|
|
||||||
configureAuth_AcceptsGitHubServerUrl,
|
|
||||||
'https://github.com'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse =
|
|
||||||
'configureAuth configures auth header even when persist credentials false'
|
|
||||||
it(
|
|
||||||
configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse,
|
|
||||||
async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(
|
|
||||||
configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse
|
|
||||||
)
|
|
||||||
expect(settings.authToken).toBeTruthy() // sanity check
|
|
||||||
settings.persistCredentials = false
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
|
|
||||||
// Assert config
|
|
||||||
const configContent = (
|
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
|
||||||
).toString()
|
|
||||||
expect(
|
|
||||||
configContent.indexOf(
|
|
||||||
`http.https://github.com/.extraheader AUTHORIZATION`
|
|
||||||
)
|
|
||||||
).toBeGreaterThanOrEqual(0)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const configureAuth_copiesUserKnownHosts =
|
|
||||||
'configureAuth copies user known hosts'
|
|
||||||
it(configureAuth_copiesUserKnownHosts, async () => {
|
|
||||||
if (!sshPath) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Skipped test "${configureAuth_copiesUserKnownHosts}". Executable 'ssh' not found in the PATH.\n`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arange
|
|
||||||
await setup(configureAuth_copiesUserKnownHosts)
|
|
||||||
expect(settings.sshKey).toBeTruthy() // sanity check
|
|
||||||
|
|
||||||
// Mock fs.promises.readFile
|
|
||||||
const realReadFile = fs.promises.readFile
|
|
||||||
jest
|
|
||||||
.spyOn(fs.promises, 'readFile')
|
|
||||||
.mockImplementation(async (file: any, options: any): Promise<Buffer> => {
|
|
||||||
const userKnownHostsPath = path.join(
|
|
||||||
os.homedir(),
|
|
||||||
'.ssh',
|
|
||||||
'known_hosts'
|
|
||||||
)
|
|
||||||
if (file === userKnownHostsPath) {
|
|
||||||
return Buffer.from('some-domain.com ssh-rsa ABCDEF')
|
|
||||||
}
|
|
||||||
|
|
||||||
return await realReadFile(file, options)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
|
|
||||||
// Assert known hosts
|
|
||||||
const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
|
|
||||||
const actualSshKnownHostsContent = (
|
|
||||||
await fs.promises.readFile(actualSshKnownHostsPath)
|
|
||||||
).toString()
|
|
||||||
expect(actualSshKnownHostsContent).toMatch(
|
|
||||||
/some-domain\.com ssh-rsa ABCDEF/
|
|
||||||
)
|
|
||||||
expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
|
|
||||||
})
|
|
||||||
|
|
||||||
const configureAuth_registersBasicCredentialAsSecret =
|
|
||||||
'configureAuth registers basic credential as secret'
|
|
||||||
it(configureAuth_registersBasicCredentialAsSecret, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(configureAuth_registersBasicCredentialAsSecret)
|
|
||||||
expect(settings.authToken).toBeTruthy() // sanity check
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
|
|
||||||
// Assert secret
|
|
||||||
const setSecretSpy = core.setSecret as jest.Mock<any, any>
|
|
||||||
expect(setSecretSpy).toHaveBeenCalledTimes(1)
|
|
||||||
const expectedSecret = Buffer.from(
|
|
||||||
`x-access-token:${settings.authToken}`,
|
|
||||||
'utf8'
|
|
||||||
).toString('base64')
|
|
||||||
expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret)
|
|
||||||
})
|
|
||||||
|
|
||||||
const setsSshCommandEnvVarWhenPersistCredentialsFalse =
|
|
||||||
'sets SSH command env var when persist-credentials false'
|
|
||||||
it(setsSshCommandEnvVarWhenPersistCredentialsFalse, async () => {
|
|
||||||
if (!sshPath) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Skipped test "${setsSshCommandEnvVarWhenPersistCredentialsFalse}". Executable 'ssh' not found in the PATH.\n`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrange
|
|
||||||
await setup(setsSshCommandEnvVarWhenPersistCredentialsFalse)
|
|
||||||
settings.persistCredentials = false
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
|
|
||||||
// Assert git env var
|
|
||||||
const actualKeyPath = await getActualSshKeyPath()
|
|
||||||
const actualKnownHostsPath = await getActualSshKnownHostsPath()
|
|
||||||
const expectedSshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
|
|
||||||
actualKeyPath
|
|
||||||
)}" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
|
|
||||||
actualKnownHostsPath
|
|
||||||
)}"`
|
|
||||||
expect(git.setEnvironmentVariable).toHaveBeenCalledWith(
|
|
||||||
'GIT_SSH_COMMAND',
|
|
||||||
expectedSshCommand
|
|
||||||
)
|
|
||||||
|
|
||||||
// Asserty git config
|
|
||||||
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
|
|
||||||
.toString()
|
|
||||||
.split('\n')
|
|
||||||
.filter(x => x)
|
|
||||||
expect(gitConfigLines).toHaveLength(1)
|
|
||||||
expect(gitConfigLines[0]).toMatch(/^http\./)
|
|
||||||
})
|
|
||||||
|
|
||||||
const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
|
|
||||||
'sets SSH command when persist-credentials true'
|
|
||||||
it(configureAuth_setsSshCommandWhenPersistCredentialsTrue, async () => {
|
|
||||||
if (!sshPath) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Skipped test "${configureAuth_setsSshCommandWhenPersistCredentialsTrue}". Executable 'ssh' not found in the PATH.\n`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrange
|
|
||||||
await setup(configureAuth_setsSshCommandWhenPersistCredentialsTrue)
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
|
|
||||||
// Assert git env var
|
|
||||||
const actualKeyPath = await getActualSshKeyPath()
|
|
||||||
const actualKnownHostsPath = await getActualSshKnownHostsPath()
|
|
||||||
const expectedSshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
|
|
||||||
actualKeyPath
|
|
||||||
)}" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
|
|
||||||
actualKnownHostsPath
|
|
||||||
)}"`
|
|
||||||
expect(git.setEnvironmentVariable).toHaveBeenCalledWith(
|
|
||||||
'GIT_SSH_COMMAND',
|
|
||||||
expectedSshCommand
|
|
||||||
)
|
|
||||||
|
|
||||||
// Asserty git config
|
|
||||||
expect(git.config).toHaveBeenCalledWith(
|
|
||||||
'core.sshCommand',
|
|
||||||
expectedSshCommand
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const configureAuth_writesExplicitKnownHosts = 'writes explicit known hosts'
|
|
||||||
it(configureAuth_writesExplicitKnownHosts, async () => {
|
|
||||||
if (!sshPath) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Skipped test "${configureAuth_writesExplicitKnownHosts}". Executable 'ssh' not found in the PATH.\n`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrange
|
|
||||||
await setup(configureAuth_writesExplicitKnownHosts)
|
|
||||||
expect(settings.sshKey).toBeTruthy() // sanity check
|
|
||||||
settings.sshKnownHosts = 'my-custom-host.com ssh-rsa ABC123'
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
|
|
||||||
// Assert known hosts
|
|
||||||
const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
|
|
||||||
const actualSshKnownHostsContent = (
|
|
||||||
await fs.promises.readFile(actualSshKnownHostsPath)
|
|
||||||
).toString()
|
|
||||||
expect(actualSshKnownHostsContent).toMatch(
|
|
||||||
/my-custom-host\.com ssh-rsa ABC123/
|
|
||||||
)
|
|
||||||
expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
|
|
||||||
})
|
|
||||||
|
|
||||||
const configureAuth_writesSshKeyAndImplicitKnownHosts =
|
|
||||||
'writes SSH key and implicit known hosts'
|
|
||||||
it(configureAuth_writesSshKeyAndImplicitKnownHosts, async () => {
|
|
||||||
if (!sshPath) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Skipped test "${configureAuth_writesSshKeyAndImplicitKnownHosts}". Executable 'ssh' not found in the PATH.\n`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrange
|
|
||||||
await setup(configureAuth_writesSshKeyAndImplicitKnownHosts)
|
|
||||||
expect(settings.sshKey).toBeTruthy() // sanity check
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
|
|
||||||
// Assert SSH key
|
|
||||||
const actualSshKeyPath = await getActualSshKeyPath()
|
|
||||||
expect(actualSshKeyPath).toBeTruthy()
|
|
||||||
const actualSshKeyContent = (
|
|
||||||
await fs.promises.readFile(actualSshKeyPath)
|
|
||||||
).toString()
|
|
||||||
expect(actualSshKeyContent).toBe(settings.sshKey + '\n')
|
|
||||||
if (!isWindows) {
|
|
||||||
// Assert read/write for user, not group or others.
|
|
||||||
// Otherwise SSH client will error.
|
|
||||||
expect((await fs.promises.stat(actualSshKeyPath)).mode & 0o777).toBe(
|
|
||||||
0o600
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert known hosts
|
|
||||||
const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
|
|
||||||
const actualSshKnownHostsContent = (
|
|
||||||
await fs.promises.readFile(actualSshKnownHostsPath)
|
|
||||||
).toString()
|
|
||||||
expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
|
|
||||||
})
|
|
||||||
|
|
||||||
const configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet =
|
|
||||||
'configureGlobalAuth configures URL insteadOf when SSH key not set'
|
|
||||||
it(configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet)
|
|
||||||
settings.sshKey = ''
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
await authHelper.configureGlobalAuth()
|
|
||||||
|
|
||||||
// Assert temporary global config
|
|
||||||
expect(git.env['HOME']).toBeTruthy()
|
|
||||||
const configContent = (
|
|
||||||
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
|
|
||||||
).toString()
|
|
||||||
expect(
|
|
||||||
configContent.indexOf(`url.https://github.com/.insteadOf git@github.com`)
|
|
||||||
).toBeGreaterThanOrEqual(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
const configureGlobalAuth_copiesGlobalGitConfig =
|
|
||||||
'configureGlobalAuth copies global git config'
|
|
||||||
it(configureGlobalAuth_copiesGlobalGitConfig, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(configureGlobalAuth_copiesGlobalGitConfig)
|
|
||||||
await fs.promises.writeFile(globalGitConfigPath, 'value-from-global-config')
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
await authHelper.configureGlobalAuth()
|
|
||||||
|
|
||||||
// Assert original global config not altered
|
|
||||||
let configContent = (
|
|
||||||
await fs.promises.readFile(globalGitConfigPath)
|
|
||||||
).toString()
|
|
||||||
expect(configContent).toBe('value-from-global-config')
|
|
||||||
|
|
||||||
// Assert temporary global config
|
|
||||||
expect(git.env['HOME']).toBeTruthy()
|
|
||||||
const basicCredential = Buffer.from(
|
|
||||||
`x-access-token:${settings.authToken}`,
|
|
||||||
'utf8'
|
|
||||||
).toString('base64')
|
|
||||||
configContent = (
|
|
||||||
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
|
|
||||||
).toString()
|
|
||||||
expect(
|
|
||||||
configContent.indexOf('value-from-global-config')
|
|
||||||
).toBeGreaterThanOrEqual(0)
|
|
||||||
expect(
|
|
||||||
configContent.indexOf(
|
|
||||||
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
|
||||||
)
|
|
||||||
).toBeGreaterThanOrEqual(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
const configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist =
|
|
||||||
'configureGlobalAuth creates new git config when global does not exist'
|
|
||||||
it(
|
|
||||||
configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist,
|
|
||||||
async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(
|
|
||||||
configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist
|
|
||||||
)
|
|
||||||
await io.rmRF(globalGitConfigPath)
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
await authHelper.configureGlobalAuth()
|
|
||||||
|
|
||||||
// Assert original global config not recreated
|
|
||||||
try {
|
|
||||||
await fs.promises.stat(globalGitConfigPath)
|
|
||||||
throw new Error(
|
|
||||||
`Did not expect file to exist: '${globalGitConfigPath}'`
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
if ((err as any)?.code !== 'ENOENT') {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert temporary global config
|
|
||||||
expect(git.env['HOME']).toBeTruthy()
|
|
||||||
const basicCredential = Buffer.from(
|
|
||||||
`x-access-token:${settings.authToken}`,
|
|
||||||
'utf8'
|
|
||||||
).toString('base64')
|
|
||||||
const configContent = (
|
|
||||||
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
|
|
||||||
).toString()
|
|
||||||
expect(
|
|
||||||
configContent.indexOf(
|
|
||||||
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
|
|
||||||
)
|
|
||||||
).toBeGreaterThanOrEqual(0)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet =
|
|
||||||
'configureSubmoduleAuth configures submodules when persist credentials false and SSH key not set'
|
|
||||||
it(
|
|
||||||
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet,
|
|
||||||
async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(
|
|
||||||
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet
|
|
||||||
)
|
|
||||||
settings.persistCredentials = false
|
|
||||||
settings.sshKey = ''
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
|
||||||
mockSubmoduleForeach.mockClear() // reset calls
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureSubmoduleAuth()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockSubmoduleForeach).toBeCalledTimes(1)
|
|
||||||
expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch(
|
|
||||||
/unset-all.*insteadOf/
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet =
|
|
||||||
'configureSubmoduleAuth configures submodules when persist credentials false and SSH key set'
|
|
||||||
it(
|
|
||||||
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet,
|
|
||||||
async () => {
|
|
||||||
if (!sshPath) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrange
|
|
||||||
await setup(
|
|
||||||
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet
|
|
||||||
)
|
|
||||||
settings.persistCredentials = false
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
|
||||||
mockSubmoduleForeach.mockClear() // reset calls
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureSubmoduleAuth()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(1)
|
|
||||||
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
|
||||||
/unset-all.*insteadOf/
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet =
|
|
||||||
'configureSubmoduleAuth configures submodules when persist credentials true and SSH key not set'
|
|
||||||
it(
|
|
||||||
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet,
|
|
||||||
async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(
|
|
||||||
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet
|
|
||||||
)
|
|
||||||
settings.sshKey = ''
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
|
||||||
mockSubmoduleForeach.mockClear() // reset calls
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureSubmoduleAuth()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
|
|
||||||
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
|
||||||
/unset-all.*insteadOf/
|
|
||||||
)
|
|
||||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
|
|
||||||
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
|
|
||||||
/url.*insteadOf.*git@github.com:/
|
|
||||||
)
|
|
||||||
expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
|
|
||||||
/url.*insteadOf.*org-123456@github.com:/
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet =
|
|
||||||
'configureSubmoduleAuth configures submodules when persist credentials true and SSH key set'
|
|
||||||
it(
|
|
||||||
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet,
|
|
||||||
async () => {
|
|
||||||
if (!sshPath) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrange
|
|
||||||
await setup(
|
|
||||||
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet
|
|
||||||
)
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
|
|
||||||
mockSubmoduleForeach.mockClear() // reset calls
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.configureSubmoduleAuth()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
|
|
||||||
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
|
|
||||||
/unset-all.*insteadOf/
|
|
||||||
)
|
|
||||||
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
|
|
||||||
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const removeAuth_removesSshCommand = 'removeAuth removes SSH command'
|
|
||||||
it(removeAuth_removesSshCommand, async () => {
|
|
||||||
if (!sshPath) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Skipped test "${removeAuth_removesSshCommand}". Executable 'ssh' not found in the PATH.\n`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrange
|
|
||||||
await setup(removeAuth_removesSshCommand)
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
let gitConfigContent = (
|
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
|
||||||
).toString()
|
|
||||||
expect(gitConfigContent.indexOf('core.sshCommand')).toBeGreaterThanOrEqual(
|
|
||||||
0
|
|
||||||
) // sanity check
|
|
||||||
const actualKeyPath = await getActualSshKeyPath()
|
|
||||||
expect(actualKeyPath).toBeTruthy()
|
|
||||||
await fs.promises.stat(actualKeyPath)
|
|
||||||
const actualKnownHostsPath = await getActualSshKnownHostsPath()
|
|
||||||
expect(actualKnownHostsPath).toBeTruthy()
|
|
||||||
await fs.promises.stat(actualKnownHostsPath)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.removeAuth()
|
|
||||||
|
|
||||||
// Assert git config
|
|
||||||
gitConfigContent = (
|
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
|
||||||
).toString()
|
|
||||||
expect(gitConfigContent.indexOf('core.sshCommand')).toBeLessThan(0)
|
|
||||||
|
|
||||||
// Assert SSH key file
|
|
||||||
try {
|
|
||||||
await fs.promises.stat(actualKeyPath)
|
|
||||||
throw new Error('SSH key should have been deleted')
|
|
||||||
} catch (err) {
|
|
||||||
if ((err as any)?.code !== 'ENOENT') {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert known hosts file
|
|
||||||
try {
|
|
||||||
await fs.promises.stat(actualKnownHostsPath)
|
|
||||||
throw new Error('SSH known hosts should have been deleted')
|
|
||||||
} catch (err) {
|
|
||||||
if ((err as any)?.code !== 'ENOENT') {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const removeAuth_removesToken = 'removeAuth removes token'
|
|
||||||
it(removeAuth_removesToken, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(removeAuth_removesToken)
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
let gitConfigContent = (
|
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
|
||||||
).toString()
|
|
||||||
expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.removeAuth()
|
|
||||||
|
|
||||||
// Assert git config
|
|
||||||
gitConfigContent = (
|
|
||||||
await fs.promises.readFile(localGitConfigPath)
|
|
||||||
).toString()
|
|
||||||
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
const removeGlobalConfig_removesOverride =
|
|
||||||
'removeGlobalConfig removes override'
|
|
||||||
it(removeGlobalConfig_removesOverride, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(removeGlobalConfig_removesOverride)
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
await authHelper.configureGlobalAuth()
|
|
||||||
const homeOverride = git.env['HOME'] // Sanity check
|
|
||||||
expect(homeOverride).toBeTruthy()
|
|
||||||
await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig'))
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await authHelper.removeGlobalConfig()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(git.env['HOME']).toBeUndefined()
|
|
||||||
try {
|
|
||||||
await fs.promises.stat(homeOverride)
|
|
||||||
throw new Error(`Should have been deleted '${homeOverride}'`)
|
|
||||||
} catch (err) {
|
|
||||||
if ((err as any)?.code !== 'ENOENT') {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
async function setup(testName: string): Promise<void> {
|
|
||||||
testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-')
|
|
||||||
|
|
||||||
// Directories
|
|
||||||
workspace = path.join(testWorkspace, testName, 'workspace')
|
|
||||||
runnerTemp = path.join(testWorkspace, testName, 'runner-temp')
|
|
||||||
tempHomedir = path.join(testWorkspace, testName, 'home-dir')
|
|
||||||
await fs.promises.mkdir(workspace, {recursive: true})
|
|
||||||
await fs.promises.mkdir(runnerTemp, {recursive: true})
|
|
||||||
await fs.promises.mkdir(tempHomedir, {recursive: true})
|
|
||||||
process.env['RUNNER_TEMP'] = runnerTemp
|
|
||||||
process.env['HOME'] = tempHomedir
|
|
||||||
|
|
||||||
// Create git config
|
|
||||||
globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
|
|
||||||
await fs.promises.writeFile(globalGitConfigPath, '')
|
|
||||||
localGitConfigPath = path.join(workspace, '.git', 'config')
|
|
||||||
await fs.promises.mkdir(path.dirname(localGitConfigPath), {recursive: true})
|
|
||||||
await fs.promises.writeFile(localGitConfigPath, '')
|
|
||||||
|
|
||||||
git = {
|
|
||||||
branchDelete: jest.fn(),
|
|
||||||
branchExists: jest.fn(),
|
|
||||||
branchList: jest.fn(),
|
|
||||||
disableSparseCheckout: jest.fn(),
|
|
||||||
sparseCheckout: jest.fn(),
|
|
||||||
sparseCheckoutNonConeMode: jest.fn(),
|
|
||||||
checkout: jest.fn(),
|
|
||||||
checkoutDetach: jest.fn(),
|
|
||||||
config: jest.fn(
|
|
||||||
async (key: string, value: string, globalConfig?: boolean) => {
|
|
||||||
const configPath = globalConfig
|
|
||||||
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
|
||||||
: localGitConfigPath
|
|
||||||
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
configExists: jest.fn(
|
|
||||||
async (key: string, globalConfig?: boolean): Promise<boolean> => {
|
|
||||||
const configPath = globalConfig
|
|
||||||
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
|
||||||
: localGitConfigPath
|
|
||||||
const content = await fs.promises.readFile(configPath)
|
|
||||||
const lines = content
|
|
||||||
.toString()
|
|
||||||
.split('\n')
|
|
||||||
.filter(x => x)
|
|
||||||
return lines.some(x => x.startsWith(key))
|
|
||||||
}
|
|
||||||
),
|
|
||||||
env: {},
|
|
||||||
fetch: jest.fn(),
|
|
||||||
getDefaultBranch: jest.fn(),
|
|
||||||
getWorkingDirectory: jest.fn(() => workspace),
|
|
||||||
init: jest.fn(),
|
|
||||||
isDetached: jest.fn(),
|
|
||||||
lfsFetch: jest.fn(),
|
|
||||||
lfsInstall: jest.fn(),
|
|
||||||
log1: jest.fn(),
|
|
||||||
remoteAdd: jest.fn(),
|
|
||||||
removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
|
|
||||||
revParse: jest.fn(),
|
|
||||||
setEnvironmentVariable: jest.fn((name: string, value: string) => {
|
|
||||||
git.env[name] = value
|
|
||||||
}),
|
|
||||||
shaExists: jest.fn(),
|
|
||||||
submoduleForeach: jest.fn(async () => {
|
|
||||||
return ''
|
|
||||||
}),
|
|
||||||
submoduleSync: jest.fn(),
|
|
||||||
submoduleStatus: jest.fn(async () => {
|
|
||||||
return true
|
|
||||||
}),
|
|
||||||
submoduleUpdate: jest.fn(),
|
|
||||||
tagExists: jest.fn(),
|
|
||||||
tryClean: jest.fn(),
|
|
||||||
tryConfigUnset: jest.fn(
|
|
||||||
async (key: string, globalConfig?: boolean): Promise<boolean> => {
|
|
||||||
const configPath = globalConfig
|
|
||||||
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
|
|
||||||
: localGitConfigPath
|
|
||||||
let content = await fs.promises.readFile(configPath)
|
|
||||||
let lines = content
|
|
||||||
.toString()
|
|
||||||
.split('\n')
|
|
||||||
.filter(x => x)
|
|
||||||
.filter(x => !x.startsWith(key))
|
|
||||||
await fs.promises.writeFile(configPath, lines.join('\n'))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
),
|
|
||||||
tryDisableAutomaticGarbageCollection: jest.fn(),
|
|
||||||
tryGetFetchUrl: jest.fn(),
|
|
||||||
tryReset: jest.fn(),
|
|
||||||
version: jest.fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
authToken: 'some auth token',
|
|
||||||
clean: true,
|
|
||||||
commit: '',
|
|
||||||
filter: undefined,
|
|
||||||
sparseCheckout: [],
|
|
||||||
sparseCheckoutConeMode: true,
|
|
||||||
fetchDepth: 1,
|
|
||||||
fetchTags: false,
|
|
||||||
showProgress: true,
|
|
||||||
lfs: false,
|
|
||||||
submodules: false,
|
|
||||||
nestedSubmodules: false,
|
|
||||||
persistCredentials: true,
|
|
||||||
ref: 'refs/heads/main',
|
|
||||||
repositoryName: 'my-repo',
|
|
||||||
repositoryOwner: 'my-org',
|
|
||||||
repositoryPath: '',
|
|
||||||
sshKey: sshPath ? 'some ssh private key' : '',
|
|
||||||
sshKnownHosts: '',
|
|
||||||
sshStrict: true,
|
|
||||||
sshUser: '',
|
|
||||||
workflowOrganizationId: 123456,
|
|
||||||
setSafeDirectory: true,
|
|
||||||
githubServerUrl: githubServerUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getActualSshKeyPath(): Promise<string> {
|
|
||||||
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
|
||||||
.sort()
|
|
||||||
.map(x => path.join(runnerTemp, x))
|
|
||||||
if (actualTempFiles.length === 0) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(actualTempFiles).toHaveLength(2)
|
|
||||||
expect(actualTempFiles[0].endsWith('_known_hosts')).toBeFalsy()
|
|
||||||
return actualTempFiles[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getActualSshKnownHostsPath(): Promise<string> {
|
|
||||||
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
|
|
||||||
.sort()
|
|
||||||
.map(x => path.join(runnerTemp, x))
|
|
||||||
if (actualTempFiles.length === 0) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(actualTempFiles).toHaveLength(2)
|
|
||||||
expect(actualTempFiles[1].endsWith('_known_hosts')).toBeTruthy()
|
|
||||||
expect(actualTempFiles[1].startsWith(actualTempFiles[0])).toBeTruthy()
|
|
||||||
return actualTempFiles[1]
|
|
||||||
}
|
|
@ -1,378 +0,0 @@
|
|||||||
import * as exec from '@actions/exec'
|
|
||||||
import * as fshelper from '../lib/fs-helper'
|
|
||||||
import * as commandManager from '../lib/git-command-manager'
|
|
||||||
|
|
||||||
let git: commandManager.IGitCommandManager
|
|
||||||
let mockExec = jest.fn()
|
|
||||||
|
|
||||||
describe('git-auth-helper tests', () => {
|
|
||||||
beforeAll(async () => {})
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(() => {})
|
|
||||||
|
|
||||||
it('branch list matches', async () => {
|
|
||||||
mockExec.mockImplementation((path, args, options) => {
|
|
||||||
console.log(args, options.listeners.stdout)
|
|
||||||
|
|
||||||
if (args.includes('version')) {
|
|
||||||
options.listeners.stdout(Buffer.from('2.18'))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.includes('rev-parse')) {
|
|
||||||
options.listeners.stdline(Buffer.from('refs/heads/foo'))
|
|
||||||
options.listeners.stdline(Buffer.from('refs/heads/bar'))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1
|
|
||||||
})
|
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
|
||||||
const workingDirectory = 'test'
|
|
||||||
const lfs = false
|
|
||||||
const doSparseCheckout = false
|
|
||||||
git = await commandManager.createCommandManager(
|
|
||||||
workingDirectory,
|
|
||||||
lfs,
|
|
||||||
doSparseCheckout
|
|
||||||
)
|
|
||||||
|
|
||||||
let branches = await git.branchList(false)
|
|
||||||
|
|
||||||
expect(branches).toHaveLength(2)
|
|
||||||
expect(branches.sort()).toEqual(['foo', 'bar'].sort())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('ambiguous ref name output is captured', async () => {
|
|
||||||
mockExec.mockImplementation((path, args, options) => {
|
|
||||||
console.log(args, options.listeners.stdout)
|
|
||||||
|
|
||||||
if (args.includes('version')) {
|
|
||||||
options.listeners.stdout(Buffer.from('2.18'))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.includes('rev-parse')) {
|
|
||||||
options.listeners.stdline(Buffer.from('refs/heads/foo'))
|
|
||||||
// If refs/tags/v1 and refs/heads/tags/v1 existed on this repository
|
|
||||||
options.listeners.errline(
|
|
||||||
Buffer.from("error: refname 'tags/v1' is ambiguous")
|
|
||||||
)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1
|
|
||||||
})
|
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
|
||||||
const workingDirectory = 'test'
|
|
||||||
const lfs = false
|
|
||||||
const doSparseCheckout = false
|
|
||||||
git = await commandManager.createCommandManager(
|
|
||||||
workingDirectory,
|
|
||||||
lfs,
|
|
||||||
doSparseCheckout
|
|
||||||
)
|
|
||||||
|
|
||||||
let branches = await git.branchList(false)
|
|
||||||
|
|
||||||
expect(branches).toHaveLength(1)
|
|
||||||
expect(branches.sort()).toEqual(['foo'].sort())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Test fetchDepth and fetchTags options', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
|
|
||||||
mockExec.mockImplementation((path, args, options) => {
|
|
||||||
console.log(args, options.listeners.stdout)
|
|
||||||
|
|
||||||
if (args.includes('version')) {
|
|
||||||
options.listeners.stdout(Buffer.from('2.18'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is true', async () => {
|
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
|
||||||
const workingDirectory = 'test'
|
|
||||||
const lfs = false
|
|
||||||
const doSparseCheckout = false
|
|
||||||
git = await commandManager.createCommandManager(
|
|
||||||
workingDirectory,
|
|
||||||
lfs,
|
|
||||||
doSparseCheckout
|
|
||||||
)
|
|
||||||
|
|
||||||
const refSpec = ['refspec1', 'refspec2']
|
|
||||||
const options = {
|
|
||||||
filter: 'filterValue',
|
|
||||||
fetchDepth: 0,
|
|
||||||
fetchTags: true
|
|
||||||
}
|
|
||||||
|
|
||||||
await git.fetch(refSpec, options)
|
|
||||||
|
|
||||||
expect(mockExec).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
[
|
|
||||||
'-c',
|
|
||||||
'protocol.version=2',
|
|
||||||
'fetch',
|
|
||||||
'--prune',
|
|
||||||
'--no-recurse-submodules',
|
|
||||||
'--filter=filterValue',
|
|
||||||
'origin',
|
|
||||||
'refspec1',
|
|
||||||
'refspec2'
|
|
||||||
],
|
|
||||||
expect.any(Object)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is false', async () => {
|
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
|
||||||
|
|
||||||
const workingDirectory = 'test'
|
|
||||||
const lfs = false
|
|
||||||
const doSparseCheckout = false
|
|
||||||
git = await commandManager.createCommandManager(
|
|
||||||
workingDirectory,
|
|
||||||
lfs,
|
|
||||||
doSparseCheckout
|
|
||||||
)
|
|
||||||
const refSpec = ['refspec1', 'refspec2']
|
|
||||||
const options = {
|
|
||||||
filter: 'filterValue',
|
|
||||||
fetchDepth: 0,
|
|
||||||
fetchTags: false
|
|
||||||
}
|
|
||||||
|
|
||||||
await git.fetch(refSpec, options)
|
|
||||||
|
|
||||||
expect(mockExec).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
[
|
|
||||||
'-c',
|
|
||||||
'protocol.version=2',
|
|
||||||
'fetch',
|
|
||||||
'--no-tags',
|
|
||||||
'--prune',
|
|
||||||
'--no-recurse-submodules',
|
|
||||||
'--filter=filterValue',
|
|
||||||
'origin',
|
|
||||||
'refspec1',
|
|
||||||
'refspec2'
|
|
||||||
],
|
|
||||||
expect.any(Object)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is false', async () => {
|
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
|
||||||
|
|
||||||
const workingDirectory = 'test'
|
|
||||||
const lfs = false
|
|
||||||
const doSparseCheckout = false
|
|
||||||
git = await commandManager.createCommandManager(
|
|
||||||
workingDirectory,
|
|
||||||
lfs,
|
|
||||||
doSparseCheckout
|
|
||||||
)
|
|
||||||
const refSpec = ['refspec1', 'refspec2']
|
|
||||||
const options = {
|
|
||||||
filter: 'filterValue',
|
|
||||||
fetchDepth: 1,
|
|
||||||
fetchTags: false
|
|
||||||
}
|
|
||||||
|
|
||||||
await git.fetch(refSpec, options)
|
|
||||||
|
|
||||||
expect(mockExec).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
[
|
|
||||||
'-c',
|
|
||||||
'protocol.version=2',
|
|
||||||
'fetch',
|
|
||||||
'--no-tags',
|
|
||||||
'--prune',
|
|
||||||
'--no-recurse-submodules',
|
|
||||||
'--filter=filterValue',
|
|
||||||
'--depth=1',
|
|
||||||
'origin',
|
|
||||||
'refspec1',
|
|
||||||
'refspec2'
|
|
||||||
],
|
|
||||||
expect.any(Object)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is true', async () => {
|
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
|
||||||
|
|
||||||
const workingDirectory = 'test'
|
|
||||||
const lfs = false
|
|
||||||
const doSparseCheckout = false
|
|
||||||
git = await commandManager.createCommandManager(
|
|
||||||
workingDirectory,
|
|
||||||
lfs,
|
|
||||||
doSparseCheckout
|
|
||||||
)
|
|
||||||
const refSpec = ['refspec1', 'refspec2']
|
|
||||||
const options = {
|
|
||||||
filter: 'filterValue',
|
|
||||||
fetchDepth: 1,
|
|
||||||
fetchTags: true
|
|
||||||
}
|
|
||||||
|
|
||||||
await git.fetch(refSpec, options)
|
|
||||||
|
|
||||||
expect(mockExec).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
[
|
|
||||||
'-c',
|
|
||||||
'protocol.version=2',
|
|
||||||
'fetch',
|
|
||||||
'--prune',
|
|
||||||
'--no-recurse-submodules',
|
|
||||||
'--filter=filterValue',
|
|
||||||
'--depth=1',
|
|
||||||
'origin',
|
|
||||||
'refspec1',
|
|
||||||
'refspec2'
|
|
||||||
],
|
|
||||||
expect.any(Object)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when showProgress is true', async () => {
|
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
|
||||||
|
|
||||||
const workingDirectory = 'test'
|
|
||||||
const lfs = false
|
|
||||||
const doSparseCheckout = false
|
|
||||||
git = await commandManager.createCommandManager(
|
|
||||||
workingDirectory,
|
|
||||||
lfs,
|
|
||||||
doSparseCheckout
|
|
||||||
)
|
|
||||||
const refSpec = ['refspec1', 'refspec2']
|
|
||||||
const options = {
|
|
||||||
filter: 'filterValue',
|
|
||||||
showProgress: true
|
|
||||||
}
|
|
||||||
|
|
||||||
await git.fetch(refSpec, options)
|
|
||||||
|
|
||||||
expect(mockExec).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
[
|
|
||||||
'-c',
|
|
||||||
'protocol.version=2',
|
|
||||||
'fetch',
|
|
||||||
'--no-tags',
|
|
||||||
'--prune',
|
|
||||||
'--no-recurse-submodules',
|
|
||||||
'--progress',
|
|
||||||
'--filter=filterValue',
|
|
||||||
'origin',
|
|
||||||
'refspec1',
|
|
||||||
'refspec2'
|
|
||||||
],
|
|
||||||
expect.any(Object)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when fetchDepth is 42 and showProgress is true', async () => {
|
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
|
||||||
|
|
||||||
const workingDirectory = 'test'
|
|
||||||
const lfs = false
|
|
||||||
const doSparseCheckout = false
|
|
||||||
git = await commandManager.createCommandManager(
|
|
||||||
workingDirectory,
|
|
||||||
lfs,
|
|
||||||
doSparseCheckout
|
|
||||||
)
|
|
||||||
const refSpec = ['refspec1', 'refspec2']
|
|
||||||
const options = {
|
|
||||||
filter: 'filterValue',
|
|
||||||
fetchDepth: 42,
|
|
||||||
showProgress: true
|
|
||||||
}
|
|
||||||
|
|
||||||
await git.fetch(refSpec, options)
|
|
||||||
|
|
||||||
expect(mockExec).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
[
|
|
||||||
'-c',
|
|
||||||
'protocol.version=2',
|
|
||||||
'fetch',
|
|
||||||
'--no-tags',
|
|
||||||
'--prune',
|
|
||||||
'--no-recurse-submodules',
|
|
||||||
'--progress',
|
|
||||||
'--filter=filterValue',
|
|
||||||
'--depth=42',
|
|
||||||
'origin',
|
|
||||||
'refspec1',
|
|
||||||
'refspec2'
|
|
||||||
],
|
|
||||||
expect.any(Object)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call execGit with the correct arguments when fetchTags is true and showProgress is true', async () => {
|
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
|
||||||
|
|
||||||
const workingDirectory = 'test'
|
|
||||||
const lfs = false
|
|
||||||
const doSparseCheckout = false
|
|
||||||
git = await commandManager.createCommandManager(
|
|
||||||
workingDirectory,
|
|
||||||
lfs,
|
|
||||||
doSparseCheckout
|
|
||||||
)
|
|
||||||
const refSpec = ['refspec1', 'refspec2']
|
|
||||||
const options = {
|
|
||||||
filter: 'filterValue',
|
|
||||||
fetchTags: true,
|
|
||||||
showProgress: true
|
|
||||||
}
|
|
||||||
|
|
||||||
await git.fetch(refSpec, options)
|
|
||||||
|
|
||||||
expect(mockExec).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
[
|
|
||||||
'-c',
|
|
||||||
'protocol.version=2',
|
|
||||||
'fetch',
|
|
||||||
'--prune',
|
|
||||||
'--no-recurse-submodules',
|
|
||||||
'--progress',
|
|
||||||
'--filter=filterValue',
|
|
||||||
'origin',
|
|
||||||
'refspec1',
|
|
||||||
'refspec2'
|
|
||||||
],
|
|
||||||
expect.any(Object)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,507 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
import * as gitDirectoryHelper from '../lib/git-directory-helper'
|
|
||||||
import * as io from '@actions/io'
|
|
||||||
import * as path from 'path'
|
|
||||||
import {IGitCommandManager} from '../lib/git-command-manager'
|
|
||||||
|
|
||||||
const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper')
|
|
||||||
let repositoryPath: string
|
|
||||||
let repositoryUrl: string
|
|
||||||
let clean: boolean
|
|
||||||
let ref: string
|
|
||||||
let git: IGitCommandManager
|
|
||||||
|
|
||||||
describe('git-directory-helper tests', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
// Clear test workspace
|
|
||||||
await io.rmRF(testWorkspace)
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock error/warning/info/debug
|
|
||||||
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
// Unregister mocks
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
const cleansWhenCleanTrue = 'cleans when clean true'
|
|
||||||
it(cleansWhenCleanTrue, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(cleansWhenCleanTrue)
|
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
||||||
|
|
||||||
// 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.tryClean).toHaveBeenCalled()
|
|
||||||
expect(git.tryReset).toHaveBeenCalled()
|
|
||||||
expect(core.warning).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
const checkoutDetachWhenNotDetached = 'checkout detach when not detached'
|
|
||||||
it(checkoutDetachWhenNotDetached, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(checkoutDetachWhenNotDetached)
|
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
||||||
|
|
||||||
// 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.checkoutDetach).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
const doesNotCheckoutDetachWhenNotAlreadyDetached =
|
|
||||||
'does not checkout detach when already detached'
|
|
||||||
it(doesNotCheckoutDetachWhenNotAlreadyDetached, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(doesNotCheckoutDetachWhenNotAlreadyDetached)
|
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
||||||
const mockIsDetached = git.isDetached as jest.Mock<any, any>
|
|
||||||
mockIsDetached.mockImplementation(async () => {
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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.checkoutDetach).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
const doesNotCleanWhenCleanFalse = 'does not clean when clean false'
|
|
||||||
it(doesNotCleanWhenCleanFalse, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(doesNotCleanWhenCleanFalse)
|
|
||||||
clean = false
|
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
||||||
|
|
||||||
// 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.isDetached).toHaveBeenCalled()
|
|
||||||
expect(git.branchList).toHaveBeenCalled()
|
|
||||||
expect(core.warning).not.toHaveBeenCalled()
|
|
||||||
expect(git.tryClean).not.toHaveBeenCalled()
|
|
||||||
expect(git.tryReset).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
const removesContentsWhenCleanFails = 'removes contents when clean fails'
|
|
||||||
it(removesContentsWhenCleanFails, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(removesContentsWhenCleanFails)
|
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
||||||
let mockTryClean = git.tryClean as jest.Mock<any, any>
|
|
||||||
mockTryClean.mockImplementation(async () => {
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await gitDirectoryHelper.prepareExistingDirectory(
|
|
||||||
git,
|
|
||||||
repositoryPath,
|
|
||||||
repositoryUrl,
|
|
||||||
clean,
|
|
||||||
ref
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const files = await fs.promises.readdir(repositoryPath)
|
|
||||||
expect(files).toHaveLength(0)
|
|
||||||
expect(git.tryClean).toHaveBeenCalled()
|
|
||||||
expect(core.warning).toHaveBeenCalled()
|
|
||||||
expect(git.tryReset).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
const removesContentsWhenDifferentRepositoryUrl =
|
|
||||||
'removes contents when different repository url'
|
|
||||||
it(removesContentsWhenDifferentRepositoryUrl, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(removesContentsWhenDifferentRepositoryUrl)
|
|
||||||
clean = false
|
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
||||||
const differentRepositoryUrl =
|
|
||||||
'https://github.com/my-different-org/my-different-repo'
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await gitDirectoryHelper.prepareExistingDirectory(
|
|
||||||
git,
|
|
||||||
repositoryPath,
|
|
||||||
differentRepositoryUrl,
|
|
||||||
clean,
|
|
||||||
ref
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const files = await fs.promises.readdir(repositoryPath)
|
|
||||||
expect(files).toHaveLength(0)
|
|
||||||
expect(core.warning).not.toHaveBeenCalled()
|
|
||||||
expect(git.isDetached).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
const removesContentsWhenNoGitDirectory =
|
|
||||||
'removes contents when no git directory'
|
|
||||||
it(removesContentsWhenNoGitDirectory, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(removesContentsWhenNoGitDirectory)
|
|
||||||
clean = false
|
|
||||||
await io.rmRF(path.join(repositoryPath, '.git'))
|
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await gitDirectoryHelper.prepareExistingDirectory(
|
|
||||||
git,
|
|
||||||
repositoryPath,
|
|
||||||
repositoryUrl,
|
|
||||||
clean,
|
|
||||||
ref
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const files = await fs.promises.readdir(repositoryPath)
|
|
||||||
expect(files).toHaveLength(0)
|
|
||||||
expect(core.warning).not.toHaveBeenCalled()
|
|
||||||
expect(git.isDetached).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
const removesContentsWhenResetFails = 'removes contents when reset fails'
|
|
||||||
it(removesContentsWhenResetFails, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(removesContentsWhenResetFails)
|
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
||||||
let mockTryReset = git.tryReset as jest.Mock<any, any>
|
|
||||||
mockTryReset.mockImplementation(async () => {
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await gitDirectoryHelper.prepareExistingDirectory(
|
|
||||||
git,
|
|
||||||
repositoryPath,
|
|
||||||
repositoryUrl,
|
|
||||||
clean,
|
|
||||||
ref
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const files = await fs.promises.readdir(repositoryPath)
|
|
||||||
expect(files).toHaveLength(0)
|
|
||||||
expect(git.tryClean).toHaveBeenCalled()
|
|
||||||
expect(git.tryReset).toHaveBeenCalled()
|
|
||||||
expect(core.warning).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
const removesContentsWhenUndefinedGitCommandManager =
|
|
||||||
'removes contents when undefined git command manager'
|
|
||||||
it(removesContentsWhenUndefinedGitCommandManager, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(removesContentsWhenUndefinedGitCommandManager)
|
|
||||||
clean = false
|
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await gitDirectoryHelper.prepareExistingDirectory(
|
|
||||||
undefined,
|
|
||||||
repositoryPath,
|
|
||||||
repositoryUrl,
|
|
||||||
clean,
|
|
||||||
ref
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const files = await fs.promises.readdir(repositoryPath)
|
|
||||||
expect(files).toHaveLength(0)
|
|
||||||
expect(core.warning).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
const removesLocalBranches = 'removes local branches'
|
|
||||||
it(removesLocalBranches, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(removesLocalBranches)
|
|
||||||
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 ? [] : ['local-branch-1', 'local-branch-2']
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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).toHaveBeenCalledWith(false, 'local-branch-1')
|
|
||||||
expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-2')
|
|
||||||
})
|
|
||||||
|
|
||||||
const cleanWhenSubmoduleStatusIsFalse =
|
|
||||||
'cleans when submodule status is false'
|
|
||||||
|
|
||||||
it(cleanWhenSubmoduleStatusIsFalse, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(cleanWhenSubmoduleStatusIsFalse)
|
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
||||||
|
|
||||||
//mock bad submodule
|
|
||||||
|
|
||||||
const submoduleStatus = git.submoduleStatus as jest.Mock<any, any>
|
|
||||||
submoduleStatus.mockImplementation(async (remote: boolean) => {
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await gitDirectoryHelper.prepareExistingDirectory(
|
|
||||||
git,
|
|
||||||
repositoryPath,
|
|
||||||
repositoryUrl,
|
|
||||||
clean,
|
|
||||||
ref
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const files = await fs.promises.readdir(repositoryPath)
|
|
||||||
expect(files).toHaveLength(0)
|
|
||||||
expect(git.tryClean).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
const doesNotCleanWhenSubmoduleStatusIsTrue =
|
|
||||||
'does not clean when submodule status is true'
|
|
||||||
|
|
||||||
it(doesNotCleanWhenSubmoduleStatusIsTrue, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(doesNotCleanWhenSubmoduleStatusIsTrue)
|
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
||||||
|
|
||||||
const submoduleStatus = git.submoduleStatus as jest.Mock<any, any>
|
|
||||||
submoduleStatus.mockImplementation(async (remote: boolean) => {
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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.tryClean).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
const removesLockFiles = 'removes lock files'
|
|
||||||
it(removesLockFiles, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(removesLockFiles)
|
|
||||||
clean = false
|
|
||||||
await fs.promises.writeFile(
|
|
||||||
path.join(repositoryPath, '.git', 'index.lock'),
|
|
||||||
''
|
|
||||||
)
|
|
||||||
await fs.promises.writeFile(
|
|
||||||
path.join(repositoryPath, '.git', 'shallow.lock'),
|
|
||||||
''
|
|
||||||
)
|
|
||||||
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await gitDirectoryHelper.prepareExistingDirectory(
|
|
||||||
git,
|
|
||||||
repositoryPath,
|
|
||||||
repositoryUrl,
|
|
||||||
clean,
|
|
||||||
ref
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
let files = await fs.promises.readdir(path.join(repositoryPath, '.git'))
|
|
||||||
expect(files).toHaveLength(0)
|
|
||||||
files = await fs.promises.readdir(repositoryPath)
|
|
||||||
expect(files.sort()).toEqual(['.git', 'my-file'])
|
|
||||||
expect(git.isDetached).toHaveBeenCalled()
|
|
||||||
expect(git.branchList).toHaveBeenCalled()
|
|
||||||
expect(core.warning).not.toHaveBeenCalled()
|
|
||||||
expect(git.tryClean).not.toHaveBeenCalled()
|
|
||||||
expect(git.tryReset).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
const removesAncestorRemoteBranch = 'removes ancestor remote branch'
|
|
||||||
it(removesAncestorRemoteBranch, async () => {
|
|
||||||
// Arrange
|
|
||||||
await setup(removesAncestorRemoteBranch)
|
|
||||||
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', 'origin/remote-branch-2'] : []
|
|
||||||
})
|
|
||||||
ref = 'remote-branch-1/conflict'
|
|
||||||
|
|
||||||
// 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'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
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'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
async function setup(testName: string): Promise<void> {
|
|
||||||
testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-')
|
|
||||||
|
|
||||||
// Repository directory
|
|
||||||
repositoryPath = path.join(testWorkspace, testName)
|
|
||||||
await fs.promises.mkdir(path.join(repositoryPath, '.git'), {recursive: true})
|
|
||||||
|
|
||||||
// Repository URL
|
|
||||||
repositoryUrl = 'https://github.com/my-org/my-repo'
|
|
||||||
|
|
||||||
// Clean
|
|
||||||
clean = true
|
|
||||||
|
|
||||||
// Ref
|
|
||||||
ref = ''
|
|
||||||
|
|
||||||
// Git command manager
|
|
||||||
git = {
|
|
||||||
branchDelete: jest.fn(),
|
|
||||||
branchExists: jest.fn(),
|
|
||||||
branchList: jest.fn(async () => {
|
|
||||||
return []
|
|
||||||
}),
|
|
||||||
disableSparseCheckout: jest.fn(),
|
|
||||||
sparseCheckout: jest.fn(),
|
|
||||||
sparseCheckoutNonConeMode: jest.fn(),
|
|
||||||
checkout: jest.fn(),
|
|
||||||
checkoutDetach: jest.fn(),
|
|
||||||
config: jest.fn(),
|
|
||||||
configExists: jest.fn(),
|
|
||||||
fetch: jest.fn(),
|
|
||||||
getDefaultBranch: jest.fn(),
|
|
||||||
getWorkingDirectory: jest.fn(() => repositoryPath),
|
|
||||||
init: jest.fn(),
|
|
||||||
isDetached: jest.fn(),
|
|
||||||
lfsFetch: jest.fn(),
|
|
||||||
lfsInstall: jest.fn(),
|
|
||||||
log1: jest.fn(),
|
|
||||||
remoteAdd: jest.fn(),
|
|
||||||
removeEnvironmentVariable: jest.fn(),
|
|
||||||
revParse: jest.fn(),
|
|
||||||
setEnvironmentVariable: jest.fn(),
|
|
||||||
shaExists: jest.fn(),
|
|
||||||
submoduleForeach: jest.fn(),
|
|
||||||
submoduleSync: jest.fn(),
|
|
||||||
submoduleUpdate: jest.fn(),
|
|
||||||
submoduleStatus: jest.fn(async () => {
|
|
||||||
return true
|
|
||||||
}),
|
|
||||||
tagExists: jest.fn(),
|
|
||||||
tryClean: jest.fn(async () => {
|
|
||||||
return true
|
|
||||||
}),
|
|
||||||
tryConfigUnset: jest.fn(),
|
|
||||||
tryDisableAutomaticGarbageCollection: jest.fn(),
|
|
||||||
tryGetFetchUrl: jest.fn(async () => {
|
|
||||||
// Sanity check - this function shouldn't be called when the .git directory doesn't exist
|
|
||||||
await fs.promises.stat(path.join(repositoryPath, '.git'))
|
|
||||||
return repositoryUrl
|
|
||||||
}),
|
|
||||||
tryReset: jest.fn(async () => {
|
|
||||||
return true
|
|
||||||
}),
|
|
||||||
version: jest.fn()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,4 @@
|
|||||||
import {GitVersion} from '../src/git-version'
|
import {GitVersion} from '../lib/git-version'
|
||||||
import {MinimumGitSparseCheckoutVersion} from '../src/git-command-manager'
|
|
||||||
|
|
||||||
describe('git-version tests', () => {
|
describe('git-version tests', () => {
|
||||||
it('basics', async () => {
|
it('basics', async () => {
|
||||||
@ -43,44 +42,4 @@ describe('git-version tests', () => {
|
|||||||
expect(version.checkMinimum(new GitVersion('5.1'))).toBeFalsy()
|
expect(version.checkMinimum(new GitVersion('5.1'))).toBeFalsy()
|
||||||
expect(version.checkMinimum(new GitVersion('5.1.2'))).toBeFalsy()
|
expect(version.checkMinimum(new GitVersion('5.1.2'))).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sparse checkout', async () => {
|
|
||||||
const minSparseVer = MinimumGitSparseCheckoutVersion
|
|
||||||
expect(new GitVersion('1.0').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('1.99').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.0').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.24').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.24.0').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.24.9').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.25').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.25.0').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.25.1').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.25.9').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.26').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.26.0').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.26.1').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.26.9').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.27').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.27.0').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.27.1').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
expect(new GitVersion('2.27.9').checkMinimum(minSparseVer)).toBeFalsy()
|
|
||||||
// /---------------------------------------
|
|
||||||
// ^^^ before / after vvv
|
|
||||||
// --------------------------/
|
|
||||||
expect(new GitVersion('2.28').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('2.28.0').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('2.28.1').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('2.28.9').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('2.29').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('2.29.0').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('2.29.1').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('2.29.9').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('2.99').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('3.0').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('3.99').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('4.0').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('4.99').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('5.0').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
expect(new GitVersion('5.99').checkMinimum(minSparseVer)).toBeTruthy()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@ -1,55 +1,47 @@
|
|||||||
import * as core from '@actions/core'
|
import * as assert from 'assert'
|
||||||
import * as fsHelper from '../lib/fs-helper'
|
|
||||||
import * as github from '@actions/github'
|
|
||||||
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 {ISourceSettings} from '../lib/git-source-provider'
|
||||||
import {IGitSourceSettings} from '../lib/git-source-settings'
|
|
||||||
|
|
||||||
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
|
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||||
const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
|
const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
|
||||||
|
|
||||||
// Inputs for mock @actions/core
|
// Late bind
|
||||||
let inputs = {} as any
|
let inputHelper: any
|
||||||
|
|
||||||
// Shallow clone original @actions/github context
|
// Mock @actions/core
|
||||||
let originalContext = {...github.context}
|
let inputs = {} as any
|
||||||
|
const mockCore = jest.genMockFromModule('@actions/core') as any
|
||||||
|
mockCore.getInput = (name: string) => {
|
||||||
|
return inputs[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock @actions/github
|
||||||
|
const mockGitHub = jest.genMockFromModule('@actions/github') as any
|
||||||
|
mockGitHub.context = {
|
||||||
|
repo: {
|
||||||
|
owner: 'some-owner',
|
||||||
|
repo: 'some-repo'
|
||||||
|
},
|
||||||
|
ref: 'refs/heads/some-ref',
|
||||||
|
sha: '1234567890123456789012345678901234567890'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock ./fs-helper
|
||||||
|
const mockFSHelper = jest.genMockFromModule('../lib/fs-helper') as any
|
||||||
|
mockFSHelper.directoryExistsSync = (path: string) => path == gitHubWorkspace
|
||||||
|
|
||||||
describe('input-helper tests', () => {
|
describe('input-helper tests', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// Mock getInput
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
|
|
||||||
return inputs[name]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Mock error/warning/info/debug
|
|
||||||
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
|
||||||
|
|
||||||
// Mock github context
|
|
||||||
jest.spyOn(github.context, 'repo', 'get').mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
owner: 'some-owner',
|
|
||||||
repo: 'some-repo'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
github.context.ref = 'refs/heads/some-ref'
|
|
||||||
github.context.sha = '1234567890123456789012345678901234567890'
|
|
||||||
|
|
||||||
// Mock ./fs-helper directoryExistsSync()
|
|
||||||
jest
|
|
||||||
.spyOn(fsHelper, 'directoryExistsSync')
|
|
||||||
.mockImplementation((path: string) => path == gitHubWorkspace)
|
|
||||||
|
|
||||||
// Mock ./workflowContextHelper getOrganizationId()
|
|
||||||
jest
|
|
||||||
.spyOn(workflowContextHelper, 'getOrganizationId')
|
|
||||||
.mockImplementation(() => Promise.resolve(123456))
|
|
||||||
|
|
||||||
// GitHub workspace
|
// GitHub workspace
|
||||||
process.env['GITHUB_WORKSPACE'] = gitHubWorkspace
|
process.env['GITHUB_WORKSPACE'] = gitHubWorkspace
|
||||||
|
|
||||||
|
// Mocks
|
||||||
|
jest.setMock('@actions/core', mockCore)
|
||||||
|
jest.setMock('@actions/github', mockGitHub)
|
||||||
|
jest.setMock('../lib/fs-helper', mockFSHelper)
|
||||||
|
|
||||||
|
// Now import
|
||||||
|
inputHelper = require('../lib/input-helper')
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -58,90 +50,71 @@ describe('input-helper tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
// Restore GitHub workspace
|
// Reset GitHub workspace
|
||||||
delete process.env['GITHUB_WORKSPACE']
|
delete process.env['GITHUB_WORKSPACE']
|
||||||
if (originalGitHubWorkspace) {
|
if (originalGitHubWorkspace) {
|
||||||
process.env['GITHUB_WORKSPACE'] = originalGitHubWorkspace
|
process.env['GITHUB_WORKSPACE'] = originalGitHubWorkspace
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore @actions/github context
|
// Reset modules
|
||||||
github.context.ref = originalContext.ref
|
jest.resetModules()
|
||||||
github.context.sha = originalContext.sha
|
|
||||||
|
|
||||||
// Restore
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets defaults', async () => {
|
it('sets defaults', () => {
|
||||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
const settings: ISourceSettings = inputHelper.getInputs()
|
||||||
expect(settings).toBeTruthy()
|
expect(settings).toBeTruthy()
|
||||||
expect(settings.authToken).toBeFalsy()
|
expect(settings.accessToken).toBeFalsy()
|
||||||
expect(settings.clean).toBe(true)
|
expect(settings.clean).toBe(true)
|
||||||
expect(settings.commit).toBeTruthy()
|
expect(settings.commit).toBeTruthy()
|
||||||
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
|
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
|
||||||
expect(settings.filter).toBe(undefined)
|
|
||||||
expect(settings.sparseCheckout).toBe(undefined)
|
|
||||||
expect(settings.sparseCheckoutConeMode).toBe(true)
|
|
||||||
expect(settings.fetchDepth).toBe(1)
|
expect(settings.fetchDepth).toBe(1)
|
||||||
expect(settings.fetchTags).toBe(false)
|
|
||||||
expect(settings.showProgress).toBe(true)
|
|
||||||
expect(settings.lfs).toBe(false)
|
expect(settings.lfs).toBe(false)
|
||||||
expect(settings.ref).toBe('refs/heads/some-ref')
|
expect(settings.ref).toBe('refs/heads/some-ref')
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('qualifies ref', async () => {
|
it('requires qualified repo', () => {
|
||||||
let originalRef = github.context.ref
|
|
||||||
try {
|
|
||||||
github.context.ref = 'some-unqualified-ref'
|
|
||||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
|
||||||
expect(settings).toBeTruthy()
|
|
||||||
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
|
|
||||||
expect(settings.ref).toBe('refs/heads/some-unqualified-ref')
|
|
||||||
} finally {
|
|
||||||
github.context.ref = originalRef
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('requires qualified repo', async () => {
|
|
||||||
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: ISourceSettings = 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: ISourceSettings = 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: ISourceSettings = 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: ISourceSettings = 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('sets workflow organization ID', async () => {
|
it('gives good error message for submodules input', () => {
|
||||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
inputs.submodules = 'true'
|
||||||
expect(settings.workflowOrganizationId).toBe(123456)
|
assert.throws(() => {
|
||||||
|
inputHelper.getInputs()
|
||||||
|
}, /The input 'submodules' is not supported/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
|
|
||||||
mkdir override-git-version
|
|
||||||
cd override-git-version
|
|
||||||
echo @echo override git version 1.2.3 > git.cmd
|
|
||||||
echo "%CD%" >> $GITHUB_PATH
|
|
||||||
cd ..
|
|
@ -1,9 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
mkdir override-git-version
|
|
||||||
cd override-git-version
|
|
||||||
echo "#!/bin/sh" > git
|
|
||||||
echo "echo override git version 1.2.3" >> git
|
|
||||||
chmod +x git
|
|
||||||
echo "$(pwd)" >> $GITHUB_PATH
|
|
||||||
cd ..
|
|
@ -7,16 +7,16 @@ let git: IGitCommandManager
|
|||||||
|
|
||||||
describe('ref-helper tests', () => {
|
describe('ref-helper tests', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
git = {} as unknown as IGitCommandManager
|
git = ({} as unknown) as IGitCommandManager
|
||||||
})
|
})
|
||||||
|
|
||||||
it('getCheckoutInfo requires git', async () => {
|
it('getCheckoutInfo requires git', async () => {
|
||||||
const git = null as unknown as IGitCommandManager
|
const git = (null as unknown) as IGitCommandManager
|
||||||
try {
|
try {
|
||||||
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,17 +1,18 @@
|
|||||||
import * as core from '@actions/core'
|
const mockCore = jest.genMockFromModule('@actions/core') as any
|
||||||
import {RetryHelper} from '../lib/retry-helper'
|
mockCore.info = (message: string) => {
|
||||||
|
info.push(message)
|
||||||
|
}
|
||||||
let info: string[]
|
let info: string[]
|
||||||
let retryHelper: any
|
let retryHelper: any
|
||||||
|
|
||||||
describe('retry-helper tests', () => {
|
describe('retry-helper tests', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// Mock @actions/core info()
|
// Mocks
|
||||||
jest.spyOn(core, 'info').mockImplementation((message: string) => {
|
jest.setMock('@actions/core', mockCore)
|
||||||
info.push(message)
|
|
||||||
})
|
|
||||||
|
|
||||||
retryHelper = new RetryHelper(3, 0, 0)
|
// Now import
|
||||||
|
const retryHelperModule = require('../lib/retry-helper')
|
||||||
|
retryHelper = new retryHelperModule.RetryHelper(3, 0, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -20,8 +21,8 @@ describe('retry-helper tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
// Restore
|
// Reset modules
|
||||||
jest.restoreAllMocks()
|
jest.resetModules()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('first attempt succeeds', async () => {
|
it('first attempt succeeds', async () => {
|
||||||
@ -68,13 +69,13 @@ describe('retry-helper tests', () => {
|
|||||||
|
|
||||||
it('all attempts fail succeeds', async () => {
|
it('all attempts fail succeeds', async () => {
|
||||||
let attempts = 0
|
let attempts = 0
|
||||||
let error: Error = null as unknown as Error
|
let error: Error = (null as unknown) as Error
|
||||||
try {
|
try {
|
||||||
await retryHelper.execute(() => {
|
await retryHelper.execute(() => {
|
||||||
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)
|
||||||
|
@ -5,7 +5,7 @@ if [ ! -f "./basic/basic-file.txt" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$1" = "--archive" ]; then
|
if [ "$1" = "container" ]; then
|
||||||
# Verify no .git folder
|
# Verify no .git folder
|
||||||
if [ -d "./basic/.git" ]; then
|
if [ -d "./basic/.git" ]; then
|
||||||
echo "Did not expect ./basic/.git folder to exist"
|
echo "Did not expect ./basic/.git folder to exist"
|
||||||
@ -18,21 +18,7 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify that sparse-checkout is disabled.
|
|
||||||
SPARSE_CHECKOUT_ENABLED=$(git -C ./basic config --local --get-all core.sparseCheckout)
|
|
||||||
if [ "$SPARSE_CHECKOUT_ENABLED" != "" ]; then
|
|
||||||
echo "Expected sparse-checkout to be disabled (discovered: $SPARSE_CHECKOUT_ENABLED)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify git configuration shows worktreeConfig is effectively disabled
|
|
||||||
WORKTREE_CONFIG_ENABLED=$(git -C ./basic config --local --get-all extensions.worktreeConfig)
|
|
||||||
if [[ "$WORKTREE_CONFIG_ENABLED" != "" ]]; then
|
|
||||||
echo "Expected extensions.worktreeConfig (boolean) to be disabled in git config. This could be an artifact of sparse checkout functionality."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify auth token
|
# Verify auth token
|
||||||
cd basic
|
cd basic
|
||||||
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
|
git fetch --depth=1
|
||||||
fi
|
fi
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Verify .git folder
|
|
||||||
if [ ! -d "./fetch-filter/.git" ]; then
|
|
||||||
echo "Expected ./fetch-filter/.git folder to exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify .git/config contains partialclonefilter
|
|
||||||
|
|
||||||
CLONE_FILTER=$(git -C fetch-filter config --local --get remote.origin.partialclonefilter)
|
|
||||||
|
|
||||||
if [ "$CLONE_FILTER" != "blob:none" ]; then
|
|
||||||
echo "Expected ./fetch-filter/.git/config to have 'remote.origin.partialclonefilter' set to 'blob:none'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
@ -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
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Verify .git folder
|
|
||||||
if [ ! -d "./sparse-checkout-non-cone-mode/.git" ]; then
|
|
||||||
echo "Expected ./sparse-checkout-non-cone-mode/.git folder to exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify sparse-checkout (non-cone-mode)
|
|
||||||
cd sparse-checkout-non-cone-mode
|
|
||||||
|
|
||||||
ENABLED=$(git config --local --get-all core.sparseCheckout)
|
|
||||||
|
|
||||||
if [ "$?" != "0" ]; then
|
|
||||||
echo "Failed to verify that sparse-checkout is enabled"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check that sparse-checkout is enabled
|
|
||||||
if [ "$ENABLED" != "true" ]; then
|
|
||||||
echo "Expected sparse-checkout to be enabled (is: $ENABLED)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SPARSE_CHECKOUT_FILE=$(git rev-parse --git-path info/sparse-checkout)
|
|
||||||
|
|
||||||
if [ "$?" != "0" ]; then
|
|
||||||
echo "Failed to validate sparse-checkout"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check that sparse-checkout list is not empty
|
|
||||||
if [ ! -f "$SPARSE_CHECKOUT_FILE" ]; then
|
|
||||||
echo "Expected sparse-checkout file to exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check that all folders from sparse-checkout exists
|
|
||||||
for pattern in $(cat "$SPARSE_CHECKOUT_FILE")
|
|
||||||
do
|
|
||||||
if [ ! -d "${pattern#/}" ]; then
|
|
||||||
echo "Expected directory '${pattern#/}' to exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Verify that the root directory is not checked out
|
|
||||||
if [ -f README.md ]; then
|
|
||||||
echo "Expected top-level files not to exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
@ -1,63 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Verify .git folder
|
|
||||||
if [ ! -d "./sparse-checkout/.git" ]; then
|
|
||||||
echo "Expected ./sparse-checkout/.git folder to exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify sparse-checkout
|
|
||||||
cd sparse-checkout
|
|
||||||
|
|
||||||
SPARSE=$(git sparse-checkout list)
|
|
||||||
|
|
||||||
if [ "$?" != "0" ]; then
|
|
||||||
echo "Failed to validate sparse-checkout"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check that sparse-checkout list is not empty
|
|
||||||
if [ -z "$SPARSE" ]; then
|
|
||||||
echo "Expected sparse-checkout list to not be empty"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check that all folders of the sparse checkout exist
|
|
||||||
for pattern in $SPARSE
|
|
||||||
do
|
|
||||||
if [ ! -d "$pattern" ]; then
|
|
||||||
echo "Expected directory '$pattern' to exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
checkSparse () {
|
|
||||||
if [ ! -d "./$1" ]; then
|
|
||||||
echo "Expected directory '$1' to exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
for file in $(git ls-tree -r --name-only HEAD $1)
|
|
||||||
do
|
|
||||||
if [ ! -f "$file" ]; then
|
|
||||||
echo "Expected file '$file' to exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check that all folders and their children have been checked out
|
|
||||||
checkSparse __test__
|
|
||||||
checkSparse .github
|
|
||||||
checkSparse dist
|
|
||||||
|
|
||||||
# Check that only sparse-checkout folders have been checked out
|
|
||||||
for pattern in $(git ls-tree --name-only HEAD)
|
|
||||||
do
|
|
||||||
if [ -d "$pattern" ]; then
|
|
||||||
if [[ "$pattern" != "__test__" && "$pattern" != ".github" && "$pattern" != "dist" ]]; then
|
|
||||||
echo "Expected directory '$pattern' to not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
@ -1,11 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [ ! -f "./submodules-false/regular-file.txt" ]; then
|
|
||||||
echo "Expected regular file does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "./submodules-false/submodule-level-1/submodule-file.txt" ]; then
|
|
||||||
echo "Unexpected submodule file exists"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
11
__test__/verify-submodules-not-checked-out.sh
Executable file
11
__test__/verify-submodules-not-checked-out.sh
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ ! -f "./submodules-not-checked-out/regular-file.txt" ]; then
|
||||||
|
echo "Expected regular file does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "./submodules-not-checked-out/submodule-level-1/submodule-file.txt" ]; then
|
||||||
|
echo "Unexpected submodule file exists"
|
||||||
|
exit 1
|
||||||
|
fi
|
@ -1,26 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [ ! -f "./submodules-recursive/regular-file.txt" ]; then
|
|
||||||
echo "Expected regular file does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "./submodules-recursive/submodule-level-1/submodule-file.txt" ]; then
|
|
||||||
echo "Expected submodule file does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "./submodules-recursive/submodule-level-1/submodule-level-2/nested-submodule-file.txt" ]; then
|
|
||||||
echo "Expected nested submodule file does not exists"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Testing persisted credential"
|
|
||||||
pushd ./submodules-recursive/submodule-level-1/submodule-level-2
|
|
||||||
git config --local --name-only --get-regexp http.+extraheader && git fetch
|
|
||||||
if [ "$?" != "0" ]; then
|
|
||||||
echo "Failed to validate persisted credential"
|
|
||||||
popd
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
popd
|
|
@ -1,26 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [ ! -f "./submodules-true/regular-file.txt" ]; then
|
|
||||||
echo "Expected regular file does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "./submodules-true/submodule-level-1/submodule-file.txt" ]; then
|
|
||||||
echo "Expected submodule file does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "./submodules-true/submodule-level-1/submodule-level-2/nested-submodule-file.txt" ]; then
|
|
||||||
echo "Unexpected nested submodule file exists"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Testing persisted credential"
|
|
||||||
pushd ./submodules-true/submodule-level-1
|
|
||||||
git config --local --name-only --get-regexp http.+extraheader && git fetch
|
|
||||||
if [ "$?" != "0" ]; then
|
|
||||||
echo "Failed to validate persisted credential"
|
|
||||||
popd
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
popd
|
|
89
action.yml
89
action.yml
@ -1,104 +1,29 @@
|
|||||||
name: 'Checkout'
|
name: 'Checkout'
|
||||||
description: 'Checkout a Git repository at a particular version'
|
description: 'Checkout a Git repository at a particular version'
|
||||||
inputs:
|
inputs:
|
||||||
repository:
|
repository:
|
||||||
description: 'Repository name with owner. For example, actions/checkout'
|
description: 'Repository name with owner. For example, actions/checkout'
|
||||||
default: ${{ github.repository }}
|
default: ${{ github.repository }}
|
||||||
ref:
|
ref:
|
||||||
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
|
||||||
triggered a workflow, this defaults to the reference or SHA for that
|
that triggered a workflow, this defaults to the reference or SHA for
|
||||||
event. Otherwise, uses the default branch.
|
that event. Otherwise, defaults to `master`.
|
||||||
token:
|
token:
|
||||||
description: >
|
description: 'Access token for clone repository'
|
||||||
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
|
|
||||||
commands. The post-job step removes the PAT.
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
description: >
|
|
||||||
SSH key used to fetch the repository. The SSH key is configured with the local
|
|
||||||
git config, which enables your scripts to run authenticated git commands.
|
|
||||||
The post-job step removes the SSH key.
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
ssh-known-hosts:
|
|
||||||
description: >
|
|
||||||
Known hosts in addition to the user and global host key database. The public
|
|
||||||
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-strict:
|
|
||||||
description: >
|
|
||||||
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
|
|
||||||
ssh-user:
|
|
||||||
description: >
|
|
||||||
The user to use when connecting to the remote SSH host. By default 'git' is used.
|
|
||||||
default: git
|
|
||||||
persist-credentials:
|
|
||||||
description: 'Whether to configure the token or SSH key with the local git config'
|
|
||||||
default: true
|
|
||||||
path:
|
path:
|
||||||
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
|
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
|
||||||
clean:
|
clean:
|
||||||
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
|
||||||
filter:
|
|
||||||
description: >
|
|
||||||
Partially clone against a given filter.
|
|
||||||
Overrides sparse-checkout if set.
|
|
||||||
default: null
|
|
||||||
sparse-checkout:
|
|
||||||
description: >
|
|
||||||
Do a sparse checkout on given patterns.
|
|
||||||
Each pattern should be separated with new lines.
|
|
||||||
default: null
|
|
||||||
sparse-checkout-cone-mode:
|
|
||||||
description: >
|
|
||||||
Specifies whether to use cone-mode when doing a sparse checkout.
|
|
||||||
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
|
||||||
fetch-tags:
|
|
||||||
description: 'Whether to fetch tags, even if fetch-depth > 0.'
|
|
||||||
default: false
|
|
||||||
show-progress:
|
|
||||||
description: 'Whether to show progress status output when fetching.'
|
|
||||||
default: true
|
|
||||||
lfs:
|
lfs:
|
||||||
description: 'Whether to download Git-LFS files'
|
description: 'Whether to download Git-LFS files'
|
||||||
default: false
|
default: false
|
||||||
submodules:
|
|
||||||
description: >
|
|
||||||
Whether to checkout submodules: `true` to checkout submodules or `recursive` to
|
|
||||||
recursively checkout submodules.
|
|
||||||
|
|
||||||
|
|
||||||
When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are
|
|
||||||
converted to HTTPS.
|
|
||||||
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
|
|
||||||
github-server-url:
|
|
||||||
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
|
|
||||||
required: false
|
|
||||||
runs:
|
runs:
|
||||||
using: node20
|
using: node12
|
||||||
main: dist/index.js
|
main: dist/index.js
|
||||||
post: dist/index.js
|
post: dist/index.js
|
||||||
|
@ -1,290 +0,0 @@
|
|||||||
# ADR 0153: Checkout v2
|
|
||||||
|
|
||||||
**Date**: 2019-10-21
|
|
||||||
|
|
||||||
**Status**: Accepted
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
This ADR details the behavior for `actions/checkout@v2`.
|
|
||||||
|
|
||||||
The new action will be written in typescript. We are moving away from runner-plugin actions.
|
|
||||||
|
|
||||||
We want to take this opportunity to make behavioral changes, from v1. This document is scoped to those differences.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
### Inputs
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
repository:
|
|
||||||
description: 'Repository name with owner. For example, actions/checkout'
|
|
||||||
default: ${{ github.repository }}
|
|
||||||
ref:
|
|
||||||
description: >
|
|
||||||
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. Otherwise, uses the default branch.
|
|
||||||
token:
|
|
||||||
description: >
|
|
||||||
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
|
|
||||||
commands. The post-job step removes the PAT.
|
|
||||||
|
|
||||||
|
|
||||||
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 }}
|
|
||||||
ssh-key:
|
|
||||||
description: >
|
|
||||||
SSH key used to fetch the repository. The SSH key is configured with the local
|
|
||||||
git config, which enables your scripts to run authenticated git commands.
|
|
||||||
The post-job step removes the SSH key.
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
ssh-known-hosts:
|
|
||||||
description: >
|
|
||||||
Known hosts in addition to the user and global host key database. The public
|
|
||||||
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-strict:
|
|
||||||
description: >
|
|
||||||
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
|
|
||||||
persist-credentials:
|
|
||||||
description: 'Whether to configure the token or SSH key with the local git config'
|
|
||||||
default: true
|
|
||||||
path:
|
|
||||||
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
|
|
||||||
clean:
|
|
||||||
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
|
|
||||||
default: true
|
|
||||||
fetch-depth:
|
|
||||||
description: 'Number of commits to fetch. 0 indicates all history for all tags and branches.'
|
|
||||||
default: 1
|
|
||||||
lfs:
|
|
||||||
description: 'Whether to download Git-LFS files'
|
|
||||||
default: false
|
|
||||||
submodules:
|
|
||||||
description: >
|
|
||||||
Whether to checkout submodules: `true` to checkout submodules or `recursive` to
|
|
||||||
recursively checkout submodules.
|
|
||||||
|
|
||||||
|
|
||||||
When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are
|
|
||||||
converted to HTTPS.
|
|
||||||
default: false
|
|
||||||
```
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- SSH support is new
|
|
||||||
- `persist-credentials` is new
|
|
||||||
- `path` behavior is different (refer [below](#path) for details)
|
|
||||||
|
|
||||||
### Fallback to GitHub API
|
|
||||||
|
|
||||||
When a sufficient version of git is not in the PATH, fallback to the [web API](https://developer.github.com/v3/repos/contents/#get-archive-link) to download a tarball/zipball.
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- LFS files are not included in the archive. Therefore fail if LFS is set to true.
|
|
||||||
- Submodules are also not included in the archive.
|
|
||||||
|
|
||||||
### Persist credentials
|
|
||||||
|
|
||||||
The credentials will be persisted on disk. This will allow users to script authenticated git commands, like `git fetch`.
|
|
||||||
|
|
||||||
A post script will remove the credentials (cleanup for self-hosted).
|
|
||||||
|
|
||||||
Users may opt-out by specifying `persist-credentials: false`
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- Users scripting `git commit` may need to set the username and email. The service does not provide any reasonable default value. Users can add `git config user.name <NAME>` and `git config user.email <EMAIL>`. We will document this guidance.
|
|
||||||
|
|
||||||
#### PAT
|
|
||||||
|
|
||||||
When using the `${{github.token}}` or a PAT, the token will be persisted in the local git config. The config key `http.https://github.com/.extraheader` enables an auth header to be specified on all authenticated commands `AUTHORIZATION: basic <BASE64_U:P>`.
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- The auth header is scoped to all of github `http.https://github.com/.extraheader`
|
|
||||||
- Additional public remotes also just work.
|
|
||||||
- If users want to authenticate to an additional private remote, they should provide the `token` input.
|
|
||||||
|
|
||||||
#### SSH key
|
|
||||||
|
|
||||||
The SSH key will be written to disk under the `$RUNNER_TEMP` directory. The SSH key will
|
|
||||||
be removed by the action's post-job hook. Additionally, RUNNER_TEMP is cleared by the
|
|
||||||
runner between jobs.
|
|
||||||
|
|
||||||
The SSH key must be written with strict file permissions. The SSH client requires the file
|
|
||||||
to be read/write for the user, and not accessible by others.
|
|
||||||
|
|
||||||
The user host key database (`~/.ssh/known_hosts`) will be copied to a unique file under
|
|
||||||
`$RUNNER_TEMP`. And values from the input `ssh-known-hosts` will be added to the file.
|
|
||||||
|
|
||||||
The SSH command will be overridden for the local git config:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git config core.sshCommand 'ssh -i "$RUNNER_TEMP/path-to-ssh-key" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/path-to-known-hosts"'
|
|
||||||
```
|
|
||||||
|
|
||||||
When the input `ssh-strict` is set to `false`, the options `CheckHostIP` and `StrictHostKeyChecking` will not be overridden.
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- When `ssh-strict` is set to `true` (default), the SSH option `CheckHostIP` can safely be disabled.
|
|
||||||
Strict host checking verifies the server's public key. Therefore, IP verification is unnecessary
|
|
||||||
and noisy. For example:
|
|
||||||
> Warning: Permanently added the RSA host key for IP address '140.82.113.4' to the list of known hosts.
|
|
||||||
- Since GIT_SSH_COMMAND overrides core.sshCommand, temporarily set the env var when fetching the repo. When creds
|
|
||||||
are persisted, core.sshCommand is leveraged to avoid multiple checkout steps stomping over each other.
|
|
||||||
- Modify actions/runner to mount RUNNER_TEMP to enable scripting authenticated git commands from a container action.
|
|
||||||
- Refer [here](https://linux.die.net/man/5/ssh_config) for SSH config details.
|
|
||||||
|
|
||||||
### Fetch behavior
|
|
||||||
|
|
||||||
Fetch only the SHA being built and set depth=1. This significantly reduces the fetch time for large repos.
|
|
||||||
|
|
||||||
If a SHA isn't available (e.g. multi repo), then fetch only the specified ref with depth=1.
|
|
||||||
|
|
||||||
The input `fetch-depth` can be used to control the depth.
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- Fetching a single commit is supported by Git wire protocol version 2. The git client uses protocol version 0 by default. The desired protocol version can be overridden in the git config or on the fetch command line invocation (`-c protocol.version=2`). We will override on the fetch command line, for transparency.
|
|
||||||
- Git client version 2.18+ (released June 2018) is required for wire protocol version 2.
|
|
||||||
|
|
||||||
### Checkout behavior
|
|
||||||
|
|
||||||
For CI, checkout will create a local ref with the upstream set. This allows users to script git as they normally would.
|
|
||||||
|
|
||||||
For PR, continue to checkout detached head. The PR branch is special - the branch and merge commit are created by the server. It doesn't match a users' local workflow.
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- Consider deleting all local refs during cleanup if that helps avoid collisions. More testing required.
|
|
||||||
|
|
||||||
### Path
|
|
||||||
|
|
||||||
For the mainline scenario, the disk-layout behavior remains the same.
|
|
||||||
|
|
||||||
Remember, given the repo `johndoe/foo`, the mainline disk layout looks like:
|
|
||||||
|
|
||||||
```
|
|
||||||
GITHUB_WORKSPACE=/home/runner/work/foo/foo
|
|
||||||
RUNNER_WORKSPACE=/home/runner/work/foo
|
|
||||||
```
|
|
||||||
|
|
||||||
V2 introduces a new constraint on the checkout path. The location must now be under `github.workspace`. Whereas the checkout@v1 constraint was one level up, under `runner.workspace`.
|
|
||||||
|
|
||||||
V2 no longer changes `github.workspace` to follow wherever the self repo is checked-out.
|
|
||||||
|
|
||||||
These behavioral changes align better with container actions. The [documented filesystem contract](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/virtual-environments-for-github-hosted-runners#docker-container-filesystem) is:
|
|
||||||
|
|
||||||
- `/github/home`
|
|
||||||
- `/github/workspace` - Note: GitHub Actions must be run by the default Docker user (root). Ensure your Dockerfile does not set the USER instruction, otherwise you will not be able to access `GITHUB_WORKSPACE`.
|
|
||||||
- `/github/workflow`
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- The tracking config will not be updated to reflect the path of the workflow repo.
|
|
||||||
- Any existing workflow repo will not be moved when the checkout path changes. In fact some customers want to checkout the workflow repo twice, side by side against different branches.
|
|
||||||
- Actions that need to operate only against the root of the self repo, should expose a `path` input.
|
|
||||||
|
|
||||||
#### Default value for `path` input
|
|
||||||
|
|
||||||
The `path` input will default to `./` which is rooted against `github.workspace`.
|
|
||||||
|
|
||||||
This default fits the mainline scenario well: single checkout
|
|
||||||
|
|
||||||
For multi-checkout, users must specify the `path` input for at least one of the repositories.
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- An alternative is for the self repo to default to `./` and other repos default to `<REPO_NAME>`. However nested layout is an atypical git layout and therefore is not a good default. Users should supply the path info.
|
|
||||||
|
|
||||||
#### Example - Nested layout
|
|
||||||
|
|
||||||
The following example checks-out two repositories and creates a nested layout.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Self repo - Checkout to $GITHUB_WORKSPACE
|
|
||||||
- uses: checkout@v2
|
|
||||||
|
|
||||||
# Other repo - Checkout to $GITHUB_WORKSPACE/myscripts
|
|
||||||
- uses: checkout@v2
|
|
||||||
with:
|
|
||||||
repository: myorg/myscripts
|
|
||||||
path: myscripts
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example - Side by side layout
|
|
||||||
|
|
||||||
The following example checks-out two repositories and creates a side-by-side layout.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Self repo - Checkout to $GITHUB_WORKSPACE/foo
|
|
||||||
- uses: checkout@v2
|
|
||||||
with:
|
|
||||||
path: foo
|
|
||||||
|
|
||||||
# Other repo - Checkout to $GITHUB_WORKSPACE/myscripts
|
|
||||||
- uses: checkout@v2
|
|
||||||
with:
|
|
||||||
repository: myorg/myscripts
|
|
||||||
path: myscripts
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Path impact to problem matchers
|
|
||||||
|
|
||||||
Problem matchers associate the source files with annotations.
|
|
||||||
|
|
||||||
Today the runner verifies the source file is under the `github.workspace`. Otherwise the source file property is dropped.
|
|
||||||
|
|
||||||
Multi-checkout complicates the matter. However even today submodules may cause this heuristic to be inaccurate.
|
|
||||||
|
|
||||||
A better solution is:
|
|
||||||
|
|
||||||
Given a source file path, walk up the directories until the first `.git/config` is found. Check if it matches the self repo (`url = https://github.com/OWNER/REPO`). If not, drop the source file path.
|
|
||||||
|
|
||||||
### Submodules
|
|
||||||
|
|
||||||
With both PAT and SSH key support, we should be able to provide frictionless support for
|
|
||||||
submodules scenarios: recursive, non-recursive, relative submodule paths.
|
|
||||||
|
|
||||||
When fetching submodules, follow the `fetch-depth` settings.
|
|
||||||
|
|
||||||
Also when fetching submodules, if the `ssh-key` input is not provided then convert SSH URLs to HTTPS: `-c url."https://github.com/".insteadOf "git@github.com:"`
|
|
||||||
|
|
||||||
Credentials will be persisted in the submodules local git config too.
|
|
||||||
|
|
||||||
### Port to typescript
|
|
||||||
|
|
||||||
The checkout action should be a typescript action on the GitHub graph, for the following reasons:
|
|
||||||
- Enables customers to fork the checkout repo and modify
|
|
||||||
- Serves as an example for customers
|
|
||||||
- Demystifies the checkout action manifest
|
|
||||||
- Simplifies the runner
|
|
||||||
- Reduce the amount of runner code to port (if we ever do)
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- This means job-container images will need git in the PATH, for checkout.
|
|
||||||
|
|
||||||
### Branching strategy and release tags
|
|
||||||
|
|
||||||
- Create a servicing branch for V1: `releases/v1`
|
|
||||||
- Merge the changes into the default branch
|
|
||||||
- Release using a new tag `preview`
|
|
||||||
- When stable, release using a new tag `v2`
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- Update the checkout action and readme
|
|
||||||
- Update samples to consume `actions/checkout@v2`
|
|
||||||
- Job containers now require git in the PATH for checkout, otherwise fallback to REST API
|
|
||||||
- Minimum git version 2.18
|
|
||||||
- Update problem matcher logic regarding source file verification (runner)
|
|
48157
dist/index.js
vendored
48157
dist/index.js
vendored
File diff suppressed because one or more lines are too long
@ -1,12 +0,0 @@
|
|||||||
# Defines the test-ubuntu-git Container Image.
|
|
||||||
# Consumed by actions/checkout CI/CD validation workflows.
|
|
||||||
|
|
||||||
FROM ubuntu:latest
|
|
||||||
|
|
||||||
RUN apt update
|
|
||||||
RUN apt install -y git
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.title="Ubuntu + git (validation image)"
|
|
||||||
LABEL org.opencontainers.image.description="Ubuntu image with git pre-installed. Intended primarily for testing `actions/checkout` during CI/CD workflows."
|
|
||||||
LABEL org.opencontainers.image.documentation="https://github.com/actions/checkout/tree/main/images/test-ubuntu-git.md"
|
|
||||||
LABEL org.opencontainers.image.licenses=MIT
|
|
@ -1,15 +0,0 @@
|
|||||||
# `test-ubuntu-git` Container Image
|
|
||||||
|
|
||||||
[![Publish test-ubuntu-git Container](https://github.com/actions/checkout/actions/workflows/update-test-ubuntu-git.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/update-test-ubuntu-git.yml)
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
`test-ubuntu-git` is a container image hosted on the GitHub Container Registry, `ghcr.io`.
|
|
||||||
|
|
||||||
It is intended primarily for testing the [`actions/checkout` repository](https://github.com/actions/checkout) as part of `actions/checkout`'s CI/CD workflows.
|
|
||||||
|
|
||||||
The composition of `test-ubuntu-git` is intentionally minimal. It is comprised of [git](https://git-scm.com/) installed on top of a [base-level ubuntu image](https://hub.docker.com/_/ubuntu/tags).
|
|
||||||
|
|
||||||
# License
|
|
||||||
|
|
||||||
`test-ubuntu-git` is released under the [MIT License](/LICENSE).
|
|
@ -1,6 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
fakeTimers: {},
|
|
||||||
moduleFileExtensions: ['js', 'ts'],
|
moduleFileExtensions: ['js', 'ts'],
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
testMatch: ['**/*.test.ts'],
|
testMatch: ['**/*.test.ts'],
|
||||||
|
12809
package-lock.json
generated
12809
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
54
package.json
54
package.json
@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "checkout",
|
"name": "checkout",
|
||||||
"version": "4.1.4",
|
"version": "2.0.0",
|
||||||
"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",
|
||||||
"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",
|
||||||
|
"pack": "ncc build",
|
||||||
|
"gendocs": "node lib/misc/generate-docs.js",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"licensed-check": "src/misc/licensed-check.sh",
|
"all": "npm run build && npm run format && npm run lint && npm run pack && npm run gendocs && npm test"
|
||||||
"licensed-generate": "src/misc/licensed-generate.sh"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -28,28 +29,27 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/actions/checkout#readme",
|
"homepage": "https://github.com/actions/checkout#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.1.3",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.0.1",
|
||||||
"@actions/github": "^6.0.0",
|
"@actions/github": "^2.0.0",
|
||||||
"@actions/io": "^1.1.3",
|
"@actions/io": "^1.0.1",
|
||||||
"@actions/tool-cache": "^2.0.1",
|
"@actions/tool-cache": "^1.1.2",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^3.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^24.0.23",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^12.7.12",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^3.4.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
"@typescript-eslint/parser": "^2.8.0",
|
||||||
"@typescript-eslint/parser": "^7.7.1",
|
"@zeit/ncc": "^0.20.5",
|
||||||
"@vercel/ncc": "^0.38.1",
|
"eslint": "^5.16.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint-plugin-github": "^2.0.0",
|
||||||
"eslint-plugin-github": "^4.10.2",
|
"eslint-plugin-jest": "^22.21.0",
|
||||||
"eslint-plugin-jest": "^28.2.0",
|
"jest": "^24.9.0",
|
||||||
"jest": "^29.7.0",
|
"jest-circus": "^24.9.0",
|
||||||
"jest-circus": "^29.7.0",
|
"js-yaml": "^3.13.1",
|
||||||
"js-yaml": "^4.1.0",
|
"prettier": "^1.19.1",
|
||||||
"prettier": "^3.2.5",
|
"ts-jest": "^24.2.0",
|
||||||
"ts-jest": "^29.1.2",
|
"typescript": "^3.6.4"
|
||||||
"typescript": "^5.4.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,9 +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: ${
|
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
|
||||||
(error as any)?.message ?? error
|
|
||||||
}`
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,14 +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: ${
|
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
|
||||||
(error as any)?.message ?? error
|
|
||||||
}`
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,14 +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: ${
|
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
|
||||||
(error as any)?.message ?? error
|
|
||||||
}`
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,374 +0,0 @@
|
|||||||
import * as assert from 'assert'
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
import * as exec from '@actions/exec'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
import * as io from '@actions/io'
|
|
||||||
import * as os from 'os'
|
|
||||||
import * as path from 'path'
|
|
||||||
import * as regexpHelper from './regexp-helper'
|
|
||||||
import * as stateHelper from './state-helper'
|
|
||||||
import * as urlHelper from './url-helper'
|
|
||||||
import {v4 as uuid} from 'uuid'
|
|
||||||
import {IGitCommandManager} from './git-command-manager'
|
|
||||||
import {IGitSourceSettings} from './git-source-settings'
|
|
||||||
|
|
||||||
const IS_WINDOWS = process.platform === 'win32'
|
|
||||||
const SSH_COMMAND_KEY = 'core.sshCommand'
|
|
||||||
|
|
||||||
export interface IGitAuthHelper {
|
|
||||||
configureAuth(): Promise<void>
|
|
||||||
configureGlobalAuth(): Promise<void>
|
|
||||||
configureSubmoduleAuth(): Promise<void>
|
|
||||||
configureTempGlobalConfig(): Promise<string>
|
|
||||||
removeAuth(): Promise<void>
|
|
||||||
removeGlobalConfig(): Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAuthHelper(
|
|
||||||
git: IGitCommandManager,
|
|
||||||
settings?: IGitSourceSettings
|
|
||||||
): IGitAuthHelper {
|
|
||||||
return new GitAuthHelper(git, settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
class GitAuthHelper {
|
|
||||||
private readonly git: IGitCommandManager
|
|
||||||
private readonly settings: IGitSourceSettings
|
|
||||||
private readonly tokenConfigKey: string
|
|
||||||
private readonly tokenConfigValue: string
|
|
||||||
private readonly tokenPlaceholderConfigValue: string
|
|
||||||
private readonly insteadOfKey: string
|
|
||||||
private readonly insteadOfValues: string[] = []
|
|
||||||
private sshCommand = ''
|
|
||||||
private sshKeyPath = ''
|
|
||||||
private sshKnownHostsPath = ''
|
|
||||||
private temporaryHomePath = ''
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
gitCommandManager: IGitCommandManager,
|
|
||||||
gitSourceSettings: IGitSourceSettings | undefined
|
|
||||||
) {
|
|
||||||
this.git = gitCommandManager
|
|
||||||
this.settings = gitSourceSettings || ({} as unknown as IGitSourceSettings)
|
|
||||||
|
|
||||||
// Token auth header
|
|
||||||
const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl)
|
|
||||||
this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader` // "origin" is SCHEME://HOSTNAME[:PORT]
|
|
||||||
const basicCredential = Buffer.from(
|
|
||||||
`x-access-token:${this.settings.authToken}`,
|
|
||||||
'utf8'
|
|
||||||
).toString('base64')
|
|
||||||
core.setSecret(basicCredential)
|
|
||||||
this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***`
|
|
||||||
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> {
|
|
||||||
// Remove possible previous values
|
|
||||||
await this.removeAuth()
|
|
||||||
|
|
||||||
// Configure new values
|
|
||||||
await this.configureSsh()
|
|
||||||
await this.configureToken()
|
|
||||||
}
|
|
||||||
|
|
||||||
async configureTempGlobalConfig(): Promise<string> {
|
|
||||||
// Already setup global config
|
|
||||||
if (this.temporaryHomePath?.length > 0) {
|
|
||||||
return path.join(this.temporaryHomePath, '.gitconfig')
|
|
||||||
}
|
|
||||||
// Create a temp home directory
|
|
||||||
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
|
||||||
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
|
||||||
const uniqueId = uuid()
|
|
||||||
this.temporaryHomePath = path.join(runnerTemp, uniqueId)
|
|
||||||
await fs.promises.mkdir(this.temporaryHomePath, {recursive: true})
|
|
||||||
|
|
||||||
// Copy the global git config
|
|
||||||
const gitConfigPath = path.join(
|
|
||||||
process.env['HOME'] || os.homedir(),
|
|
||||||
'.gitconfig'
|
|
||||||
)
|
|
||||||
const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig')
|
|
||||||
let configExists = false
|
|
||||||
try {
|
|
||||||
await fs.promises.stat(gitConfigPath)
|
|
||||||
configExists = true
|
|
||||||
} catch (err) {
|
|
||||||
if ((err as any)?.code !== 'ENOENT') {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (configExists) {
|
|
||||||
core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`)
|
|
||||||
await io.cp(gitConfigPath, newGitConfigPath)
|
|
||||||
} else {
|
|
||||||
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 {
|
|
||||||
// Configure the token
|
|
||||||
await this.configureToken(newGitConfigPath, true)
|
|
||||||
|
|
||||||
// Configure HTTPS instead of SSH
|
|
||||||
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
|
||||||
if (!this.settings.sshKey) {
|
|
||||||
for (const insteadOfValue of this.insteadOfValues) {
|
|
||||||
await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Unset in case somehow written to the real global config
|
|
||||||
core.info(
|
|
||||||
'Encountered an error when attempting to configure token. Attempting unconfigure.'
|
|
||||||
)
|
|
||||||
await this.git.tryConfigUnset(this.tokenConfigKey, true)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async configureSubmoduleAuth(): Promise<void> {
|
|
||||||
// Remove possible previous HTTPS instead of SSH
|
|
||||||
await this.removeGitConfig(this.insteadOfKey, true)
|
|
||||||
|
|
||||||
if (this.settings.persistCredentials) {
|
|
||||||
// Configure a placeholder value. This approach avoids the credential being captured
|
|
||||||
// by process creation audit events, which are commonly logged. For more information,
|
|
||||||
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
|
||||||
const output = await this.git.submoduleForeach(
|
|
||||||
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
|
|
||||||
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
|
|
||||||
this.settings.nestedSubmodules
|
|
||||||
)
|
|
||||||
|
|
||||||
// Replace the placeholder
|
|
||||||
const configPaths: string[] =
|
|
||||||
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
|
|
||||||
for (const configPath of configPaths) {
|
|
||||||
core.debug(`Replacing token placeholder in '${configPath}'`)
|
|
||||||
await this.replaceTokenPlaceholder(configPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.settings.sshKey) {
|
|
||||||
// Configure core.sshCommand
|
|
||||||
await this.git.submoduleForeach(
|
|
||||||
`git config --local '${SSH_COMMAND_KEY}' '${this.sshCommand}'`,
|
|
||||||
this.settings.nestedSubmodules
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Configure HTTPS instead of SSH
|
|
||||||
for (const insteadOfValue of this.insteadOfValues) {
|
|
||||||
await this.git.submoduleForeach(
|
|
||||||
`git config --local --add '${this.insteadOfKey}' '${insteadOfValue}'`,
|
|
||||||
this.settings.nestedSubmodules
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeAuth(): Promise<void> {
|
|
||||||
await this.removeSsh()
|
|
||||||
await this.removeToken()
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeGlobalConfig(): Promise<void> {
|
|
||||||
if (this.temporaryHomePath?.length > 0) {
|
|
||||||
core.debug(`Unsetting HOME override`)
|
|
||||||
this.git.removeEnvironmentVariable('HOME')
|
|
||||||
await io.rmRF(this.temporaryHomePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async configureSsh(): Promise<void> {
|
|
||||||
if (!this.settings.sshKey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write key
|
|
||||||
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
|
||||||
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
|
||||||
const uniqueId = uuid()
|
|
||||||
this.sshKeyPath = path.join(runnerTemp, uniqueId)
|
|
||||||
stateHelper.setSshKeyPath(this.sshKeyPath)
|
|
||||||
await fs.promises.mkdir(runnerTemp, {recursive: true})
|
|
||||||
await fs.promises.writeFile(
|
|
||||||
this.sshKeyPath,
|
|
||||||
this.settings.sshKey.trim() + '\n',
|
|
||||||
{mode: 0o600}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Remove inherited permissions on Windows
|
|
||||||
if (IS_WINDOWS) {
|
|
||||||
const icacls = await io.which('icacls.exe')
|
|
||||||
await exec.exec(
|
|
||||||
`"${icacls}" "${this.sshKeyPath}" /grant:r "${process.env['USERDOMAIN']}\\${process.env['USERNAME']}:F"`
|
|
||||||
)
|
|
||||||
await exec.exec(`"${icacls}" "${this.sshKeyPath}" /inheritance:r`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write known hosts
|
|
||||||
const userKnownHostsPath = path.join(os.homedir(), '.ssh', 'known_hosts')
|
|
||||||
let userKnownHosts = ''
|
|
||||||
try {
|
|
||||||
userKnownHosts = (
|
|
||||||
await fs.promises.readFile(userKnownHostsPath)
|
|
||||||
).toString()
|
|
||||||
} catch (err) {
|
|
||||||
if ((err as any)?.code !== 'ENOENT') {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let knownHosts = ''
|
|
||||||
if (userKnownHosts) {
|
|
||||||
knownHosts += `# Begin from ${userKnownHostsPath}\n${userKnownHosts}\n# End from ${userKnownHostsPath}\n`
|
|
||||||
}
|
|
||||||
if (this.settings.sshKnownHosts) {
|
|
||||||
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`
|
|
||||||
this.sshKnownHostsPath = path.join(runnerTemp, `${uniqueId}_known_hosts`)
|
|
||||||
stateHelper.setSshKnownHostsPath(this.sshKnownHostsPath)
|
|
||||||
await fs.promises.writeFile(this.sshKnownHostsPath, knownHosts)
|
|
||||||
|
|
||||||
// Configure GIT_SSH_COMMAND
|
|
||||||
const sshPath = await io.which('ssh', true)
|
|
||||||
this.sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
|
|
||||||
this.sshKeyPath
|
|
||||||
)}"`
|
|
||||||
if (this.settings.sshStrict) {
|
|
||||||
this.sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no'
|
|
||||||
}
|
|
||||||
this.sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
|
|
||||||
this.sshKnownHostsPath
|
|
||||||
)}"`
|
|
||||||
core.info(`Temporarily overriding GIT_SSH_COMMAND=${this.sshCommand}`)
|
|
||||||
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand)
|
|
||||||
|
|
||||||
// Configure core.sshCommand
|
|
||||||
if (this.settings.persistCredentials) {
|
|
||||||
await this.git.config(SSH_COMMAND_KEY, this.sshCommand)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async configureToken(
|
|
||||||
configPath?: string,
|
|
||||||
globalConfig?: boolean
|
|
||||||
): Promise<void> {
|
|
||||||
// Validate args
|
|
||||||
assert.ok(
|
|
||||||
(configPath && globalConfig) || (!configPath && !globalConfig),
|
|
||||||
'Unexpected configureToken parameter combinations'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Default config path
|
|
||||||
if (!configPath && !globalConfig) {
|
|
||||||
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure a placeholder value. This approach avoids the credential being captured
|
|
||||||
// by process creation audit events, which are commonly logged. For more information,
|
|
||||||
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
|
||||||
await this.git.config(
|
|
||||||
this.tokenConfigKey,
|
|
||||||
this.tokenPlaceholderConfigValue,
|
|
||||||
globalConfig
|
|
||||||
)
|
|
||||||
|
|
||||||
// Replace the placeholder
|
|
||||||
await this.replaceTokenPlaceholder(configPath || '')
|
|
||||||
}
|
|
||||||
|
|
||||||
private async replaceTokenPlaceholder(configPath: string): Promise<void> {
|
|
||||||
assert.ok(configPath, 'configPath is not defined')
|
|
||||||
let content = (await fs.promises.readFile(configPath)).toString()
|
|
||||||
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
|
|
||||||
if (
|
|
||||||
placeholderIndex < 0 ||
|
|
||||||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
|
|
||||||
) {
|
|
||||||
throw new Error(`Unable to replace auth placeholder in ${configPath}`)
|
|
||||||
}
|
|
||||||
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
|
|
||||||
content = content.replace(
|
|
||||||
this.tokenPlaceholderConfigValue,
|
|
||||||
this.tokenConfigValue
|
|
||||||
)
|
|
||||||
await fs.promises.writeFile(configPath, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async removeSsh(): Promise<void> {
|
|
||||||
// SSH key
|
|
||||||
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
|
|
||||||
if (keyPath) {
|
|
||||||
try {
|
|
||||||
await io.rmRF(keyPath)
|
|
||||||
} catch (err) {
|
|
||||||
core.debug(`${(err as any)?.message ?? err}`)
|
|
||||||
core.warning(`Failed to remove SSH key '${keyPath}'`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SSH known hosts
|
|
||||||
const knownHostsPath =
|
|
||||||
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
|
|
||||||
if (knownHostsPath) {
|
|
||||||
try {
|
|
||||||
await io.rmRF(knownHostsPath)
|
|
||||||
} catch {
|
|
||||||
// Intentionally empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SSH command
|
|
||||||
await this.removeGitConfig(SSH_COMMAND_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async removeToken(): Promise<void> {
|
|
||||||
// HTTP extra header
|
|
||||||
await this.removeGitConfig(this.tokenConfigKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async removeGitConfig(
|
|
||||||
configKey: string,
|
|
||||||
submoduleOnly: boolean = false
|
|
||||||
): Promise<void> {
|
|
||||||
if (!submoduleOnly) {
|
|
||||||
if (
|
|
||||||
(await this.git.configExists(configKey)) &&
|
|
||||||
!(await this.git.tryConfigUnset(configKey))
|
|
||||||
) {
|
|
||||||
// Load the config contents
|
|
||||||
core.warning(`Failed to remove '${configKey}' from the git config`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pattern = regexpHelper.escape(configKey)
|
|
||||||
await this.git.submoduleForeach(
|
|
||||||
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
|
|
||||||
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,80 +1,44 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as exec from '@actions/exec'
|
import * as exec from '@actions/exec'
|
||||||
import * as fs from 'fs'
|
|
||||||
import * as 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 retryHelper from './retry-helper'
|
import * as retryHelper from './retry-helper'
|
||||||
import {GitVersion} from './git-version'
|
import {GitVersion} from './git-version'
|
||||||
|
|
||||||
// Auth header not supported before 2.9
|
// Auth header not supported before 2.9
|
||||||
// Wire protocol v2 not supported before 2.18
|
// Wire protocol v2 not supported before 2.18
|
||||||
// sparse-checkout not [well-]supported before 2.28 (see https://github.com/actions/checkout/issues/1386)
|
|
||||||
export const MinimumGitVersion = new GitVersion('2.18')
|
export const MinimumGitVersion = new GitVersion('2.18')
|
||||||
export const MinimumGitSparseCheckoutVersion = new GitVersion('2.28')
|
|
||||||
|
|
||||||
export interface IGitCommandManager {
|
export interface IGitCommandManager {
|
||||||
branchDelete(remote: boolean, branch: string): Promise<void>
|
branchDelete(remote: boolean, branch: string): Promise<void>
|
||||||
branchExists(remote: boolean, pattern: string): Promise<boolean>
|
branchExists(remote: boolean, pattern: string): Promise<boolean>
|
||||||
branchList(remote: boolean): Promise<string[]>
|
branchList(remote: boolean): Promise<string[]>
|
||||||
disableSparseCheckout(): Promise<void>
|
|
||||||
sparseCheckout(sparseCheckout: string[]): Promise<void>
|
|
||||||
sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void>
|
|
||||||
checkout(ref: string, startPoint: string): Promise<void>
|
checkout(ref: string, startPoint: string): Promise<void>
|
||||||
checkoutDetach(): Promise<void>
|
checkoutDetach(): Promise<void>
|
||||||
config(
|
config(configKey: string, configValue: string): Promise<void>
|
||||||
configKey: string,
|
configExists(configKey: string): Promise<boolean>
|
||||||
configValue: string,
|
fetch(fetchDepth: number, refSpec: string[]): Promise<void>
|
||||||
globalConfig?: boolean,
|
|
||||||
add?: boolean
|
|
||||||
): Promise<void>
|
|
||||||
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
|
|
||||||
fetch(
|
|
||||||
refSpec: string[],
|
|
||||||
options: {
|
|
||||||
filter?: string
|
|
||||||
fetchDepth?: number
|
|
||||||
fetchTags?: boolean
|
|
||||||
showProgress?: boolean
|
|
||||||
}
|
|
||||||
): 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
|
|
||||||
revParse(ref: string): Promise<string>
|
|
||||||
setEnvironmentVariable(name: string, value: string): void
|
|
||||||
shaExists(sha: string): Promise<boolean>
|
|
||||||
submoduleForeach(command: string, recursive: boolean): Promise<string>
|
|
||||||
submoduleSync(recursive: boolean): Promise<void>
|
|
||||||
submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void>
|
|
||||||
submoduleStatus(): Promise<boolean>
|
|
||||||
tagExists(pattern: string): Promise<boolean>
|
tagExists(pattern: string): Promise<boolean>
|
||||||
tryClean(): Promise<boolean>
|
tryClean(): Promise<boolean>
|
||||||
tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
|
tryConfigUnset(configKey: string): Promise<boolean>
|
||||||
tryDisableAutomaticGarbageCollection(): Promise<boolean>
|
tryDisableAutomaticGarbageCollection(): Promise<boolean>
|
||||||
tryGetFetchUrl(): Promise<string>
|
tryGetFetchUrl(): Promise<string>
|
||||||
tryReset(): Promise<boolean>
|
tryReset(): Promise<boolean>
|
||||||
version(): Promise<GitVersion>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createCommandManager(
|
export async function CreateCommandManager(
|
||||||
workingDirectory: string,
|
workingDirectory: string,
|
||||||
lfs: boolean,
|
lfs: boolean
|
||||||
doSparseCheckout: boolean
|
|
||||||
): Promise<IGitCommandManager> {
|
): Promise<IGitCommandManager> {
|
||||||
return await GitCommandManager.createCommandManager(
|
return await GitCommandManager.createCommandManager(workingDirectory, lfs)
|
||||||
workingDirectory,
|
|
||||||
lfs,
|
|
||||||
doSparseCheckout
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class GitCommandManager {
|
class GitCommandManager {
|
||||||
@ -84,9 +48,7 @@ class GitCommandManager {
|
|||||||
}
|
}
|
||||||
private gitPath = ''
|
private gitPath = ''
|
||||||
private lfs = false
|
private lfs = false
|
||||||
private doSparseCheckout = false
|
|
||||||
private workingDirectory = ''
|
private workingDirectory = ''
|
||||||
private gitVersion: GitVersion = new GitVersion()
|
|
||||||
|
|
||||||
// Private constructor; use createCommandManager()
|
// Private constructor; use createCommandManager()
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
@ -115,94 +77,28 @@ class GitCommandManager {
|
|||||||
async branchList(remote: boolean): Promise<string[]> {
|
async branchList(remote: boolean): Promise<string[]> {
|
||||||
const result: string[] = []
|
const result: string[] = []
|
||||||
|
|
||||||
// Note, this implementation uses "rev-parse --symbolic-full-name" because the output from
|
// Note, this implementation uses "rev-parse --symbolic" because the output from
|
||||||
// "branch --list" is more difficult when in a detached HEAD state.
|
// "branch --list" is more difficult when in a detached HEAD state.
|
||||||
|
|
||||||
// TODO(https://github.com/actions/checkout/issues/786): this implementation uses
|
const args = ['rev-parse', '--symbolic']
|
||||||
// "rev-parse --symbolic-full-name" because there is a bug
|
|
||||||
// in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. When
|
|
||||||
// 2.18 is no longer supported, we can switch back to --symbolic.
|
|
||||||
|
|
||||||
const args = ['rev-parse', '--symbolic-full-name']
|
|
||||||
if (remote) {
|
if (remote) {
|
||||||
args.push('--remotes=origin')
|
args.push('--remotes=origin')
|
||||||
} else {
|
} else {
|
||||||
args.push('--branches')
|
args.push('--branches')
|
||||||
}
|
}
|
||||||
|
|
||||||
const stderr: string[] = []
|
const output = await this.execGit(args)
|
||||||
const errline: string[] = []
|
|
||||||
const stdout: string[] = []
|
|
||||||
const stdline: string[] = []
|
|
||||||
|
|
||||||
const listeners = {
|
for (let branch of output.stdout.trim().split('\n')) {
|
||||||
stderr: (data: Buffer) => {
|
|
||||||
stderr.push(data.toString())
|
|
||||||
},
|
|
||||||
errline: (data: Buffer) => {
|
|
||||||
errline.push(data.toString())
|
|
||||||
},
|
|
||||||
stdout: (data: Buffer) => {
|
|
||||||
stdout.push(data.toString())
|
|
||||||
},
|
|
||||||
stdline: (data: Buffer) => {
|
|
||||||
stdline.push(data.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress the output in order to avoid flooding annotations with innocuous errors.
|
|
||||||
await this.execGit(args, false, true, listeners)
|
|
||||||
|
|
||||||
core.debug(`stderr callback is: ${stderr}`)
|
|
||||||
core.debug(`errline callback is: ${errline}`)
|
|
||||||
core.debug(`stdout callback is: ${stdout}`)
|
|
||||||
core.debug(`stdline callback is: ${stdline}`)
|
|
||||||
|
|
||||||
for (let branch of stdline) {
|
|
||||||
branch = branch.trim()
|
branch = branch.trim()
|
||||||
if (!branch) {
|
if (branch) {
|
||||||
continue
|
result.push(branch)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (branch.startsWith('refs/heads/')) {
|
|
||||||
branch = branch.substring('refs/heads/'.length)
|
|
||||||
} else if (branch.startsWith('refs/remotes/')) {
|
|
||||||
branch = branch.substring('refs/remotes/'.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(branch)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async disableSparseCheckout(): Promise<void> {
|
|
||||||
await this.execGit(['sparse-checkout', 'disable'])
|
|
||||||
// Disabling 'sparse-checkout` leaves behind an undesirable side-effect in config (even in a pristine environment).
|
|
||||||
await this.tryConfigUnset('extensions.worktreeConfig', false)
|
|
||||||
}
|
|
||||||
|
|
||||||
async sparseCheckout(sparseCheckout: string[]): Promise<void> {
|
|
||||||
await this.execGit(['sparse-checkout', 'set', ...sparseCheckout])
|
|
||||||
}
|
|
||||||
|
|
||||||
async sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void> {
|
|
||||||
await this.execGit(['config', 'core.sparseCheckout', 'true'])
|
|
||||||
const output = await this.execGit([
|
|
||||||
'rev-parse',
|
|
||||||
'--git-path',
|
|
||||||
'info/sparse-checkout'
|
|
||||||
])
|
|
||||||
const sparseCheckoutPath = path.join(
|
|
||||||
this.workingDirectory,
|
|
||||||
output.stdout.trimRight()
|
|
||||||
)
|
|
||||||
await fs.promises.appendFile(
|
|
||||||
sparseCheckoutPath,
|
|
||||||
`\n${sparseCheckout.join('\n')}\n`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkout(ref: string, startPoint: string): Promise<void> {
|
async checkout(ref: string, startPoint: string): Promise<void> {
|
||||||
const args = ['checkout', '--progress', '--force']
|
const args = ['checkout', '--progress', '--force']
|
||||||
if (startPoint) {
|
if (startPoint) {
|
||||||
@ -219,63 +115,33 @@ class GitCommandManager {
|
|||||||
await this.execGit(args)
|
await this.execGit(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
async config(
|
async config(configKey: string, configValue: string): Promise<void> {
|
||||||
configKey: string,
|
await this.execGit(['config', configKey, configValue])
|
||||||
configValue: string,
|
|
||||||
globalConfig?: boolean,
|
|
||||||
add?: boolean
|
|
||||||
): Promise<void> {
|
|
||||||
const args: string[] = ['config', globalConfig ? '--global' : '--local']
|
|
||||||
if (add) {
|
|
||||||
args.push('--add')
|
|
||||||
}
|
|
||||||
args.push(...[configKey, configValue])
|
|
||||||
await this.execGit(args)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async configExists(
|
async configExists(configKey: string): Promise<boolean> {
|
||||||
configKey: string,
|
const pattern = configKey.replace(/[^a-zA-Z0-9_]/g, x => {
|
||||||
globalConfig?: boolean
|
return `\\${x}`
|
||||||
): Promise<boolean> {
|
})
|
||||||
const pattern = regexpHelper.escape(configKey)
|
|
||||||
const output = await this.execGit(
|
const output = await this.execGit(
|
||||||
[
|
['config', '--name-only', '--get-regexp', pattern],
|
||||||
'config',
|
|
||||||
globalConfig ? '--global' : '--local',
|
|
||||||
'--name-only',
|
|
||||||
'--get-regexp',
|
|
||||||
pattern
|
|
||||||
],
|
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
return output.exitCode === 0
|
return output.exitCode === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch(
|
async fetch(fetchDepth: number, refSpec: string[]): Promise<void> {
|
||||||
refSpec: string[],
|
const args = [
|
||||||
options: {
|
'-c',
|
||||||
filter?: string
|
'protocol.version=2',
|
||||||
fetchDepth?: number
|
'fetch',
|
||||||
fetchTags?: boolean
|
'--no-tags',
|
||||||
showProgress?: boolean
|
'--prune',
|
||||||
}
|
'--progress',
|
||||||
): Promise<void> {
|
'--no-recurse-submodules'
|
||||||
const args = ['-c', 'protocol.version=2', 'fetch']
|
]
|
||||||
if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
|
if (fetchDepth > 0) {
|
||||||
args.push('--no-tags')
|
args.push(`--depth=${fetchDepth}`)
|
||||||
}
|
|
||||||
|
|
||||||
args.push('--prune', '--no-recurse-submodules')
|
|
||||||
if (options.showProgress) {
|
|
||||||
args.push('--progress')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.filter) {
|
|
||||||
args.push(`--filter=${options.filter}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.fetchDepth && options.fetchDepth > 0) {
|
|
||||||
args.push(`--depth=${options.fetchDepth}`)
|
|
||||||
} else if (
|
} else if (
|
||||||
fshelper.fileExistsSync(
|
fshelper.fileExistsSync(
|
||||||
path.join(this.workingDirectory, '.git', 'shallow')
|
path.join(this.workingDirectory, '.git', 'shallow')
|
||||||
@ -295,34 +161,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
|
||||||
}
|
}
|
||||||
@ -332,12 +170,12 @@ class GitCommandManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isDetached(): Promise<boolean> {
|
async isDetached(): Promise<boolean> {
|
||||||
// Note, "branch --show-current" would be simpler but isn't available until Git 2.22
|
// Note, this implementation uses "branch --show-current" because
|
||||||
const output = await this.execGit(
|
// "rev-parse --symbolic-full-name HEAD" can fail on a new repo
|
||||||
['rev-parse', '--symbolic-full-name', '--verify', '--quiet', 'HEAD'],
|
// with nothing checked out.
|
||||||
true
|
|
||||||
)
|
const output = await this.execGit(['branch', '--show-current'])
|
||||||
return !output.stdout.trim().startsWith('refs/heads/')
|
return output.stdout.trim() === ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async lfsFetch(ref: string): Promise<void> {
|
async lfsFetch(ref: string): Promise<void> {
|
||||||
@ -353,82 +191,14 @@ class GitCommandManager {
|
|||||||
await this.execGit(['lfs', 'install', '--local'])
|
await this.execGit(['lfs', 'install', '--local'])
|
||||||
}
|
}
|
||||||
|
|
||||||
async log1(format?: string): Promise<string> {
|
async log1(): Promise<void> {
|
||||||
const args = format ? ['log', '-1', format] : ['log', '-1']
|
await this.execGit(['log', '-1'])
|
||||||
const 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> {
|
||||||
await this.execGit(['remote', 'add', remoteName, remoteUrl])
|
await this.execGit(['remote', 'add', remoteName, remoteUrl])
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEnvironmentVariable(name: string): void {
|
|
||||||
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 {
|
|
||||||
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> {
|
|
||||||
const args = ['submodule', 'foreach']
|
|
||||||
if (recursive) {
|
|
||||||
args.push('--recursive')
|
|
||||||
}
|
|
||||||
args.push(command)
|
|
||||||
|
|
||||||
const output = await this.execGit(args)
|
|
||||||
return output.stdout
|
|
||||||
}
|
|
||||||
|
|
||||||
async submoduleSync(recursive: boolean): Promise<void> {
|
|
||||||
const args = ['submodule', 'sync']
|
|
||||||
if (recursive) {
|
|
||||||
args.push('--recursive')
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.execGit(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
async submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void> {
|
|
||||||
const args = ['-c', 'protocol.version=2']
|
|
||||||
args.push('submodule', 'update', '--init', '--force')
|
|
||||||
if (fetchDepth > 0) {
|
|
||||||
args.push(`--depth=${fetchDepth}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recursive) {
|
|
||||||
args.push('--recursive')
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.execGit(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
async submoduleStatus(): Promise<boolean> {
|
|
||||||
const output = await this.execGit(['submodule', 'status'], true)
|
|
||||||
core.debug(output.stdout)
|
|
||||||
return output.exitCode === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
async tagExists(pattern: string): Promise<boolean> {
|
async tagExists(pattern: string): Promise<boolean> {
|
||||||
const output = await this.execGit(['tag', '--list', pattern])
|
const output = await this.execGit(['tag', '--list', pattern])
|
||||||
return !!output.stdout.trim()
|
return !!output.stdout.trim()
|
||||||
@ -439,33 +209,22 @@ class GitCommandManager {
|
|||||||
return output.exitCode === 0
|
return output.exitCode === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryConfigUnset(
|
async tryConfigUnset(configKey: string): Promise<boolean> {
|
||||||
configKey: string,
|
|
||||||
globalConfig?: boolean
|
|
||||||
): Promise<boolean> {
|
|
||||||
const output = await this.execGit(
|
const output = await this.execGit(
|
||||||
[
|
['config', '--unset-all', configKey],
|
||||||
'config',
|
|
||||||
globalConfig ? '--global' : '--local',
|
|
||||||
'--unset-all',
|
|
||||||
configKey
|
|
||||||
],
|
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
return output.exitCode === 0
|
return output.exitCode === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
|
async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
|
||||||
const output = await this.execGit(
|
const output = await this.execGit(['config', 'gc.auto', '0'], true)
|
||||||
['config', '--local', 'gc.auto', '0'],
|
|
||||||
true
|
|
||||||
)
|
|
||||||
return output.exitCode === 0
|
return output.exitCode === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryGetFetchUrl(): Promise<string> {
|
async tryGetFetchUrl(): Promise<string> {
|
||||||
const output = await this.execGit(
|
const output = await this.execGit(
|
||||||
['config', '--local', '--get', 'remote.origin.url'],
|
['config', '--get', 'remote.origin.url'],
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -486,29 +245,18 @@ class GitCommandManager {
|
|||||||
return output.exitCode === 0
|
return output.exitCode === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async version(): Promise<GitVersion> {
|
|
||||||
return this.gitVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
static async createCommandManager(
|
static async createCommandManager(
|
||||||
workingDirectory: string,
|
workingDirectory: string,
|
||||||
lfs: boolean,
|
lfs: boolean
|
||||||
doSparseCheckout: boolean
|
|
||||||
): Promise<GitCommandManager> {
|
): Promise<GitCommandManager> {
|
||||||
const result = new GitCommandManager()
|
const result = new GitCommandManager()
|
||||||
await result.initializeCommandManager(
|
await result.initializeCommandManager(workingDirectory, lfs)
|
||||||
workingDirectory,
|
|
||||||
lfs,
|
|
||||||
doSparseCheckout
|
|
||||||
)
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private async execGit(
|
private async execGit(
|
||||||
args: string[],
|
args: string[],
|
||||||
allowAllExitCodes = false,
|
allowAllExitCodes = false
|
||||||
silent = false,
|
|
||||||
customListeners = {}
|
|
||||||
): Promise<GitOutput> {
|
): Promise<GitOutput> {
|
||||||
fshelper.directoryExistsSync(this.workingDirectory, true)
|
fshelper.directoryExistsSync(this.workingDirectory, true)
|
||||||
|
|
||||||
@ -522,36 +270,27 @@ class GitCommandManager {
|
|||||||
env[key] = this.gitEnv[key]
|
env[key] = this.gitEnv[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultListener = {
|
|
||||||
stdout: (data: Buffer) => {
|
|
||||||
stdout.push(data.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedListeners = {...defaultListener, ...customListeners}
|
|
||||||
|
|
||||||
const stdout: string[] = []
|
const stdout: string[] = []
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
cwd: this.workingDirectory,
|
cwd: this.workingDirectory,
|
||||||
env,
|
env,
|
||||||
silent,
|
|
||||||
ignoreReturnCode: allowAllExitCodes,
|
ignoreReturnCode: allowAllExitCodes,
|
||||||
listeners: mergedListeners
|
listeners: {
|
||||||
|
stdout: (data: Buffer) => {
|
||||||
|
stdout.push(data.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
|
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
|
||||||
result.stdout = stdout.join('')
|
result.stdout = stdout.join('')
|
||||||
|
|
||||||
core.debug(result.exitCode.toString())
|
|
||||||
core.debug(result.stdout)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initializeCommandManager(
|
private async initializeCommandManager(
|
||||||
workingDirectory: string,
|
workingDirectory: string,
|
||||||
lfs: boolean,
|
lfs: boolean
|
||||||
doSparseCheckout: boolean
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.workingDirectory = workingDirectory
|
this.workingDirectory = workingDirectory
|
||||||
|
|
||||||
@ -566,23 +305,23 @@ class GitCommandManager {
|
|||||||
|
|
||||||
// Git version
|
// Git version
|
||||||
core.debug('Getting git version')
|
core.debug('Getting git version')
|
||||||
this.gitVersion = new GitVersion()
|
let gitVersion = new GitVersion()
|
||||||
let gitOutput = await this.execGit(['version'])
|
let gitOutput = await this.execGit(['version'])
|
||||||
let stdout = gitOutput.stdout.trim()
|
let stdout = gitOutput.stdout.trim()
|
||||||
if (!stdout.includes('\n')) {
|
if (!stdout.includes('\n')) {
|
||||||
const match = stdout.match(/\d+\.\d+(\.\d+)?/)
|
const match = stdout.match(/\d+\.\d+(\.\d+)?/)
|
||||||
if (match) {
|
if (match) {
|
||||||
this.gitVersion = new GitVersion(match[0])
|
gitVersion = new GitVersion(match[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.gitVersion.isValid()) {
|
if (!gitVersion.isValid()) {
|
||||||
throw new Error('Unable to determine git version')
|
throw new Error('Unable to determine git version')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimum git version
|
// Minimum git version
|
||||||
if (!this.gitVersion.checkMinimum(MinimumGitVersion)) {
|
if (!gitVersion.checkMinimum(MinimumGitVersion)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Minimum required git version is ${MinimumGitVersion}. Your git ('${this.gitPath}') is ${this.gitVersion}`
|
`Minimum required git version is ${MinimumGitVersion}. Your git ('${this.gitPath}') is ${gitVersion}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -614,16 +353,8 @@ class GitCommandManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.doSparseCheckout = doSparseCheckout
|
|
||||||
if (this.doSparseCheckout) {
|
|
||||||
if (!this.gitVersion.checkMinimum(MinimumGitSparseCheckoutVersion)) {
|
|
||||||
throw new Error(
|
|
||||||
`Minimum Git version required for sparse checkout is ${MinimumGitSparseCheckoutVersion}. Your git ('${this.gitPath}') is ${this.gitVersion}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Set the user agent
|
// Set the user agent
|
||||||
const gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
|
const gitHttpUserAgent = `git/${gitVersion} (github-actions-checkout)`
|
||||||
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
|
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
|
||||||
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
|
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
|
||||||
}
|
}
|
||||||
|
@ -1,125 +0,0 @@
|
|||||||
import * as assert from 'assert'
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
import * as fsHelper from './fs-helper'
|
|
||||||
import * as io from '@actions/io'
|
|
||||||
import * as path from 'path'
|
|
||||||
import {IGitCommandManager} from './git-command-manager'
|
|
||||||
|
|
||||||
export async function prepareExistingDirectory(
|
|
||||||
git: IGitCommandManager | undefined,
|
|
||||||
repositoryPath: string,
|
|
||||||
repositoryUrl: string,
|
|
||||||
clean: boolean,
|
|
||||||
ref: string
|
|
||||||
): 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
|
|
||||||
|
|
||||||
// Check whether using git or REST API
|
|
||||||
if (!git) {
|
|
||||||
remove = true
|
|
||||||
}
|
|
||||||
// Fetch URL does not match
|
|
||||||
else if (
|
|
||||||
!fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) ||
|
|
||||||
repositoryUrl !== (await git.tryGetFetchUrl())
|
|
||||||
) {
|
|
||||||
remove = true
|
|
||||||
} else {
|
|
||||||
// Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process
|
|
||||||
const lockPaths = [
|
|
||||||
path.join(repositoryPath, '.git', 'index.lock'),
|
|
||||||
path.join(repositoryPath, '.git', 'shallow.lock')
|
|
||||||
]
|
|
||||||
for (const lockPath of lockPaths) {
|
|
||||||
try {
|
|
||||||
await io.rmRF(lockPath)
|
|
||||||
} catch (error) {
|
|
||||||
core.debug(
|
|
||||||
`Unable to delete '${lockPath}'. ${(error as any)?.message ?? error}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
core.startGroup('Removing previously created refs, to avoid conflicts')
|
|
||||||
// Checkout detached HEAD
|
|
||||||
if (!(await git.isDetached())) {
|
|
||||||
await git.checkoutDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all refs/heads/*
|
|
||||||
let branches = await git.branchList(false)
|
|
||||||
for (const branch of branches) {
|
|
||||||
await git.branchDelete(false, branch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any conflicting refs/remotes/origin/*
|
|
||||||
// Example 1: Consider ref is refs/heads/foo and previously fetched refs/remotes/origin/foo/bar
|
|
||||||
// Example 2: Consider ref is refs/heads/foo/bar and previously fetched refs/remotes/origin/foo
|
|
||||||
if (ref) {
|
|
||||||
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()
|
|
||||||
|
|
||||||
// Check for submodules and delete any existing files if submodules are present
|
|
||||||
if (!(await git.submoduleStatus())) {
|
|
||||||
remove = true
|
|
||||||
core.info('Bad Submodules found, removing existing files')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean
|
|
||||||
if (clean) {
|
|
||||||
core.startGroup('Cleaning the repository')
|
|
||||||
if (!(await git.tryClean())) {
|
|
||||||
core.debug(
|
|
||||||
`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For further investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`
|
|
||||||
)
|
|
||||||
remove = true
|
|
||||||
} else if (!(await git.tryReset())) {
|
|
||||||
remove = true
|
|
||||||
}
|
|
||||||
core.endGroup()
|
|
||||||
|
|
||||||
if (remove) {
|
|
||||||
core.warning(
|
|
||||||
`Unable to clean or reset the repository. The repository will be recreated instead.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
core.warning(
|
|
||||||
`Unable to prepare the existing repository. The repository will be recreated instead.`
|
|
||||||
)
|
|
||||||
remove = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remove) {
|
|
||||||
// Delete the contents of the directory. Don't delete the directory itself
|
|
||||||
// since it might be the current working directory.
|
|
||||||
core.info(`Deleting the contents of '${repositoryPath}'`)
|
|
||||||
for (const file of await fs.promises.readdir(repositoryPath)) {
|
|
||||||
await io.rmRF(path.join(repositoryPath, file))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +1,37 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
|
import * as coreCommand from '@actions/core/lib/command'
|
||||||
|
import * as fs from 'fs'
|
||||||
import * as fsHelper from './fs-helper'
|
import * as fsHelper from './fs-helper'
|
||||||
import * as gitAuthHelper from './git-auth-helper'
|
|
||||||
import * as gitCommandManager from './git-command-manager'
|
import * as gitCommandManager from './git-command-manager'
|
||||||
import * as gitDirectoryHelper from './git-directory-helper'
|
|
||||||
import * as githubApiHelper from './github-api-helper'
|
import * as githubApiHelper from './github-api-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 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 {
|
|
||||||
MinimumGitSparseCheckoutVersion,
|
|
||||||
IGitCommandManager
|
|
||||||
} from './git-command-manager'
|
|
||||||
import {IGitSourceSettings} from './git-source-settings'
|
|
||||||
|
|
||||||
export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
const authConfigKey = `http.https://github.com/.extraheader`
|
||||||
|
|
||||||
|
export interface ISourceSettings {
|
||||||
|
repositoryPath: string
|
||||||
|
repositoryOwner: string
|
||||||
|
repositoryName: string
|
||||||
|
ref: string
|
||||||
|
commit: string
|
||||||
|
clean: boolean
|
||||||
|
fetchDepth: number
|
||||||
|
lfs: boolean
|
||||||
|
accessToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSource(settings: ISourceSettings): 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 = `https://github.com/${encodeURIComponent(
|
||||||
|
settings.repositoryOwner
|
||||||
|
)}/${encodeURIComponent(settings.repositoryName)}`
|
||||||
|
|
||||||
// Remove conflicting file path
|
// Remove conflicting file path
|
||||||
if (fsHelper.fileExistsSync(settings.repositoryPath)) {
|
if (fsHelper.fileExistsSync(settings.repositoryPath)) {
|
||||||
@ -35,73 +46,33 @@ 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
|
||||||
try {
|
if (isExisting) {
|
||||||
if (git) {
|
await prepareExistingDirectory(
|
||||||
authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
git,
|
||||||
if (settings.setSafeDirectory) {
|
settings.repositoryPath,
|
||||||
// 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
|
repositoryUrl,
|
||||||
// Otherwise all git commands we run in a container fail
|
settings.clean
|
||||||
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,
|
|
||||||
settings.githubServerUrl
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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`
|
||||||
|
)
|
||||||
|
await githubApiHelper.downloadRepository(
|
||||||
|
settings.accessToken,
|
||||||
|
settings.repositoryOwner,
|
||||||
|
settings.repositoryName,
|
||||||
|
settings.ref,
|
||||||
|
settings.commit,
|
||||||
|
settings.repositoryPath
|
||||||
|
)
|
||||||
|
} else {
|
||||||
// Save state for POST action
|
// Save state for POST action
|
||||||
stateHelper.setRepositoryPath(settings.repositoryPath)
|
stateHelper.setRepositoryPath(settings.repositoryPath)
|
||||||
|
|
||||||
@ -109,45 +80,28 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
|||||||
if (
|
if (
|
||||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||||
) {
|
) {
|
||||||
core.startGroup('Initializing the repository')
|
|
||||||
await git.init()
|
await git.init()
|
||||||
await git.remoteAdd('origin', repositoryUrl)
|
await git.remoteAdd('origin', repositoryUrl)
|
||||||
core.endGroup()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable automatic garbage collection
|
// Disable automatic garbage collection
|
||||||
core.startGroup('Disabling automatic garbage collection')
|
|
||||||
if (!(await git.tryDisableAutomaticGarbageCollection())) {
|
if (!(await git.tryDisableAutomaticGarbageCollection())) {
|
||||||
core.warning(
|
core.warning(
|
||||||
`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
|
`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
|
// Remove possible previous extraheader
|
||||||
if (!authHelper) {
|
await removeGitConfig(git, authConfigKey)
|
||||||
authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
|
||||||
}
|
|
||||||
// Configure auth
|
|
||||||
core.startGroup('Setting up auth')
|
|
||||||
await authHelper.configureAuth()
|
|
||||||
core.endGroup()
|
|
||||||
|
|
||||||
// Determine the default branch
|
// Add extraheader (auth)
|
||||||
if (!settings.ref && !settings.commit) {
|
const base64Credentials = Buffer.from(
|
||||||
core.startGroup('Determining the default branch')
|
`x-access-token:${settings.accessToken}`,
|
||||||
if (settings.sshKey) {
|
'utf8'
|
||||||
settings.ref = await git.getDefaultBranch(repositoryUrl)
|
).toString('base64')
|
||||||
} else {
|
core.setSecret(base64Credentials)
|
||||||
settings.ref = await githubApiHelper.getDefaultBranch(
|
const authConfigValue = `AUTHORIZATION: basic ${base64Credentials}`
|
||||||
settings.authToken,
|
await git.config(authConfigKey, authConfigValue)
|
||||||
settings.repositoryOwner,
|
|
||||||
settings.repositoryName,
|
|
||||||
settings.githubServerUrl
|
|
||||||
)
|
|
||||||
}
|
|
||||||
core.endGroup()
|
|
||||||
}
|
|
||||||
|
|
||||||
// LFS install
|
// LFS install
|
||||||
if (settings.lfs) {
|
if (settings.lfs) {
|
||||||
@ -155,190 +109,55 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch
|
// Fetch
|
||||||
core.startGroup('Fetching the repository')
|
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
||||||
const fetchOptions: {
|
await git.fetch(settings.fetchDepth, refSpec)
|
||||||
filter?: string
|
|
||||||
fetchDepth?: number
|
|
||||||
fetchTags?: boolean
|
|
||||||
showProgress?: boolean
|
|
||||||
} = {}
|
|
||||||
|
|
||||||
if (settings.filter) {
|
|
||||||
fetchOptions.filter = settings.filter
|
|
||||||
} else if (settings.sparseCheckout) {
|
|
||||||
fetchOptions.filter = 'blob:none'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.fetchDepth <= 0) {
|
|
||||||
// Fetch all branches and tags
|
|
||||||
let refSpec = refHelper.getRefSpecForAllHistory(
|
|
||||||
settings.ref,
|
|
||||||
settings.commit
|
|
||||||
)
|
|
||||||
await git.fetch(refSpec, fetchOptions)
|
|
||||||
|
|
||||||
// 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, fetchOptions)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fetchOptions.fetchDepth = settings.fetchDepth
|
|
||||||
fetchOptions.fetchTags = settings.fetchTags
|
|
||||||
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
|
||||||
await git.fetch(refSpec, fetchOptions)
|
|
||||||
}
|
|
||||||
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.
|
||||||
// For sparse checkouts, let `checkout` fetch the needed objects lazily.
|
if (settings.lfs) {
|
||||||
if (settings.lfs && !settings.sparseCheckout) {
|
|
||||||
core.startGroup('Fetching LFS objects')
|
|
||||||
await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
|
await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
|
||||||
core.endGroup()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sparse checkout
|
|
||||||
if (!settings.sparseCheckout) {
|
|
||||||
let gitVersion = await git.version()
|
|
||||||
// no need to disable sparse-checkout if the installed git runtime doesn't even support it.
|
|
||||||
if (gitVersion.checkMinimum(MinimumGitSparseCheckoutVersion)) {
|
|
||||||
await git.disableSparseCheckout()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
core.startGroup('Setting up sparse checkout')
|
|
||||||
if (settings.sparseCheckoutConeMode) {
|
|
||||||
await git.sparseCheckout(settings.sparseCheckout)
|
|
||||||
} else {
|
|
||||||
await git.sparseCheckoutNonConeMode(settings.sparseCheckout)
|
|
||||||
}
|
|
||||||
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
|
// Dump some info about the checked out commit
|
||||||
if (settings.submodules) {
|
await git.log1()
|
||||||
// Temporarily override global config
|
|
||||||
core.startGroup('Setting up auth for fetching submodules')
|
|
||||||
await authHelper.configureGlobalAuth()
|
|
||||||
core.endGroup()
|
|
||||||
|
|
||||||
// Checkout submodules
|
|
||||||
core.startGroup('Fetching submodules')
|
|
||||||
await git.submoduleSync(settings.nestedSubmodules)
|
|
||||||
await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules)
|
|
||||||
await git.submoduleForeach(
|
|
||||||
'git config --local gc.auto 0',
|
|
||||||
settings.nestedSubmodules
|
|
||||||
)
|
|
||||||
core.endGroup()
|
|
||||||
|
|
||||||
// Persist credentials
|
|
||||||
if (settings.persistCredentials) {
|
|
||||||
core.startGroup('Persisting credentials for submodules')
|
|
||||||
await authHelper.configureSubmoduleAuth()
|
|
||||||
core.endGroup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get commit information
|
|
||||||
const commitInfo = 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,
|
|
||||||
settings.githubServerUrl
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
// Remove auth
|
|
||||||
if (authHelper) {
|
|
||||||
if (!settings.persistCredentials) {
|
|
||||||
core.startGroup('Removing auth')
|
|
||||||
await authHelper.removeAuth()
|
|
||||||
core.endGroup()
|
|
||||||
}
|
|
||||||
authHelper.removeGlobalConfig()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanup(repositoryPath: string): Promise<void> {
|
export async function cleanup(repositoryPath: string): Promise<void> {
|
||||||
// Repo exists?
|
// Repo exists?
|
||||||
if (
|
if (!fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))) {
|
||||||
!repositoryPath ||
|
|
||||||
!fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fsHelper.directoryExistsSync(repositoryPath, true)
|
||||||
|
|
||||||
let git: IGitCommandManager
|
// Remove the config key
|
||||||
try {
|
const git = await gitCommandManager.CreateCommandManager(
|
||||||
git = await gitCommandManager.createCommandManager(
|
repositoryPath,
|
||||||
repositoryPath,
|
false
|
||||||
false,
|
)
|
||||||
false
|
await removeGitConfig(git, authConfigKey)
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove auth
|
|
||||||
const authHelper = gitAuthHelper.createAuthHelper(git)
|
|
||||||
try {
|
|
||||||
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(
|
||||||
settings: IGitSourceSettings
|
settings: ISourceSettings
|
||||||
): Promise<IGitCommandManager | undefined> {
|
): Promise<IGitCommandManager> {
|
||||||
core.info(`Working directory is '${settings.repositoryPath}'`)
|
core.info(`Working directory is '${settings.repositoryPath}'`)
|
||||||
|
let git = (null as unknown) as IGitCommandManager
|
||||||
try {
|
try {
|
||||||
return await gitCommandManager.createCommandManager(
|
return await gitCommandManager.CreateCommandManager(
|
||||||
settings.repositoryPath,
|
settings.repositoryPath,
|
||||||
settings.lfs,
|
settings.lfs
|
||||||
settings.sparseCheckout != null
|
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Git is required for LFS
|
// Git is required for LFS
|
||||||
@ -347,6 +166,119 @@ async function getGitCommandManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise fallback to REST API
|
// Otherwise fallback to REST API
|
||||||
return undefined
|
return (null as unknown) as IGitCommandManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareExistingDirectory(
|
||||||
|
git: IGitCommandManager,
|
||||||
|
repositoryPath: string,
|
||||||
|
repositoryUrl: string,
|
||||||
|
clean: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
let remove = false
|
||||||
|
|
||||||
|
// Check whether using git or REST API
|
||||||
|
if (!git) {
|
||||||
|
remove = true
|
||||||
|
}
|
||||||
|
// Fetch URL does not match
|
||||||
|
else if (
|
||||||
|
!fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) ||
|
||||||
|
repositoryUrl !== (await git.tryGetFetchUrl())
|
||||||
|
) {
|
||||||
|
remove = true
|
||||||
|
} else {
|
||||||
|
// Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process
|
||||||
|
const lockPaths = [
|
||||||
|
path.join(repositoryPath, '.git', 'index.lock'),
|
||||||
|
path.join(repositoryPath, '.git', 'shallow.lock')
|
||||||
|
]
|
||||||
|
for (const lockPath of lockPaths) {
|
||||||
|
try {
|
||||||
|
await io.rmRF(lockPath)
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(`Unable to delete '${lockPath}'. ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Checkout detached HEAD
|
||||||
|
if (!(await git.isDetached())) {
|
||||||
|
await git.checkoutDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all refs/heads/*
|
||||||
|
let branches = await git.branchList(false)
|
||||||
|
for (const branch of branches) {
|
||||||
|
await git.branchDelete(false, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all refs/remotes/origin/* to avoid conflicts
|
||||||
|
branches = await git.branchList(true)
|
||||||
|
for (const branch of branches) {
|
||||||
|
await git.branchDelete(true, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean
|
||||||
|
if (clean) {
|
||||||
|
if (!(await git.tryClean())) {
|
||||||
|
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}'.`
|
||||||
|
)
|
||||||
|
remove = true
|
||||||
|
} else if (!(await git.tryReset())) {
|
||||||
|
remove = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remove) {
|
||||||
|
core.warning(
|
||||||
|
`Unable to clean or reset the repository. The repository will be recreated instead.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(
|
||||||
|
`Unable to prepare the existing repository. The repository will be recreated instead.`
|
||||||
|
)
|
||||||
|
remove = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remove) {
|
||||||
|
// Delete the contents of the directory. Don't delete the directory itself
|
||||||
|
// since it might be the current working directory.
|
||||||
|
core.info(`Deleting the contents of '${repositoryPath}'`)
|
||||||
|
for (const file of await fs.promises.readdir(repositoryPath)) {
|
||||||
|
await io.rmRF(path.join(repositoryPath, file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeGitConfig(
|
||||||
|
git: IGitCommandManager,
|
||||||
|
configKey: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (
|
||||||
|
(await git.configExists(configKey)) &&
|
||||||
|
!(await git.tryConfigUnset(configKey))
|
||||||
|
) {
|
||||||
|
// Load the config contents
|
||||||
|
core.warning(
|
||||||
|
`Failed to remove '${configKey}' from the git config. Attempting to remove the config value by editing the file directly.`
|
||||||
|
)
|
||||||
|
const configPath = path.join(git.getWorkingDirectory(), '.git', 'config')
|
||||||
|
fsHelper.fileExistsSync(configPath)
|
||||||
|
let contents = fs.readFileSync(configPath).toString() || ''
|
||||||
|
|
||||||
|
// Filter - only includes lines that do not contain the config key
|
||||||
|
const upperConfigKey = configKey.toUpperCase()
|
||||||
|
const split = contents
|
||||||
|
.split('\n')
|
||||||
|
.filter(x => !x.toUpperCase().includes(upperConfigKey))
|
||||||
|
contents = split.join('\n')
|
||||||
|
|
||||||
|
// Rewrite the config file
|
||||||
|
fs.writeFileSync(configPath, contents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,121 +0,0 @@
|
|||||||
export interface IGitSourceSettings {
|
|
||||||
/**
|
|
||||||
* The location on disk where the repository will be placed
|
|
||||||
*/
|
|
||||||
repositoryPath: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The repository owner
|
|
||||||
*/
|
|
||||||
repositoryOwner: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The repository name
|
|
||||||
*/
|
|
||||||
repositoryName: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ref to fetch
|
|
||||||
*/
|
|
||||||
ref: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The commit to checkout
|
|
||||||
*/
|
|
||||||
commit: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether to clean the repository
|
|
||||||
*/
|
|
||||||
clean: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The filter determining which objects to include
|
|
||||||
*/
|
|
||||||
filter: string | undefined
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The array of folders to make the sparse checkout
|
|
||||||
*/
|
|
||||||
sparseCheckout: string[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether to use cone mode in the sparse checkout (if any)
|
|
||||||
*/
|
|
||||||
sparseCheckoutConeMode: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The depth when fetching
|
|
||||||
*/
|
|
||||||
fetchDepth: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch tags, even if fetchDepth > 0 (default: false)
|
|
||||||
*/
|
|
||||||
fetchTags: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether to use the --progress option when fetching
|
|
||||||
*/
|
|
||||||
showProgress: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether to fetch LFS objects
|
|
||||||
*/
|
|
||||||
lfs: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether to checkout submodules
|
|
||||||
*/
|
|
||||||
submodules: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether to recursively checkout submodules
|
|
||||||
*/
|
|
||||||
nestedSubmodules: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The auth token to use when fetching the repository
|
|
||||||
*/
|
|
||||||
authToken: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The SSH key to configure
|
|
||||||
*/
|
|
||||||
sshKey: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional SSH known hosts
|
|
||||||
*/
|
|
||||||
sshKnownHosts: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether the server must be a known host
|
|
||||||
*/
|
|
||||||
sshStrict: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The SSH user to login as
|
|
||||||
*/
|
|
||||||
sshUser: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether to persist the credentials on disk to enable scripting authenticated git commands
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User override on the GitHub Server/Host URL that hosts the repository to be cloned
|
|
||||||
*/
|
|
||||||
githubServerUrl: string | undefined
|
|
||||||
}
|
|
@ -6,30 +6,23 @@ import * as io from '@actions/io'
|
|||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as retryHelper from './retry-helper'
|
import * as retryHelper from './retry-helper'
|
||||||
import * as toolCache from '@actions/tool-cache'
|
import * as toolCache from '@actions/tool-cache'
|
||||||
import {v4 as uuid} from 'uuid'
|
import {default as uuid} from 'uuid/v4'
|
||||||
import {getServerApiUrl} from './url-helper'
|
import {ReposGetArchiveLinkParams} from '@octokit/rest'
|
||||||
|
|
||||||
const IS_WINDOWS = process.platform === 'win32'
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
export async function downloadRepository(
|
export async function downloadRepository(
|
||||||
authToken: string,
|
accessToken: string,
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
ref: string,
|
ref: string,
|
||||||
commit: string,
|
commit: string,
|
||||||
repositoryPath: string,
|
repositoryPath: string
|
||||||
baseUrl?: string
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Determine the default branch
|
|
||||||
if (!ref && !commit) {
|
|
||||||
core.info('Determining the default branch')
|
|
||||||
ref = await getDefaultBranch(authToken, owner, repo, baseUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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')
|
||||||
return await downloadArchive(authToken, owner, repo, ref, commit, baseUrl)
|
return await downloadArchive(accessToken, owner, repo, ref, commit)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Write archive to disk
|
// Write archive to disk
|
||||||
@ -48,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.
|
||||||
@ -65,79 +58,31 @@ export async function downloadRepository(
|
|||||||
for (const fileName of await fs.promises.readdir(tempRepositoryPath)) {
|
for (const fileName of await fs.promises.readdir(tempRepositoryPath)) {
|
||||||
const sourcePath = path.join(tempRepositoryPath, fileName)
|
const sourcePath = path.join(tempRepositoryPath, fileName)
|
||||||
const targetPath = path.join(repositoryPath, fileName)
|
const targetPath = path.join(repositoryPath, fileName)
|
||||||
if (IS_WINDOWS) {
|
await io.mv(sourcePath, targetPath)
|
||||||
await io.cp(sourcePath, targetPath, {recursive: true}) // Copy on Windows (Windows Defender may have a lock)
|
|
||||||
} else {
|
|
||||||
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,
|
|
||||||
baseUrl?: string
|
|
||||||
): Promise<string> {
|
|
||||||
return await retryHelper.execute(async () => {
|
|
||||||
core.info('Retrieving the default branch name')
|
|
||||||
const octokit = github.getOctokit(authToken, {
|
|
||||||
baseUrl: getServerApiUrl(baseUrl)
|
|
||||||
})
|
|
||||||
let result: string
|
|
||||||
try {
|
|
||||||
// Get the default branch from the repo info
|
|
||||||
const response = await octokit.rest.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(
|
||||||
authToken: string,
|
accessToken: string,
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
ref: string,
|
ref: string,
|
||||||
commit: string,
|
commit: string
|
||||||
baseUrl?: string
|
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
const octokit = github.getOctokit(authToken, {
|
const octokit = new github.GitHub(accessToken)
|
||||||
baseUrl: getServerApiUrl(baseUrl)
|
const params: ReposGetArchiveLinkParams = {
|
||||||
})
|
|
||||||
const download = IS_WINDOWS
|
|
||||||
? octokit.rest.repos.downloadZipballArchive
|
|
||||||
: octokit.rest.repos.downloadTarballArchive
|
|
||||||
const response = await download({
|
|
||||||
owner: owner,
|
owner: owner,
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
archive_format: IS_WINDOWS ? 'zipball' : 'tarball',
|
||||||
ref: commit || ref
|
ref: commit || ref
|
||||||
})
|
}
|
||||||
return Buffer.from(response.data as ArrayBuffer) // response.data is ArrayBuffer
|
const response = await octokit.repos.getArchiveLink(params)
|
||||||
|
if (response.status != 200) {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected response from GitHub API. Status: '${response.status}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.from(response.data) // response.data is ArrayBuffer
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,10 @@ 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 workflowContextHelper from './workflow-context-helper'
|
import {ISourceSettings} from './git-source-provider'
|
||||||
import {IGitSourceSettings} from './git-source-settings'
|
|
||||||
|
|
||||||
export async function getInputs(): Promise<IGitSourceSettings> {
|
export function getInputs(): ISourceSettings {
|
||||||
const result = {} as unknown as IGitSourceSettings
|
const result = ({} as unknown) as ISourceSettings
|
||||||
|
|
||||||
// GitHub workspace
|
// GitHub workspace
|
||||||
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
|
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
|
||||||
@ -62,12 +61,10 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
|||||||
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),
|
if (!result.ref && !result.commit) {
|
||||||
// the ref is unqualifed like "main" instead of "refs/heads/main".
|
result.ref = 'refs/heads/master'
|
||||||
if (result.commit && result.ref && !result.ref.startsWith('refs/')) {
|
|
||||||
result.ref = `refs/heads/${result.ref}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SHA?
|
// SHA?
|
||||||
@ -82,25 +79,13 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
|||||||
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
|
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
|
||||||
core.debug(`clean = ${result.clean}`)
|
core.debug(`clean = ${result.clean}`)
|
||||||
|
|
||||||
// Filter
|
// Submodules
|
||||||
const filter = core.getInput('filter')
|
if (core.getInput('submodules')) {
|
||||||
if (filter) {
|
throw new Error(
|
||||||
result.filter = filter
|
"The input 'submodules' is not supported in actions/checkout@v2"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
core.debug(`filter = ${result.filter}`)
|
|
||||||
|
|
||||||
// Sparse checkout
|
|
||||||
const sparseCheckout = core.getMultilineInput('sparse-checkout')
|
|
||||||
if (sparseCheckout.length) {
|
|
||||||
result.sparseCheckout = sparseCheckout
|
|
||||||
core.debug(`sparse checkout = ${result.sparseCheckout}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.sparseCheckoutConeMode =
|
|
||||||
(core.getInput('sparse-checkout-cone-mode') || 'true').toUpperCase() ===
|
|
||||||
'TRUE'
|
|
||||||
|
|
||||||
// Fetch depth
|
// Fetch depth
|
||||||
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))
|
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))
|
||||||
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {
|
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {
|
||||||
@ -108,58 +93,12 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
|||||||
}
|
}
|
||||||
core.debug(`fetch depth = ${result.fetchDepth}`)
|
core.debug(`fetch depth = ${result.fetchDepth}`)
|
||||||
|
|
||||||
// Fetch tags
|
|
||||||
result.fetchTags =
|
|
||||||
(core.getInput('fetch-tags') || 'false').toUpperCase() === 'TRUE'
|
|
||||||
core.debug(`fetch tags = ${result.fetchTags}`)
|
|
||||||
|
|
||||||
// Show fetch progress
|
|
||||||
result.showProgress =
|
|
||||||
(core.getInput('show-progress') || 'true').toUpperCase() === 'TRUE'
|
|
||||||
core.debug(`show progress = ${result.showProgress}`)
|
|
||||||
|
|
||||||
// LFS
|
// LFS
|
||||||
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'
|
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'
|
||||||
core.debug(`lfs = ${result.lfs}`)
|
core.debug(`lfs = ${result.lfs}`)
|
||||||
|
|
||||||
// Submodules
|
// Access token
|
||||||
result.submodules = false
|
result.accessToken = core.getInput('token')
|
||||||
result.nestedSubmodules = false
|
|
||||||
const submodulesString = (core.getInput('submodules') || '').toUpperCase()
|
|
||||||
if (submodulesString == 'RECURSIVE') {
|
|
||||||
result.submodules = true
|
|
||||||
result.nestedSubmodules = true
|
|
||||||
} else if (submodulesString == 'TRUE') {
|
|
||||||
result.submodules = true
|
|
||||||
}
|
|
||||||
core.debug(`submodules = ${result.submodules}`)
|
|
||||||
core.debug(`recursive submodules = ${result.nestedSubmodules}`)
|
|
||||||
|
|
||||||
// Auth token
|
|
||||||
result.authToken = core.getInput('token', {required: true})
|
|
||||||
|
|
||||||
// SSH
|
|
||||||
result.sshKey = core.getInput('ssh-key')
|
|
||||||
result.sshKnownHosts = core.getInput('ssh-known-hosts')
|
|
||||||
result.sshStrict =
|
|
||||||
(core.getInput('ssh-strict') || 'true').toUpperCase() === 'TRUE'
|
|
||||||
result.sshUser = core.getInput('ssh-user')
|
|
||||||
|
|
||||||
// Persist credentials
|
|
||||||
result.persistCredentials =
|
|
||||||
(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'
|
|
||||||
|
|
||||||
// Determine the GitHub URL that the repository is being hosted from
|
|
||||||
result.githubServerUrl = core.getInput('github-server-url')
|
|
||||||
core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,17 +10,17 @@ 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')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the action.yml
|
// Load the action.yml
|
||||||
const actionYaml = yaml.load(fs.readFileSync(actionYamlPath).toString())
|
const actionYaml = yaml.safeLoad(fs.readFileSync(actionYamlPath).toString())
|
||||||
|
|
||||||
// Load the README
|
// Load the README
|
||||||
const originalReadme = fs.readFileSync(readmePath).toString()
|
const originalReadme = fs.readFileSync(readmePath).toString()
|
||||||
@ -59,52 +59,28 @@ function updateUsage(
|
|||||||
|
|
||||||
// Constrain the width of the description
|
// Constrain the width of the description
|
||||||
const width = 80
|
const width = 80
|
||||||
let description = (input.description as string)
|
let description = input.description as string
|
||||||
.trimRight()
|
|
||||||
.replace(/\r\n/g, '\n') // Convert CR to LF
|
|
||||||
.replace(/ +/g, ' ') // Squash consecutive spaces
|
|
||||||
.replace(/ \n/g, '\n') // Squash space followed by newline
|
|
||||||
while (description) {
|
while (description) {
|
||||||
// Longer than width? Find a space to break apart
|
// Longer than width? Find a space to break apart
|
||||||
let segment: string = description
|
let segment: string = description
|
||||||
if (description.length > width) {
|
if (description.length > width) {
|
||||||
segment = description.substr(0, width + 1)
|
segment = description.substr(0, width + 1)
|
||||||
while (!segment.endsWith(' ') && !segment.endsWith('\n') && segment) {
|
while (!segment.endsWith(' ')) {
|
||||||
segment = segment.substr(0, segment.length - 1)
|
segment = segment.substr(0, segment.length - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trimmed too much?
|
|
||||||
if (segment.length < width * 0.67) {
|
|
||||||
segment = description
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
segment = description
|
segment = description
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for newline
|
description = description.substr(segment.length) // Remaining
|
||||||
const newlineIndex = segment.indexOf('\n')
|
segment = segment.trimRight() // Trim the trailing space
|
||||||
if (newlineIndex >= 0) {
|
newReadme.push(` # ${segment}`)
|
||||||
segment = segment.substr(0, newlineIndex + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append segment
|
|
||||||
newReadme.push(` # ${segment}`.trimRight())
|
|
||||||
|
|
||||||
// Remaining
|
|
||||||
description = description.substr(segment.length)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Input and default
|
||||||
if (input.default !== undefined) {
|
if (input.default !== undefined) {
|
||||||
// Append blank line if description had paragraphs
|
|
||||||
if ((input.description as string).trimRight().match(/\n[ ]*\r?\n/)) {
|
|
||||||
newReadme.push(` #`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default
|
|
||||||
newReadme.push(` # Default: ${input.default}`)
|
newReadme.push(` # Default: ${input.default}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input name
|
|
||||||
newReadme.push(` ${key}: ''`)
|
newReadme.push(` ${key}: ''`)
|
||||||
|
|
||||||
firstInput = false
|
firstInput = false
|
||||||
@ -120,7 +96,7 @@ function updateUsage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateUsage(
|
updateUsage(
|
||||||
'actions/checkout@v4',
|
'actions/checkout@v2-beta',
|
||||||
path.join(__dirname, '..', '..', 'action.yml'),
|
path.join(__dirname, '..', '..', 'action.yml'),
|
||||||
path.join(__dirname, '..', '..', 'README.md')
|
path.join(__dirname, '..', '..', 'README.md')
|
||||||
)
|
)
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
src/misc/licensed-download.sh
|
|
||||||
|
|
||||||
echo 'Running: licensed cached'
|
|
||||||
_temp/licensed-3.6.0/licensed status
|
|
@ -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
|
|
@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
src/misc/licensed-download.sh
|
|
||||||
|
|
||||||
echo 'Running: licensed cached'
|
|
||||||
_temp/licensed-3.6.0/licensed cache
|
|
@ -1,9 +1,4 @@
|
|||||||
import {IGitCommandManager} from './git-command-manager'
|
import {IGitCommandManager} from './git-command-manager'
|
||||||
import * as core from '@actions/core'
|
|
||||||
import * as github from '@actions/github'
|
|
||||||
import {getServerApiUrl, isGhes} from './url-helper'
|
|
||||||
|
|
||||||
export const tagsRefSpec = '+refs/tags/*:refs/tags/*'
|
|
||||||
|
|
||||||
export interface ICheckoutInfo {
|
export interface ICheckoutInfo {
|
||||||
ref: string
|
ref: string
|
||||||
@ -23,7 +18,7 @@ export async function getCheckoutInfo(
|
|||||||
throw new Error('Args ref and commit cannot both be empty')
|
throw new Error('Args ref and commit cannot both be empty')
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {} as unknown as ICheckoutInfo
|
const result = ({} as unknown) as ICheckoutInfo
|
||||||
const upperRef = (ref || '').toUpperCase()
|
const upperRef = (ref || '').toUpperCase()
|
||||||
|
|
||||||
// SHA only
|
// SHA only
|
||||||
@ -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,162 +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,
|
|
||||||
baseUrl?: string
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
// GHES?
|
|
||||||
if (isGhes(baseUrl)) {
|
|
||||||
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 = github.getOctokit(token, {
|
|
||||||
baseUrl: getServerApiUrl(baseUrl),
|
|
||||||
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.rest.repos.get({
|
|
||||||
owner: repositoryOwner,
|
|
||||||
repo: repositoryName
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
core.debug(
|
|
||||||
`Error when validating commit info: ${(err as any)?.stack ?? err}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
export function escape(value: string): string {
|
|
||||||
return value.replace(/[^a-zA-Z0-9_]/g, x => {
|
|
||||||
return `\\${x}`
|
|
||||||
})
|
|
||||||
}
|
|
@ -17,9 +17,6 @@ export class RetryHelper {
|
|||||||
this.maxAttempts = maxAttempts
|
this.maxAttempts = maxAttempts
|
||||||
this.minSeconds = Math.floor(minSeconds)
|
this.minSeconds = Math.floor(minSeconds)
|
||||||
this.maxSeconds = Math.floor(maxSeconds)
|
this.maxSeconds = Math.floor(maxSeconds)
|
||||||
if (this.minSeconds > this.maxSeconds) {
|
|
||||||
throw new Error('min seconds should be less than or equal to max seconds')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute<T>(action: () => Promise<T>): Promise<T> {
|
async execute<T>(action: () => Promise<T>): Promise<T> {
|
||||||
@ -29,7 +26,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
|
||||||
|
@ -1,60 +1,29 @@
|
|||||||
import * as core from '@actions/core'
|
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.
|
|
||||||
*/
|
|
||||||
export const SshKeyPath = core.getState('sshKeyPath')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The SSH known hosts path for the POST action. The value is empty during the MAIN action.
|
|
||||||
*/
|
|
||||||
export const SshKnownHostsPath = core.getState('sshKnownHostsPath')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
)
|
||||||
*/
|
|
||||||
export function setSshKeyPath(sshKeyPath: string) {
|
|
||||||
core.saveState('sshKeyPath', sshKeyPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the SSH known hosts path so the POST action can retrieve the value.
|
|
||||||
*/
|
|
||||||
export function setSshKnownHostsPath(sshKnownHostsPath: string) {
|
|
||||||
core.saveState('sshKnownHostsPath', sshKnownHostsPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the set-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')
|
||||||
}
|
}
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
import * as assert from 'assert'
|
|
||||||
import {URL} from 'url'
|
|
||||||
import {IGitSourceSettings} from './git-source-settings'
|
|
||||||
|
|
||||||
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(settings.githubServerUrl)
|
|
||||||
const encodedOwner = encodeURIComponent(settings.repositoryOwner)
|
|
||||||
const encodedName = encodeURIComponent(settings.repositoryName)
|
|
||||||
if (settings.sshKey) {
|
|
||||||
const user = settings.sshUser.length > 0 ? settings.sshUser : 'git'
|
|
||||||
return `${user}@${serviceUrl.hostname}:${encodedOwner}/${encodedName}.git`
|
|
||||||
}
|
|
||||||
|
|
||||||
// "origin" is SCHEME://HOSTNAME[:PORT]
|
|
||||||
return `${serviceUrl.origin}/${encodedOwner}/${encodedName}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getServerUrl(url?: string): URL {
|
|
||||||
let urlValue =
|
|
||||||
url && url.trim().length > 0
|
|
||||||
? url
|
|
||||||
: process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
|
||||||
return new URL(urlValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getServerApiUrl(url?: string): string {
|
|
||||||
let apiUrl = 'https://api.github.com'
|
|
||||||
|
|
||||||
if (isGhes(url)) {
|
|
||||||
const serverUrl = getServerUrl(url)
|
|
||||||
apiUrl = new URL(`${serverUrl.origin}/api/v3`).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isGhes(url?: string): boolean {
|
|
||||||
const ghUrl = getServerUrl(url)
|
|
||||||
|
|
||||||
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
|
|
||||||
}
|
|
@ -1,31 +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
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user