forked from actions/checkout
Compare commits
191 Commits
Update-des
...
main
Author | SHA1 | Date | |
---|---|---|---|
357fea8c37 | |||
85e6279cec | |||
009b9ae9e4 | |||
cbb722410c | |||
3b9b8c884f | |||
11bd71901b | |||
e3d2460bbb | |||
163217dfcd | |||
eef61447b9 | |||
6b42224f41 | |||
de5a000abf | |||
d632683dd7 | |||
6d193bf280 | |||
db0cee9a51 | |||
b684943689 | |||
2d7d9f7ff5 | |||
9a9194f871 | |||
dd960bd3c3 | |||
692973e3d9 | |||
6ccd57f4c5 | |||
b17fe1e4d5 | |||
b80ff79f17 | |||
b1ec3021b8 | |||
a5ac7e51b4 | |||
24ed1a3528 | |||
44c2b7a8a4 | |||
8459bc0c7e | |||
3f603f6d5e | |||
fd084cde18 | |||
9c1e94e0ad | |||
0ad4b8fada | |||
43045ae669 | |||
37b082107b | |||
9839dc14a0 | |||
9b4c13b0bf | |||
1d96c772d1 | |||
cd7d8d697e | |||
8410ad0602 | |||
9bb56186c3 | |||
8eb1f6a495 | |||
556e4c3cb0 | |||
b32f140b0c | |||
2650dbd060 | |||
aadec89964 | |||
df0bcddf6d | |||
473055ba18 | |||
b4ffde65f4 | |||
8530928916 | |||
7cdaf2fbc0 | |||
8ade135a41 | |||
c533a0a4cf | |||
72f2cec99f | |||
3df4ab11eb | |||
8b5e8b7687 | |||
97a652b800 | |||
f43a0e5ff2 | |||
7739b9ba2e | |||
96f53100ba | |||
c85c95e3d7 | |||
d106d4669b | |||
f095bcc56b | |||
47fbe2df0a | |||
8e5e7e5ab8 | |||
eb35239ec2 | |||
83b7061638 | |||
40a16ebeed | |||
8f4b7f8486 | |||
cd6a9fd493 | |||
24cb908017 | |||
27135e314d | |||
7b187184d1 | |||
ac59398561 | |||
3ba5ee6fac | |||
8856415920 | |||
755da8c3cf | |||
26d48e8ea1 | |||
bf085276ce | |||
5c3ccc22eb | |||
1f9a0c22da | |||
8230315d06 | |||
93ea575cb5 | |||
6a84743051 | |||
e6d535c99c | |||
2541b1294d | |||
0ffe6f9c55 | |||
dcd71f6466 | |||
add3486cc3 | |||
5126516654 | |||
d50f8ea767 | |||
2d1c1198e7 | |||
a12a3943b4 | |||
8f9e05e482 | |||
230611dbd0 | |||
ec3a7ce113 | |||
fd47087372 | |||
3d677ac575 | |||
826ba42d6c | |||
eb8a193c1d | |||
c49af7ca1f | |||
1e204e9a92 | |||
0299a0d2b6 | |||
be0f448456 | |||
56c00a7b1f | |||
85e47d1a2b | |||
3fc17f8645 | |||
e3bc06d986 | |||
442567ba57 | |||
7f00b66d06 | |||
eccf386318 | |||
2bd2911be9 | |||
afe4af09a7 | |||
25a956c84d | |||
5a4ac9002d | |||
c952173edf | |||
a81bbbf829 | |||
21dc310f19 | |||
be6c44d969 | |||
dac8cc78a1 | |||
2036a08e25 | |||
592cf69a22 | |||
a4b69b4886 | |||
1433f62caa | |||
61b9e3751b | |||
28c7f3d2b5 | |||
fb6f360df2 | |||
b4483adec3 | |||
00a3be8934 | |||
453ee27fca | |||
65865e15a1 | |||
aabbfeb2ce | |||
e52d022eb5 | |||
2ff2fbdea4 | |||
df86c829eb | |||
97b30c411c | |||
86f86b36ef | |||
7523e23789 | |||
ac455590d1 | |||
94c2de77cc | |||
01aecccf73 | |||
85b1f35505 | |||
574281d34c | |||
fbb30c60ab | |||
58070a9fc3 | |||
9a3a9ade82 | |||
b2e6b7ed13 | |||
80602fafba | |||
b4626ce19c | |||
422dc45671 | |||
204620207c | |||
f219062370 | |||
096e927750 | |||
f858c22e96 | |||
77904fd431 | |||
06218e4404 | |||
61fd8fd0c7 | |||
f95f2a3856 | |||
f90c7b395d | |||
090d9c9dfd | |||
db41740e12 | |||
bc50a995b8 | |||
dfd70d4a2d | |||
ae525b2262 | |||
f466b96953 | |||
c85684db76 | |||
299dd5064e | |||
722adc63f1 | |||
3537747199 | |||
a6747255bd | |||
c170eefc26 | |||
a572f640b0 | |||
cab31617d8 | |||
5881116d18 | |||
7990b10a0c | |||
01a434328a | |||
4817b449b0 | |||
689bf84be4 | |||
cc70598ce8 | |||
8461dbfed3 | |||
e347bba93b | |||
50fbc622fc | |||
e8bd1dffb6 | |||
0b496e91ec | |||
f6ce2afa70 | |||
94d077c249 | |||
0963d3b35f | |||
a14471d838 | |||
7f0669ca1f | |||
cacfc4155d | |||
6e6328ef28 | |||
53bed0742e | |||
b4b537b06a |
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
53
.eslintrc.json
Normal file
53
.eslintrc.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"plugins": ["jest", "@typescript-eslint"],
|
||||
"extends": ["plugin:github/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"eslint-comments/no-use": "off",
|
||||
"import/no-namespace": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"camelcase": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
|
||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
||||
"@typescript-eslint/no-array-constructor": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-extraneous-class": "error",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-for-in-array": "error",
|
||||
"@typescript-eslint/no-inferrable-types": "error",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
"@typescript-eslint/prefer-for-of": "warn",
|
||||
"@typescript-eslint/prefer-function-type": "warn",
|
||||
"@typescript-eslint/prefer-includes": "error",
|
||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/require-array-sort-compare": "error",
|
||||
"@typescript-eslint/restrict-plus-operands": "error",
|
||||
"semi": "off",
|
||||
"@typescript-eslint/semi": ["error", "never"],
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/unbound-method": "error"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"jest/globals": true
|
||||
}
|
||||
}
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
.licenses/** -diff linguist-generated=true
|
20
.github/dependabot.yml
vendored
Normal file
20
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
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
Normal file
51
.github/workflows/check-dist.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
# `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@v4.1.6
|
||||
|
||||
- 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
Normal file
58
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
# 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@v4.1.6
|
||||
|
||||
- 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
Normal file
14
.github/workflows/licensed.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
name: Licensed
|
||||
|
||||
on:
|
||||
push: {branches: main}
|
||||
pull_request: {branches: main}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check licenses
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.6
|
||||
- run: npm ci
|
||||
- run: npm run licensed-check
|
20
.github/workflows/publish-immutable-actions.yml
vendored
Normal file
20
.github/workflows/publish-immutable-actions.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: 'Publish Immutable Action Version'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checking out
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish
|
||||
id: publish
|
||||
uses: actions/publish-immutable-action@0.0.3
|
331
.github/workflows/test.yml
vendored
Normal file
331
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,331 @@
|
||||
name: Build and Test
|
||||
|
||||
on:
|
||||
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:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
- uses: actions/checkout@v4.1.6
|
||||
- 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:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
# Basic checkout
|
||||
- name: Checkout basic
|
||||
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@v4.1.6
|
||||
|
||||
# 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@v4.1.6
|
||||
|
||||
# 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@v4.1.6
|
||||
with:
|
||||
path: localClone
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
uses: ./localClone
|
||||
with:
|
||||
ref: test-data/v2/basic
|
||||
- name: Verify basic
|
||||
run: |
|
||||
if [ ! -f "./basic-file.txt" ]; then
|
||||
echo "Expected basic file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify .git folder
|
||||
if [ ! -d "./.git" ]; then
|
||||
echo "Expected ./.git folder to exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify auth token
|
||||
git config --global --add safe.directory "*"
|
||||
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
|
||||
|
||||
# needed to make checkout post cleanup succeed
|
||||
- name: Fix Checkout v4
|
||||
uses: actions/checkout@v4.1.6
|
||||
with:
|
||||
path: localClone
|
||||
|
||||
test-output:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
id: checkout
|
||||
uses: ./
|
||||
with:
|
||||
ref: test-data/v2/basic
|
||||
|
||||
# Verify output
|
||||
- name: Verify output
|
||||
run: |
|
||||
echo "Commit: ${{ steps.checkout.outputs.commit }}"
|
||||
echo "Ref: ${{ steps.checkout.outputs.ref }}"
|
||||
|
||||
if [ "${{ steps.checkout.outputs.ref }}" != "test-data/v2/basic" ]; then
|
||||
echo "Expected ref to be test-data/v2/basic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${{ steps.checkout.outputs.commit }}" != "82f71901cf8c021332310dcc8cdba84c4193ff5d" ]; then
|
||||
echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# needed to make checkout post cleanup succeed
|
||||
- name: Fix Checkout
|
||||
uses: actions/checkout@v4.1.6
|
35
.github/workflows/update-main-version.yml
vendored
Normal file
35
.github/workflows/update-main-version.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
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.6
|
||||
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
Normal file
59
.github/workflows/update-test-ubuntu-git.yml
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
name: Publish test-ubuntu-git Container
|
||||
|
||||
on:
|
||||
# Use an on demand workflow trigger.
|
||||
# (Forked copies of actions/checkout won't have permission to update GHCR.io/actions,
|
||||
# so avoid trigger events that run automatically.)
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
description: 'Publish to ghcr.io? (main branch only)'
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: actions/test-ubuntu-git
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Use `docker/login-action` to log in to GHCR.io.
|
||||
# Once published, the packages are scoped to the account defined here.
|
||||
- name: Log in to the ghcr.io container registry
|
||||
uses: docker/login-action@v3.3.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@v6.5.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
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
__test__/_temp
|
||||
_temp/
|
||||
lib/
|
||||
node_modules/
|
||||
.vscode/
|
14
.licensed.yml
Normal file
14
.licensed.yml
Normal file
@ -0,0 +1,14 @@
|
||||
sources:
|
||||
npm: true
|
||||
|
||||
allowed:
|
||||
- apache-2.0
|
||||
- bsd-2-clause
|
||||
- bsd-3-clause
|
||||
- isc
|
||||
- mit
|
||||
- cc0-1.0
|
||||
- unlicense
|
||||
|
||||
reviewed:
|
||||
npm:
|
20
.licenses/npm/@actions/core.dep.yml
generated
Normal file
20
.licenses/npm/@actions/core.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "@actions/core"
|
||||
version: 1.10.1
|
||||
type: npm
|
||||
summary: Actions core lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/core
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |-
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
20
.licenses/npm/@actions/exec.dep.yml
generated
Normal file
20
.licenses/npm/@actions/exec.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "@actions/exec"
|
||||
version: 1.1.1
|
||||
type: npm
|
||||
summary: Actions exec lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/exec
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |-
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
20
.licenses/npm/@actions/github.dep.yml
generated
Normal file
20
.licenses/npm/@actions/github.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "@actions/github"
|
||||
version: 6.0.0
|
||||
type: npm
|
||||
summary: Actions github lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/github
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |-
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
32
.licenses/npm/@actions/http-client.dep.yml
generated
Normal file
32
.licenses/npm/@actions/http-client.dep.yml
generated
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "@actions/http-client"
|
||||
version: 2.2.1
|
||||
type: npm
|
||||
summary: Actions Http Client
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
Actions Http Client for Node.js
|
||||
|
||||
Copyright (c) GitHub, Inc.
|
||||
|
||||
All rights reserved.
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
20
.licenses/npm/@actions/io.dep.yml
generated
Normal file
20
.licenses/npm/@actions/io.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "@actions/io"
|
||||
version: 1.1.3
|
||||
type: npm
|
||||
summary: Actions io lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/io
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |-
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
20
.licenses/npm/@actions/tool-cache.dep.yml
generated
Normal file
20
.licenses/npm/@actions/tool-cache.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "@actions/tool-cache"
|
||||
version: 2.0.1
|
||||
type: npm
|
||||
summary: Actions tool-cache lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/tool-cache
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |-
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
30
.licenses/npm/@fastify/busboy.dep.yml
generated
Normal file
30
.licenses/npm/@fastify/busboy.dep.yml
generated
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
name: "@fastify/busboy"
|
||||
version: 2.1.1
|
||||
type: npm
|
||||
summary: A streaming parser for HTML form data for node.js
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |-
|
||||
Copyright Brian White. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
notices: []
|
34
.licenses/npm/@octokit/auth-token.dep.yml
generated
Normal file
34
.licenses/npm/@octokit/auth-token.dep.yml
generated
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: "@octokit/auth-token"
|
||||
version: 4.0.0
|
||||
type: npm
|
||||
summary: GitHub API token authentication for browsers and Node.js
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2019 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
34
.licenses/npm/@octokit/core.dep.yml
generated
Normal file
34
.licenses/npm/@octokit/core.dep.yml
generated
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: "@octokit/core"
|
||||
version: 5.2.0
|
||||
type: npm
|
||||
summary: Extendable client for GitHub's REST & GraphQL APIs
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2019 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
34
.licenses/npm/@octokit/endpoint.dep.yml
generated
Normal file
34
.licenses/npm/@octokit/endpoint.dep.yml
generated
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: "@octokit/endpoint"
|
||||
version: 9.0.5
|
||||
type: npm
|
||||
summary: Turns REST API endpoints into generic request options
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2018 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
34
.licenses/npm/@octokit/graphql.dep.yml
generated
Normal file
34
.licenses/npm/@octokit/graphql.dep.yml
generated
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: "@octokit/graphql"
|
||||
version: 7.1.0
|
||||
type: npm
|
||||
summary: GitHub GraphQL API client for browsers and Node
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2018 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
20
.licenses/npm/@octokit/openapi-types-20.0.0.dep.yml
generated
Normal file
20
.licenses/npm/@octokit/openapi-types-20.0.0.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "@octokit/openapi-types"
|
||||
version: 20.0.0
|
||||
type: npm
|
||||
summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |-
|
||||
Copyright 2020 Gregor Martynus
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
20
.licenses/npm/@octokit/openapi-types-22.1.0.dep.yml
generated
Normal file
20
.licenses/npm/@octokit/openapi-types-22.1.0.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "@octokit/openapi-types"
|
||||
version: 22.1.0
|
||||
type: npm
|
||||
summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |-
|
||||
Copyright 2020 Gregor Martynus
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
20
.licenses/npm/@octokit/plugin-paginate-rest.dep.yml
generated
Normal file
20
.licenses/npm/@octokit/plugin-paginate-rest.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "@octokit/plugin-paginate-rest"
|
||||
version: 9.2.1
|
||||
type: npm
|
||||
summary: Octokit plugin to paginate REST API endpoint responses
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
MIT License Copyright (c) 2019 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
20
.licenses/npm/@octokit/plugin-rest-endpoint-methods.dep.yml
generated
Normal file
20
.licenses/npm/@octokit/plugin-rest-endpoint-methods.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "@octokit/plugin-rest-endpoint-methods"
|
||||
version: 10.4.1
|
||||
type: npm
|
||||
summary: Octokit plugin adding one method for all of api.github.com REST API endpoints
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
MIT License Copyright (c) 2019 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
34
.licenses/npm/@octokit/request-error.dep.yml
generated
Normal file
34
.licenses/npm/@octokit/request-error.dep.yml
generated
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: "@octokit/request-error"
|
||||
version: 5.1.0
|
||||
type: npm
|
||||
summary: Error class for Octokit request errors
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2019 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
35
.licenses/npm/@octokit/request.dep.yml
generated
Normal file
35
.licenses/npm/@octokit/request.dep.yml
generated
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
name: "@octokit/request"
|
||||
version: 8.4.0
|
||||
type: npm
|
||||
summary: Send parameterized requests to GitHub's APIs with sensible defaults in browsers
|
||||
and Node
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2018 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
20
.licenses/npm/@octokit/types-12.6.0.dep.yml
generated
Normal file
20
.licenses/npm/@octokit/types-12.6.0.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "@octokit/types"
|
||||
version: 12.6.0
|
||||
type: npm
|
||||
summary: Shared TypeScript definitions for Octokit projects
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
MIT License Copyright (c) 2019 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
20
.licenses/npm/@octokit/types-13.4.1.dep.yml
generated
Normal file
20
.licenses/npm/@octokit/types-13.4.1.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "@octokit/types"
|
||||
version: 13.4.1
|
||||
type: npm
|
||||
summary: Shared TypeScript definitions for Octokit projects
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
MIT License Copyright (c) 2019 Octokit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[MIT](LICENSE)"
|
||||
notices: []
|
214
.licenses/npm/before-after-hook.dep.yml
generated
Normal file
214
.licenses/npm/before-after-hook.dep.yml
generated
Normal file
@ -0,0 +1,214 @@
|
||||
---
|
||||
name: before-after-hook
|
||||
version: 2.2.3
|
||||
type: npm
|
||||
summary: asynchronous before/error/after hooks for internal functionality
|
||||
homepage:
|
||||
license: apache-2.0
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |2
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Gregor Martynus and other contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
- sources: README.md
|
||||
text: "[Apache 2.0](LICENSE)"
|
||||
notices: []
|
28
.licenses/npm/deprecation.dep.yml
generated
Normal file
28
.licenses/npm/deprecation.dep.yml
generated
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: deprecation
|
||||
version: 2.3.1
|
||||
type: npm
|
||||
summary: Log a deprecation message with stack
|
||||
homepage: https://github.com/gr2m/deprecation#readme
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Gregor Martynus and contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[ISC](LICENSE)"
|
||||
notices: []
|
26
.licenses/npm/once.dep.yml
generated
Normal file
26
.licenses/npm/once.dep.yml
generated
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
name: once
|
||||
version: 1.4.0
|
||||
type: npm
|
||||
summary: Run a function exactly one time
|
||||
homepage: https://github.com/isaacs/once#readme
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
notices: []
|
26
.licenses/npm/semver.dep.yml
generated
Normal file
26
.licenses/npm/semver.dep.yml
generated
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
name: semver
|
||||
version: 6.3.1
|
||||
type: npm
|
||||
summary: The semantic version parser used by npm.
|
||||
homepage:
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
notices: []
|
35
.licenses/npm/tunnel.dep.yml
generated
Normal file
35
.licenses/npm/tunnel.dep.yml
generated
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
name: tunnel
|
||||
version: 0.0.6
|
||||
type: npm
|
||||
summary: Node HTTP/HTTPS Agents for tunneling proxies
|
||||
homepage: https://github.com/koichik/node-tunnel/
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012 Koichi Kobayashi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: Licensed under the [MIT](https://github.com/koichik/node-tunnel/blob/master/LICENSE)
|
||||
license.
|
||||
notices: []
|
34
.licenses/npm/undici.dep.yml
generated
Normal file
34
.licenses/npm/undici.dep.yml
generated
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: undici
|
||||
version: 5.28.4
|
||||
type: npm
|
||||
summary: An HTTP/1.1 client, written from scratch for Node.js
|
||||
homepage: https://undici.nodejs.org
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
MIT License
|
||||
|
||||
Copyright (c) Matteo Collina and Undici contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
- sources: README.md
|
||||
text: MIT
|
||||
notices: []
|
20
.licenses/npm/universal-user-agent.dep.yml
generated
Normal file
20
.licenses/npm/universal-user-agent.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: universal-user-agent
|
||||
version: 6.0.1
|
||||
type: npm
|
||||
summary: Get a user agent string in both browser and node
|
||||
homepage:
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |
|
||||
# [ISC License](https://spdx.org/licenses/ISC)
|
||||
|
||||
Copyright (c) 2018, Gregor Martynus (https://github.com/gr2m)
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[ISC](LICENSE.md)"
|
||||
notices: []
|
39
.licenses/npm/uuid-3.4.0.dep.yml
generated
Normal file
39
.licenses/npm/uuid-3.4.0.dep.yml
generated
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
name: uuid
|
||||
version: 3.4.0
|
||||
type: npm
|
||||
summary: RFC4122 (v1, v4, and v5) UUIDs
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010-2016 Robert Kieffer and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
notices:
|
||||
- sources: AUTHORS
|
||||
text: |-
|
||||
Robert Kieffer <robert@broofa.com>
|
||||
Christoph Tavan <dev@tavan.de>
|
||||
AJ ONeal <coolaj86@gmail.com>
|
||||
Vincent Voyer <vincent@zeroload.net>
|
||||
Roman Shtylman <shtylman@gmail.com>
|
20
.licenses/npm/uuid-8.3.2.dep.yml
generated
Normal file
20
.licenses/npm/uuid-8.3.2.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: uuid
|
||||
version: 8.3.2
|
||||
type: npm
|
||||
summary: RFC4122 (v1, v4, and v5) UUIDs
|
||||
homepage: https://github.com/uuidjs/uuid#readme
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010-2020 Robert Kieffer and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
20
.licenses/npm/uuid-9.0.1.dep.yml
generated
Normal file
20
.licenses/npm/uuid-9.0.1.dep.yml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: uuid
|
||||
version: 9.0.1
|
||||
type: npm
|
||||
summary: RFC4122 (v1, v4, and v5) UUIDs
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010-2020 Robert Kieffer and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
26
.licenses/npm/wrappy.dep.yml
generated
Normal file
26
.licenses/npm/wrappy.dep.yml
generated
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
name: wrappy
|
||||
version: 1.0.2
|
||||
type: npm
|
||||
summary: Callback wrapping utility
|
||||
homepage: https://github.com/npm/wrappy
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
notices: []
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
11
.prettierrc.json
Normal file
11
.prettierrc.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "avoid",
|
||||
"parser": "typescript"
|
||||
}
|
160
CHANGELOG.md
Normal file
160
CHANGELOG.md
Normal file
@ -0,0 +1,160 @@
|
||||
# 更新日志
|
||||
|
||||
## v4.2.2
|
||||
* `url-helper.ts` 现在利用众所周知的环境变量,由 @jww3 在 https://github.com/actions/checkout/pull/1941 中贡献
|
||||
* 扩展 `isGhes` 的单元测试覆盖率,由 @jww3 在 https://github.com/actions/checkout/pull/1946 中贡献
|
||||
|
||||
## v4.2.1
|
||||
* 如果提供了 commit,则检出其他 refs/*,否则回退到 ref,由 @orhantoy 在 https://github.com/actions/checkout/pull/1924 中贡献
|
||||
|
||||
## v4.2.0
|
||||
|
||||
* 添加 Ref 和 Commit 输出,由 @lucacome 在 https://github.com/actions/checkout/pull/1180 中贡献
|
||||
* 依赖项更新,由 @dependabot 在 https://github.com/actions/checkout/pull/1777 和 https://github.com/actions/checkout/pull/1872 中贡献
|
||||
|
||||
## v4.1.7
|
||||
* 在 1 个目录中更新次要 npm 依赖项组,共 4 次更新,由 @dependabot 在 https://github.com/actions/checkout/pull/1739 中贡献
|
||||
* 将 actions/checkout 从 3 升级到 4,由 @dependabot 在 https://github.com/actions/checkout/pull/1697 中贡献
|
||||
* 按 commit 检出其他 refs/*,由 @orhantoy 在 https://github.com/actions/checkout/pull/1774 中贡献
|
||||
* 将 actions/checkout 自身的工作流固定到已知、良好、稳定的版本,由 @jww3 在 https://github.com/actions/checkout/pull/1776 中贡献
|
||||
|
||||
## v4.1.6
|
||||
* 检查平台以适当设置存档扩展名,由 @cory-miller 在 https://github.com/actions/checkout/pull/1732 中贡献
|
||||
|
||||
## v4.1.5
|
||||
* 更新 NPM 依赖项,由 @cory-miller 在 https://github.com/actions/checkout/pull/1703 中贡献
|
||||
* 将 github/codeql-action 从 2 升级到 3,由 @dependabot 在 https://github.com/actions/checkout/pull/1694 中贡献
|
||||
* 将 actions/setup-node 从 1 升级到 4,由 @dependabot 在 https://github.com/actions/checkout/pull/1696 中贡献
|
||||
* 将 actions/upload-artifact 从 2 升级到 4,由 @dependabot 在 https://github.com/actions/checkout/pull/1695 中贡献
|
||||
* README:建议将 `user.email` 设置为 `41898282+github-actions[bot]@users.noreply.github.com`,由 @cory-miller 在 https://github.com/actions/checkout/pull/1707 中贡献
|
||||
|
||||
## v4.1.4
|
||||
- 禁用 `sparse-checkout` 时禁用 `extensions.worktreeConfig`,由 @jww3 在 https://github.com/actions/checkout/pull/1692 中贡献
|
||||
- 添加 dependabot 配置,由 @cory-miller 在 https://github.com/actions/checkout/pull/1688 中贡献
|
||||
- 使用 2 次更新更新次要 actions 依赖项组,由 @dependabot 在 https://github.com/actions/checkout/pull/1693 中贡献
|
||||
- 将 word-wrap 从 1.2.3 升级到 1.2.5,由 @dependabot 在 https://github.com/actions/checkout/pull/1643 中贡献
|
||||
|
||||
## v4.1.3
|
||||
- 在尝试禁用 `sparse-checkout` 之前检查 git 版本,由 @jww3 在 https://github.com/actions/checkout/pull/1656 中贡献
|
||||
- 添加 SSH 用户参数,由 @cory-miller 在 https://github.com/actions/checkout/pull/1685 中贡献
|
||||
- 在 `update-main-version.yml` 中更新 `actions/checkout` 版本,由 @jww3 在 https://github.com/actions/checkout/pull/1650 中贡献
|
||||
|
||||
## v4.1.2
|
||||
- 修复:当 `sparse-checkout` 选项不存在时禁用稀疏检出,由 @dscho 在 https://github.com/actions/checkout/pull/1598 中贡献
|
||||
|
||||
## v4.1.1
|
||||
- 修正指向 GitHub 文档的链接,由 @peterbe 在 https://github.com/actions/checkout/pull/1511 中贡献
|
||||
- 从“新特性”部分链接到发布页面,由 @cory-miller 在 https://github.com/actions/checkout/pull/1514 中贡献
|
||||
|
||||
## v4.1.0
|
||||
- [添加对部分检出过滤器的支持](https://github.com/actions/checkout/pull/1396)
|
||||
|
||||
## v4.0.0
|
||||
- [支持不带 --progress 选项的获取](https://github.com/actions/checkout/pull/1067)
|
||||
- [升级到 node20](https://github.com/actions/checkout/pull/1436)
|
||||
|
||||
## v3.6.0
|
||||
- [修复:将测试脚本标记为通过 Bash 运行的 Bash'isms](https://github.com/actions/checkout/pull/1377)
|
||||
- [添加选项以在 fetch-depth > 0 时获取标签](https://github.com/actions/checkout/pull/579)
|
||||
|
||||
## v3.5.3
|
||||
- [修复:在自托管运行器上检出失败,当有故障的子模块被签入时](https://github.com/actions/checkout/pull/1196)
|
||||
- [修复 codespell 发现的拼写错误](https://github.com/actions/checkout/pull/1287)
|
||||
- [添加对稀疏检出的支持](https://github.com/actions/checkout/pull/1369)
|
||||
|
||||
## v3.5.2
|
||||
- [修复 GHES 的 api 端点](https://github.com/actions/checkout/pull/1289)
|
||||
|
||||
## v3.5.1
|
||||
- [修复 Windows 上的慢速检出](https://github.com/actions/checkout/pull/1246)
|
||||
|
||||
## v3.5.0
|
||||
* [添加新的 known_hosts 公钥](https://github.com/actions/checkout/pull/1237)
|
||||
|
||||
## v3.4.0
|
||||
- [将 codeql actions 升级到 v2](https://github.com/actions/checkout/pull/1209)
|
||||
- [升级依赖项](https://github.com/actions/checkout/pull/1210)
|
||||
- [升级 @actions/io](https://github.com/actions/checkout/pull/1225)
|
||||
|
||||
## v3.3.0
|
||||
- [通过 exec 函数的回调实现分支列表](https://github.com/actions/checkout/pull/1045)
|
||||
- [明确引用私有检出选项](https://github.com/actions/checkout/pull/1050)
|
||||
- [修复评论中的拼写错误(在 #770 中添加)](https://github.com/actions/checkout/pull/1057)
|
||||
|
||||
## v3.2.0
|
||||
- [添加 GitHub Action 以执行发布](https://github.com/actions/checkout/pull/942)
|
||||
- [修复状态徽章](https://github.com/actions/checkout/pull/967)
|
||||
- [将 datadog/squid 替换为 ubuntu/squid Docker 镜像](https://github.com/actions/checkout/pull/1002)
|
||||
- [在 submoduleForeach 的管道命令周围添加引号](https://github.com/actions/checkout/pull/964)
|
||||
- [将 @actions/io 升级到 1.1.2](https://github.com/actions/checkout/pull/1029)
|
||||
- [升级到版本 3.2.0](https://github.com/actions/checkout/pull/1039)
|
||||
|
||||
## v3.1.0
|
||||
- [使用 @actions/core `saveState` 和 `getState`](https://github.com/actions/checkout/pull/939)
|
||||
- [添加 `github-server-url` 输入](https://github.com/actions/checkout/pull/922)
|
||||
|
||||
## v3.0.2
|
||||
- [添加输入 `set-safe-directory`](https://github.com/actions/checkout/pull/770)
|
||||
|
||||
## v3.0.1
|
||||
- [修复了一个问题,由于新的 git 设置 `safe.directory`,导致在容器作业中无法运行 checkout](https://github.com/actions/checkout/pull/762)
|
||||
- [提升各种 npm 包版本](https://github.com/actions/checkout/pull/744)
|
||||
|
||||
## v3.0.0
|
||||
|
||||
- [升级到 node 16](https://github.com/actions/checkout/pull/689)
|
||||
|
||||
## v2.3.1
|
||||
|
||||
- [修复 .wiki 的默认分支解析以及使用 SSH 时的解析](https://github.com/actions/checkout/pull/284)
|
||||
|
||||
## v2.3.0
|
||||
|
||||
- [回退到默认分支](https://github.com/actions/checkout/pull/278)
|
||||
|
||||
## v2.2.0
|
||||
|
||||
- [当 fetch-depth=0 时获取所有标签和分支的所有历史记录](https://github.com/actions/checkout/pull/258)
|
||||
|
||||
## v2.1.1
|
||||
|
||||
- 支持 GHES 的更改([这里](https://github.com/actions/checkout/pull/236) 和 [这里](https://github.com/actions/checkout/pull/248))
|
||||
|
||||
## v2.1.0
|
||||
|
||||
- [分组输出](https://github.com/actions/checkout/pull/191)
|
||||
- [支持 GHES alpha 发布的更改](https://github.com/actions/checkout/pull/199)
|
||||
- [为子模块持久化 core.sshCommand](https://github.com/actions/checkout/pull/184)
|
||||
- [添加对 ssh 的支持](https://github.com/actions/checkout/pull/163)
|
||||
- [当不使用 SSH 时,将子模块 SSH URL 转换为 HTTPS](https://github.com/actions/checkout/pull/179)
|
||||
- [添加子模块支持](https://github.com/actions/checkout/pull/157)
|
||||
- [遵循代理设置](https://github.com/actions/checkout/pull/144)
|
||||
- [修复 pr 关闭事件的 ref,当 pr 被合并时](https://github.com/actions/checkout/pull/141)
|
||||
- [修复 git 版本低于 2.22 时检查分离状态的问题](https://github.com/actions/checkout/pull/128)
|
||||
|
||||
## v2.0.0
|
||||
|
||||
- [不在命令行上传递凭据](https://github.com/actions/checkout/pull/108)
|
||||
- [添加输入 persist-credentials](https://github.com/actions/checkout/pull/107)
|
||||
- [回退到 REST API 下载仓库](https://github.com/actions/checkout/pull/104)
|
||||
|
||||
## v2 (beta)
|
||||
|
||||
- 改进的获取性能
|
||||
- 默认行为现在仅获取正在检出的 SHA
|
||||
- 脚本认证的 git 命令
|
||||
- 将 `with.token` 持久化到本地 git 配置
|
||||
- 使您的脚本可以运行经过身份验证的 git 命令
|
||||
- 作业后清理移除令牌
|
||||
- 即将推出:通过将 `with.persist-credentials` 设置为 `false` 来选择退出
|
||||
- 创建本地分支
|
||||
- 检出分支时不再是分离的 HEAD
|
||||
- 创建一个带有相应上游分支设置的本地分支
|
||||
- 改进的布局
|
||||
- `with.path` 始终相对于 `github.workspace`
|
||||
- 更好地与容器操作对齐,其中 `github.workspace` 被映射进来
|
||||
- 移除输入 `submodules`
|
||||
|
||||
## v1
|
||||
|
||||
有关 V1 更新日志,请参阅 [这里](https://github.com/actions/checkout/blob/v1/CHANGELOG.md)
|
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
@ -0,0 +1 @@
|
||||
* @actions/actions-launch
|
28
CONTRIBUTING.md
Normal file
28
CONTRIBUTING.md
Normal file
@ -0,0 +1,28 @@
|
||||
# 贡献指南
|
||||
|
||||
## 提交拉取请求
|
||||
|
||||
1. Fork 并克隆仓库
|
||||
1. 配置并安装依赖项:`npm install`
|
||||
1. 创建新分支:`git checkout -b my-branch-name`
|
||||
1. 进行更改,添加测试,并确保测试仍然通过:`npm run test`
|
||||
1. 确保代码格式正确:`npm run format`
|
||||
1. 使用 `npm run build` 更新 `dist/index.js`。这将创建一个作为操作入口点的单一 JavaScript 文件
|
||||
1. 推送到您的 Fork 并提交拉取请求
|
||||
1. 拍拍自己的背,等待您的拉取请求被审查和合并
|
||||
|
||||
以下是一些可以提高您的拉取请求被接受可能性的建议:
|
||||
|
||||
- 编写测试。
|
||||
- 尽量保持更改的专注性。如果您希望进行多个不相互依赖的更改,建议将它们作为单独的拉取请求提交。
|
||||
|
||||
## 资源
|
||||
|
||||
- [如何为开源做贡献](https://opensource.guide/how-to-contribute/)
|
||||
- [使用拉取请求](https://help.github.com/articles/about-pull-requests/)
|
||||
- [GitHub 帮助](https://help.github.com)
|
||||
- [编写良好的提交信息](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
|
||||
|
||||
谢谢!:heart: :heart: :heart:
|
||||
|
||||
GitHub Actions 团队 :octocat:
|
334
README.md
334
README.md
@ -1,22 +1,330 @@
|
||||
# checkout
|
||||
[](https://github.com/actions/checkout/actions/workflows/test.yml)
|
||||
|
||||
This action checks out your repository so that your workflow operates from the root of the repository
|
||||
# Checkout V4
|
||||
|
||||
# Usage
|
||||
此操作将您的仓库检出到 `$GITHUB_WORKSPACE` 下,以便您的工作流可以访问它。
|
||||
|
||||
See [action.yml](action.yml)
|
||||
默认情况下,仅获取触发工作流的 ref/SHA 的单个提交。设置 `fetch-depth: 0` 可以获取所有分支和标签的所有历史记录。请参考 [这里](https://docs.github.com/actions/using-workflows/events-that-trigger-workflows) 了解不同事件中 `$GITHUB_SHA` 指向的提交。
|
||||
|
||||
Basic:
|
||||
认证令牌会持久化到本地 git 配置中。这使得您的脚本可以运行经过身份验证的 git 命令。该令牌会在作业后清理步骤中被移除。设置 `persist-credentials: false` 可以选择不保留。
|
||||
|
||||
当 Git 2.18 或更高版本不在您的 PATH 中时,将回退到 REST API 来下载文件。
|
||||
|
||||
# 新特性
|
||||
|
||||
请参考 [发布页面](https://github.com/actions/checkout/releases/latest) 获取最新的发布说明。
|
||||
|
||||
# 使用方法
|
||||
|
||||
<!-- start usage -->
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/setup-node@master
|
||||
• uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
version: 10.x
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
# 仓库名称及所有者。例如,actions/checkout
|
||||
# 默认值: ${{ github.repository }}
|
||||
repository: ''
|
||||
|
||||
# 要检出的分支、标签或 SHA。当检出触发工作流的仓库时,默认为该事件的引用或 SHA。
|
||||
# 否则,使用默认分支。
|
||||
ref: ''
|
||||
|
||||
# 用于获取仓库的个人访问令牌 (PAT)。该 PAT 会配置到本地 git 配置中,使您的脚本可以运行经过身份验证的 git 命令。
|
||||
# 作业后步骤会移除该 PAT。
|
||||
#
|
||||
# 我们建议使用具有最低必要权限的服务账户。此外,在生成新的 PAT 时,请选择最低必要的范围。
|
||||
#
|
||||
# [了解有关创建和使用加密机密的更多信息](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
|
||||
#
|
||||
# 默认值: ${{ github.token }}
|
||||
token: ''
|
||||
|
||||
# 用于获取仓库的 SSH 密钥。该 SSH 密钥会配置到本地 git 配置中,使您的脚本可以运行经过身份验证的 git 命令。
|
||||
# 作业后步骤会移除该 SSH 密钥。
|
||||
#
|
||||
# 我们建议使用具有最低必要权限的服务账户。
|
||||
#
|
||||
# [了解有关创建和使用加密机密的更多信息](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
|
||||
ssh-key: ''
|
||||
|
||||
# 除用户和全局主机密钥数据库之外的已知主机。可以使用 `ssh-keyscan` 工具获取主机的公共 SSH 密钥。例如,`ssh-keyscan github.com`。
|
||||
# github.com 的公钥始终会隐式添加。
|
||||
ssh-known-hosts: ''
|
||||
|
||||
# 是否执行严格的主机密钥检查。当为 true 时,会在 SSH 命令行中添加 `StrictHostKeyChecking=yes` 和 `CheckHostIP=no` 选项。
|
||||
# 使用 `ssh-known-hosts` 输入来配置其他主机。
|
||||
# 默认值: true
|
||||
ssh-strict: ''
|
||||
|
||||
# 连接到远程 SSH 主机时使用的用户。默认值为 'git'。
|
||||
# 默认值: git
|
||||
ssh-user: ''
|
||||
|
||||
# 是否将令牌或 SSH 密钥配置到本地 git 配置中
|
||||
# 默认值: true
|
||||
persist-credentials: ''
|
||||
|
||||
# 在 $GITHUB_WORKSPACE 下放置仓库的相对路径
|
||||
path: ''
|
||||
|
||||
# 是否在获取前执行 `git clean -ffdx && git reset --hard HEAD`
|
||||
# 默认值: true
|
||||
clean: ''
|
||||
|
||||
# 针对给定过滤器进行部分克隆。如果设置了此选项,则会覆盖 sparse-checkout。
|
||||
# 默认值: null
|
||||
filter: ''
|
||||
|
||||
# 对给定模式进行稀疏检出。每个模式应以换行符分隔。
|
||||
# 默认值: null
|
||||
sparse-checkout: ''
|
||||
|
||||
# 指定在执行稀疏检出时是否使用 cone 模式。
|
||||
# 默认值: true
|
||||
sparse-checkout-cone-mode: ''
|
||||
|
||||
# 要获取的提交数量。0 表示所有分支和标签的所有历史记录。
|
||||
# 默认值: 1
|
||||
fetch-depth: ''
|
||||
|
||||
# 是否获取标签,即使 fetch-depth > 0。
|
||||
# 默认值: false
|
||||
fetch-tags: ''
|
||||
|
||||
# 获取时是否显示进度状态输出。
|
||||
# 默认值: true
|
||||
show-progress: ''
|
||||
|
||||
# 是否下载 Git-LFS 文件
|
||||
# 默认值: false
|
||||
lfs: ''
|
||||
|
||||
# 是否检出子模块:`true` 表示检出子模块,`recursive` 表示递归检出子模块。
|
||||
#
|
||||
# 当未提供 `ssh-key` 输入时,以 `git@github.com:` 开头的 SSH URL 会被转换为 HTTPS。
|
||||
#
|
||||
# 默认值: false
|
||||
submodules: ''
|
||||
|
||||
# 通过运行 `git config --global --add safe.directory <path>` 将仓库路径添加为 Git 全局配置的安全目录。
|
||||
# 默认值: true
|
||||
set-safe-directory: ''
|
||||
|
||||
# 尝试克隆的 GitHub 实例的基本 URL,将使用环境默认值从工作流运行的同一实例获取,除非指定。示例 URL 包括 https://github.com 或 https://my-ghes-server.example.com
|
||||
github-server-url: ''
|
||||
```
|
||||
<!-- end usage -->
|
||||
|
||||
# 场景
|
||||
|
||||
- [仅获取根文件](#Fetch-only-the-root-files)
|
||||
- [仅获取根文件和 `.github` 及 `src` 文件夹](#Fetch-only-the-root-files-and-github-and-src-folder)
|
||||
- [仅获取单个文件](#Fetch-only-a-single-file)
|
||||
- [获取所有标签和分支的所有历史记录](#Fetch-all-history-for-all-tags-and-branches)
|
||||
- [检出不同分支](#Checkout-a-different-branch)
|
||||
- [检出 HEAD^](#Checkout-HEAD)
|
||||
- [并行检出多个仓库](#Checkout-multiple-repos-side-by-side)
|
||||
- [嵌套检出多个仓库](#Checkout-multiple-repos-nested)
|
||||
- [检出多个私有仓库](#Checkout-multiple-repos-private)
|
||||
- [检出拉取请求的 HEAD 提交而不是合并提交](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit)
|
||||
- [在关闭事件上检出拉取请求](#Checkout-pull-request-on-closed-event)
|
||||
- [使用内置令牌推送提交](#Push-a-commit-using-the-built-in-token)
|
||||
- [使用内置令牌向拉取请求推送提交](#Push-a-commit-to-a-PR-using-the-built-in-token)
|
||||
|
||||
## 仅获取根文件
|
||||
|
||||
```yaml
|
||||
• uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
sparse-checkout: .
|
||||
```
|
||||
|
||||
# License
|
||||
## 仅获取根文件和 `.github` 及 `src` 文件夹
|
||||
|
||||
The scripts and documentation in this project are released under the [MIT License](LICENSE)
|
||||
```yaml
|
||||
• uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github
|
||||
src
|
||||
```
|
||||
|
||||
## 仅获取单个文件
|
||||
|
||||
```yaml
|
||||
• uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
sparse-checkout: |
|
||||
README.md
|
||||
sparse-checkout-cone-mode: false
|
||||
```
|
||||
|
||||
## 获取所有标签和分支的所有历史记录
|
||||
|
||||
```yaml
|
||||
• uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
fetch-depth: 0
|
||||
```
|
||||
|
||||
## 检出不同分支
|
||||
|
||||
```yaml
|
||||
• uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
ref: my-branch
|
||||
```
|
||||
|
||||
## 检出 HEAD^
|
||||
|
||||
```yaml
|
||||
• uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
fetch-depth: 2
|
||||
• run: git checkout HEAD^
|
||||
|
||||
```
|
||||
|
||||
## 并行检出多个仓库
|
||||
|
||||
```yaml
|
||||
• name: Checkout
|
||||
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: main
|
||||
|
||||
• name: Checkout tools repo
|
||||
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: my-org/my-tools
|
||||
path: my-tools
|
||||
```
|
||||
> - 如果您的次要仓库是私有或内部的,您需要添加 [Checkout multiple repos (private)](#Checkout-multiple-repos-private) 中提到的选项
|
||||
|
||||
## 嵌套检出多个仓库
|
||||
|
||||
```yaml
|
||||
• name: Checkout
|
||||
|
||||
uses: actions/checkout@v4
|
||||
|
||||
• name: Checkout tools repo
|
||||
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: my-org/my-tools
|
||||
path: my-tools
|
||||
```
|
||||
> - 如果您的次要仓库是私有或内部的,您需要添加 [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` 是包含您 PAT 的机密
|
||||
path: my-tools
|
||||
```
|
||||
> - `${{ github.token }}` 的范围限定在当前仓库,因此如果您想检出其他私有仓库,则需要提供您自己的 [PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line)。
|
||||
|
||||
## 检出拉取请求的 HEAD 提交而不是合并提交
|
||||
|
||||
```yaml
|
||||
• uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
```
|
||||
|
||||
## 在关闭事件上检出拉取请求
|
||||
|
||||
```yaml
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize, closed]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
◦ uses: actions/checkout@v4
|
||||
|
||||
```
|
||||
|
||||
## 使用内置令牌推送提交
|
||||
|
||||
```yaml
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
◦ uses: actions/checkout@v4
|
||||
|
||||
◦ run: |
|
||||
|
||||
date > generated.txt
|
||||
# 注意:以下账户信息在 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
|
||||
```
|
||||
*注意:* 用户邮箱为 `{user.id}+{user.login}@users.noreply.github.com`。请参阅用户 API: https://api.github.com/users/github-actions%5Bbot%5D
|
||||
|
||||
## 使用内置令牌向拉取请求推送提交
|
||||
|
||||
在拉取请求触发器中,需要指定 `ref`,因为 GitHub Actions 默认以分离的 HEAD 模式检出,这意味着它不会默认检出您的分支。
|
||||
|
||||
```yaml
|
||||
on: pull_request
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
◦ uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
◦ run: |
|
||||
|
||||
date > generated.txt
|
||||
# 注意:以下账户信息在 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
|
||||
```
|
||||
*注意:* 用户邮箱为 `{user.id}+{user.login}@users.noreply.github.com`。请参阅用户 API: https://api.github.com/users/github-actions%5Bbot%5D
|
||||
|
||||
# 推荐权限
|
||||
|
||||
在 GitHub Actions 工作流中使用 `checkout` 操作时,建议设置以下 `GITHUB_TOKEN` 权限以确保正常功能,除非通过 `token` 或 `ssh-key` 输入提供了替代身份验证:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read
|
||||
```
|
||||
|
||||
# 许可证
|
||||
|
||||
此项目中的脚本和文档均根据 [MIT 许可证](LICENSE) 发布
|
856
__test__/git-auth-helper.test.ts
Normal file
856
__test__/git-auth-helper.test.ts
Normal file
@ -0,0 +1,856 @@
|
||||
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]
|
||||
}
|
378
__test__/git-command-manager.test.ts
Normal file
378
__test__/git-command-manager.test.ts
Normal file
@ -0,0 +1,378 @@
|
||||
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)
|
||||
)
|
||||
})
|
||||
})
|
507
__test__/git-directory-helper.test.ts
Normal file
507
__test__/git-directory-helper.test.ts
Normal file
@ -0,0 +1,507 @@
|
||||
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()
|
||||
}
|
||||
}
|
86
__test__/git-version.test.ts
Normal file
86
__test__/git-version.test.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import {GitVersion} from '../src/git-version'
|
||||
import {MinimumGitSparseCheckoutVersion} from '../src/git-command-manager'
|
||||
|
||||
describe('git-version tests', () => {
|
||||
it('basics', async () => {
|
||||
let version = new GitVersion('')
|
||||
expect(version.isValid()).toBeFalsy()
|
||||
|
||||
version = new GitVersion('asdf')
|
||||
expect(version.isValid()).toBeFalsy()
|
||||
|
||||
version = new GitVersion('1.2')
|
||||
expect(version.isValid()).toBeTruthy()
|
||||
expect(version.toString()).toBe('1.2')
|
||||
|
||||
version = new GitVersion('1.2.3')
|
||||
expect(version.isValid()).toBeTruthy()
|
||||
expect(version.toString()).toBe('1.2.3')
|
||||
})
|
||||
|
||||
it('check minimum', async () => {
|
||||
let version = new GitVersion('4.5')
|
||||
expect(version.checkMinimum(new GitVersion('3.6'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('3.6.7'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.4'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.5'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.5.0'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.6'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('4.6.0'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('5.1'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('5.1.2'))).toBeFalsy()
|
||||
|
||||
version = new GitVersion('4.5.6')
|
||||
expect(version.checkMinimum(new GitVersion('3.6'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('3.6.7'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.4'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.5'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.5.5'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.5.6'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.5.7'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('4.6'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('4.6.0'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('5.1'))).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()
|
||||
})
|
||||
})
|
147
__test__/input-helper.test.ts
Normal file
147
__test__/input-helper.test.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import * as core from '@actions/core'
|
||||
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 workflowContextHelper from '../lib/workflow-context-helper'
|
||||
import {IGitSourceSettings} from '../lib/git-source-settings'
|
||||
|
||||
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||
const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
|
||||
|
||||
// Inputs for mock @actions/core
|
||||
let inputs = {} as any
|
||||
|
||||
// Shallow clone original @actions/github context
|
||||
let originalContext = {...github.context}
|
||||
|
||||
describe('input-helper tests', () => {
|
||||
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
|
||||
process.env['GITHUB_WORKSPACE'] = gitHubWorkspace
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset inputs
|
||||
inputs = {}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
// Restore GitHub workspace
|
||||
delete process.env['GITHUB_WORKSPACE']
|
||||
if (originalGitHubWorkspace) {
|
||||
process.env['GITHUB_WORKSPACE'] = originalGitHubWorkspace
|
||||
}
|
||||
|
||||
// Restore @actions/github context
|
||||
github.context.ref = originalContext.ref
|
||||
github.context.sha = originalContext.sha
|
||||
|
||||
// Restore
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('sets defaults', async () => {
|
||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||
expect(settings).toBeTruthy()
|
||||
expect(settings.authToken).toBeFalsy()
|
||||
expect(settings.clean).toBe(true)
|
||||
expect(settings.commit).toBeTruthy()
|
||||
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
|
||||
expect(settings.filter).toBe(undefined)
|
||||
expect(settings.sparseCheckout).toBe(undefined)
|
||||
expect(settings.sparseCheckoutConeMode).toBe(true)
|
||||
expect(settings.fetchDepth).toBe(1)
|
||||
expect(settings.fetchTags).toBe(false)
|
||||
expect(settings.showProgress).toBe(true)
|
||||
expect(settings.lfs).toBe(false)
|
||||
expect(settings.ref).toBe('refs/heads/some-ref')
|
||||
expect(settings.repositoryName).toBe('some-repo')
|
||||
expect(settings.repositoryOwner).toBe('some-owner')
|
||||
expect(settings.repositoryPath).toBe(gitHubWorkspace)
|
||||
expect(settings.setSafeDirectory).toBe(true)
|
||||
})
|
||||
|
||||
it('qualifies ref', async () => {
|
||||
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'
|
||||
try {
|
||||
await inputHelper.getInputs()
|
||||
throw 'should not reach here'
|
||||
} catch (err) {
|
||||
expect(`(${(err as any).message}`).toMatch(
|
||||
"Invalid repository 'some-unqualified-repo'"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('roots path', async () => {
|
||||
inputs.path = 'some-directory/some-subdirectory'
|
||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||
expect(settings.repositoryPath).toBe(
|
||||
path.join(gitHubWorkspace, 'some-directory', 'some-subdirectory')
|
||||
)
|
||||
})
|
||||
|
||||
it('sets ref to empty when explicit sha', async () => {
|
||||
inputs.ref = '1111111111222222222233333333334444444444'
|
||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||
expect(settings.ref).toBeFalsy()
|
||||
expect(settings.commit).toBe('1111111111222222222233333333334444444444')
|
||||
})
|
||||
|
||||
it('sets sha to empty when explicit ref', async () => {
|
||||
inputs.ref = 'refs/heads/some-other-ref'
|
||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||
expect(settings.ref).toBe('refs/heads/some-other-ref')
|
||||
expect(settings.commit).toBeFalsy()
|
||||
})
|
||||
|
||||
it('sets workflow organization ID', async () => {
|
||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||
expect(settings.workflowOrganizationId).toBe(123456)
|
||||
})
|
||||
})
|
10
__test__/modify-work-tree.sh
Executable file
10
__test__/modify-work-tree.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f "./basic/basic-file.txt" ]; then
|
||||
echo "Expected basic file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo hello >> ./basic/basic-file.txt
|
||||
echo hello >> ./basic/new-file.txt
|
||||
git -C ./basic status
|
6
__test__/override-git-version.cmd
Executable file
6
__test__/override-git-version.cmd
Executable file
@ -0,0 +1,6 @@
|
||||
|
||||
mkdir override-git-version
|
||||
cd override-git-version
|
||||
echo @echo override git version 1.2.3 > git.cmd
|
||||
echo "%CD%" >> $GITHUB_PATH
|
||||
cd ..
|
9
__test__/override-git-version.sh
Executable file
9
__test__/override-git-version.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/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 ..
|
190
__test__/ref-helper.test.ts
Normal file
190
__test__/ref-helper.test.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import * as assert from 'assert'
|
||||
import * as refHelper from '../lib/ref-helper'
|
||||
import {IGitCommandManager} from '../lib/git-command-manager'
|
||||
|
||||
const commit = '1234567890123456789012345678901234567890'
|
||||
let git: IGitCommandManager
|
||||
|
||||
describe('ref-helper tests', () => {
|
||||
beforeEach(() => {
|
||||
git = {} as unknown as IGitCommandManager
|
||||
})
|
||||
|
||||
it('getCheckoutInfo requires git', async () => {
|
||||
const git = null as unknown as IGitCommandManager
|
||||
try {
|
||||
await refHelper.getCheckoutInfo(git, 'refs/heads/my/branch', commit)
|
||||
throw new Error('Should not reach here')
|
||||
} catch (err) {
|
||||
expect((err as any)?.message).toBe('Arg git cannot be empty')
|
||||
}
|
||||
})
|
||||
|
||||
it('getCheckoutInfo requires ref or commit', async () => {
|
||||
try {
|
||||
await refHelper.getCheckoutInfo(git, '', '')
|
||||
throw new Error('Should not reach here')
|
||||
} catch (err) {
|
||||
expect((err as any)?.message).toBe(
|
||||
'Args ref and commit cannot both be empty'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('getCheckoutInfo sha only', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(git, '', commit)
|
||||
expect(checkoutInfo.ref).toBe(commit)
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/heads/', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
'refs/heads/my/branch',
|
||||
commit
|
||||
)
|
||||
expect(checkoutInfo.ref).toBe('my/branch')
|
||||
expect(checkoutInfo.startPoint).toBe('refs/remotes/origin/my/branch')
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/pull/', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
'refs/pull/123/merge',
|
||||
commit
|
||||
)
|
||||
expect(checkoutInfo.ref).toBe('refs/remotes/pull/123/merge')
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/tags/', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
'refs/tags/my-tag',
|
||||
commit
|
||||
)
|
||||
expect(checkoutInfo.ref).toBe('refs/tags/my-tag')
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
'refs/gh/queue/main/pr-123',
|
||||
commit
|
||||
)
|
||||
expect(checkoutInfo.ref).toBe(commit)
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/ without commit', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
'refs/non-standard-ref',
|
||||
''
|
||||
)
|
||||
expect(checkoutInfo.ref).toBe('refs/non-standard-ref')
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo unqualified branch only', async () => {
|
||||
git.branchExists = jest.fn(async (remote: boolean, pattern: string) => {
|
||||
return true
|
||||
})
|
||||
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(git, 'my/branch', '')
|
||||
|
||||
expect(checkoutInfo.ref).toBe('my/branch')
|
||||
expect(checkoutInfo.startPoint).toBe('refs/remotes/origin/my/branch')
|
||||
})
|
||||
|
||||
it('getCheckoutInfo unqualified tag only', async () => {
|
||||
git.branchExists = jest.fn(async (remote: boolean, pattern: string) => {
|
||||
return false
|
||||
})
|
||||
git.tagExists = jest.fn(async (pattern: string) => {
|
||||
return true
|
||||
})
|
||||
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(git, 'my-tag', '')
|
||||
|
||||
expect(checkoutInfo.ref).toBe('refs/tags/my-tag')
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo unqualified ref only, not a branch or tag', async () => {
|
||||
git.branchExists = jest.fn(async (remote: boolean, pattern: string) => {
|
||||
return false
|
||||
})
|
||||
git.tagExists = jest.fn(async (pattern: string) => {
|
||||
return false
|
||||
})
|
||||
|
||||
try {
|
||||
await refHelper.getCheckoutInfo(git, 'my-ref', '')
|
||||
throw new Error('Should not reach here')
|
||||
} catch (err) {
|
||||
expect((err as any)?.message).toBe(
|
||||
"A branch or tag with the name 'my-ref' could not be found"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('getRefSpec requires ref or commit', async () => {
|
||||
assert.throws(
|
||||
() => refHelper.getRefSpec('', ''),
|
||||
/Args ref and commit cannot both be empty/
|
||||
)
|
||||
})
|
||||
|
||||
it('getRefSpec sha + refs/heads/', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/heads/my/branch', commit)
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe(`+${commit}:refs/remotes/origin/my/branch`)
|
||||
})
|
||||
|
||||
it('getRefSpec sha + refs/pull/', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/pull/123/merge', commit)
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe(`+${commit}:refs/remotes/pull/123/merge`)
|
||||
})
|
||||
|
||||
it('getRefSpec sha + refs/tags/', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit)
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe(`+${commit}:refs/tags/my-tag`)
|
||||
})
|
||||
|
||||
it('getRefSpec sha only', async () => {
|
||||
const refSpec = refHelper.getRefSpec('', commit)
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe(commit)
|
||||
})
|
||||
|
||||
it('getRefSpec unqualified ref only', async () => {
|
||||
const refSpec = refHelper.getRefSpec('my-ref', '')
|
||||
expect(refSpec.length).toBe(2)
|
||||
expect(refSpec[0]).toBe('+refs/heads/my-ref*:refs/remotes/origin/my-ref*')
|
||||
expect(refSpec[1]).toBe('+refs/tags/my-ref*:refs/tags/my-ref*')
|
||||
})
|
||||
|
||||
it('getRefSpec refs/heads/ only', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '')
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe(
|
||||
'+refs/heads/my/branch:refs/remotes/origin/my/branch'
|
||||
)
|
||||
})
|
||||
|
||||
it('getRefSpec refs/pull/ only', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/pull/123/merge', '')
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe('+refs/pull/123/merge:refs/remotes/pull/123/merge')
|
||||
})
|
||||
|
||||
it('getRefSpec refs/tags/ only', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/tags/my-tag', '')
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe('+refs/tags/my-tag:refs/tags/my-tag')
|
||||
})
|
||||
})
|
87
__test__/retry-helper.test.ts
Normal file
87
__test__/retry-helper.test.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import * as core from '@actions/core'
|
||||
import {RetryHelper} from '../lib/retry-helper'
|
||||
|
||||
let info: string[]
|
||||
let retryHelper: any
|
||||
|
||||
describe('retry-helper tests', () => {
|
||||
beforeAll(() => {
|
||||
// Mock @actions/core info()
|
||||
jest.spyOn(core, 'info').mockImplementation((message: string) => {
|
||||
info.push(message)
|
||||
})
|
||||
|
||||
retryHelper = new RetryHelper(3, 0, 0)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset info
|
||||
info = []
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
// Restore
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('first attempt succeeds', async () => {
|
||||
const actual = await retryHelper.execute(async () => {
|
||||
return 'some result'
|
||||
})
|
||||
expect(actual).toBe('some result')
|
||||
expect(info).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('second attempt succeeds', async () => {
|
||||
let attempts = 0
|
||||
const actual = await retryHelper.execute(() => {
|
||||
if (++attempts == 1) {
|
||||
throw new Error('some error')
|
||||
}
|
||||
|
||||
return Promise.resolve('some result')
|
||||
})
|
||||
expect(attempts).toBe(2)
|
||||
expect(actual).toBe('some result')
|
||||
expect(info).toHaveLength(2)
|
||||
expect(info[0]).toBe('some error')
|
||||
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
|
||||
})
|
||||
|
||||
it('third attempt succeeds', async () => {
|
||||
let attempts = 0
|
||||
const actual = await retryHelper.execute(() => {
|
||||
if (++attempts < 3) {
|
||||
throw new Error(`some error ${attempts}`)
|
||||
}
|
||||
|
||||
return Promise.resolve('some result')
|
||||
})
|
||||
expect(attempts).toBe(3)
|
||||
expect(actual).toBe('some result')
|
||||
expect(info).toHaveLength(4)
|
||||
expect(info[0]).toBe('some error 1')
|
||||
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
|
||||
expect(info[2]).toBe('some error 2')
|
||||
expect(info[3]).toMatch(/Waiting .+ seconds before trying again/)
|
||||
})
|
||||
|
||||
it('all attempts fail succeeds', async () => {
|
||||
let attempts = 0
|
||||
let error: Error = null as unknown as Error
|
||||
try {
|
||||
await retryHelper.execute(() => {
|
||||
throw new Error(`some error ${++attempts}`)
|
||||
})
|
||||
} catch (err) {
|
||||
error = err as Error
|
||||
}
|
||||
expect(error.message).toBe('some error 3')
|
||||
expect(attempts).toBe(3)
|
||||
expect(info).toHaveLength(4)
|
||||
expect(info[0]).toBe('some error 1')
|
||||
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
|
||||
expect(info[2]).toBe('some error 2')
|
||||
expect(info[3]).toMatch(/Waiting .+ seconds before trying again/)
|
||||
})
|
||||
})
|
92
__test__/url-helper.test.ts
Normal file
92
__test__/url-helper.test.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import * as urlHelper from '../src/url-helper'
|
||||
|
||||
describe('getServerUrl tests', () => {
|
||||
it('basics', async () => {
|
||||
// Note that URL::toString will append a trailing / when passed just a domain name ...
|
||||
expect(urlHelper.getServerUrl().toString()).toBe('https://github.com/')
|
||||
expect(urlHelper.getServerUrl(' ').toString()).toBe('https://github.com/')
|
||||
expect(urlHelper.getServerUrl(' ').toString()).toBe('https://github.com/')
|
||||
expect(urlHelper.getServerUrl('http://contoso.com').toString()).toBe(
|
||||
'http://contoso.com/'
|
||||
)
|
||||
expect(urlHelper.getServerUrl('https://contoso.com').toString()).toBe(
|
||||
'https://contoso.com/'
|
||||
)
|
||||
expect(urlHelper.getServerUrl('https://contoso.com/').toString()).toBe(
|
||||
'https://contoso.com/'
|
||||
)
|
||||
|
||||
// ... but can't make that same assumption when passed an URL that includes some deeper path.
|
||||
expect(urlHelper.getServerUrl('https://contoso.com/a/b').toString()).toBe(
|
||||
'https://contoso.com/a/b'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isGhes tests', () => {
|
||||
const pristineEnv = process.env
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
process.env = {...pristineEnv}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
process.env = pristineEnv
|
||||
})
|
||||
|
||||
it('basics', async () => {
|
||||
delete process.env['GITHUB_SERVER_URL']
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://github.com')).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://contoso.ghe.com')).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://test.github.localhost')).toBeFalsy()
|
||||
expect(urlHelper.isGhes('https://src.onpremise.fabrikam.com')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable is not defined', async () => {
|
||||
delete process.env['GITHUB_SERVER_URL']
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable is set to github.com', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://github.com'
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://contoso.ghe.com'
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://mock-github.localhost'
|
||||
expect(urlHelper.isGhes()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns true when the GITHUB_SERVER_URL environment variable is set to some other URL', async () => {
|
||||
process.env['GITHUB_SERVER_URL'] = 'https://src.onpremise.fabrikam.com'
|
||||
expect(urlHelper.isGhes()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getServerApiUrl tests', () => {
|
||||
it('basics', async () => {
|
||||
expect(urlHelper.getServerApiUrl()).toBe('https://api.github.com')
|
||||
expect(urlHelper.getServerApiUrl('https://github.com')).toBe(
|
||||
'https://api.github.com'
|
||||
)
|
||||
expect(urlHelper.getServerApiUrl('https://GitHub.com')).toBe(
|
||||
'https://api.github.com'
|
||||
)
|
||||
expect(urlHelper.getServerApiUrl('https://contoso.ghe.com')).toBe(
|
||||
'https://api.contoso.ghe.com'
|
||||
)
|
||||
expect(urlHelper.getServerApiUrl('https://fabrikam.GHE.COM')).toBe(
|
||||
'https://api.fabrikam.ghe.com'
|
||||
)
|
||||
expect(
|
||||
urlHelper.getServerApiUrl('https://src.onpremise.fabrikam.com')
|
||||
).toBe('https://src.onpremise.fabrikam.com/api/v3')
|
||||
})
|
||||
})
|
38
__test__/verify-basic.sh
Executable file
38
__test__/verify-basic.sh
Executable file
@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ ! -f "./basic/basic-file.txt" ]; then
|
||||
echo "Expected basic file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$1" = "--archive" ]; then
|
||||
# Verify no .git folder
|
||||
if [ -d "./basic/.git" ]; then
|
||||
echo "Did not expect ./basic/.git folder to exist"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Verify .git folder
|
||||
if [ ! -d "./basic/.git" ]; then
|
||||
echo "Expected ./basic/.git folder to exist"
|
||||
exit 1
|
||||
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
|
||||
cd basic
|
||||
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
|
||||
fi
|
13
__test__/verify-clean.sh
Executable file
13
__test__/verify-clean.sh
Executable file
@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$(git -C ./basic status --porcelain)" != "" ]]; then
|
||||
echo ----------------------------------------
|
||||
echo git status
|
||||
echo ----------------------------------------
|
||||
git status
|
||||
echo ----------------------------------------
|
||||
echo git diff
|
||||
echo ----------------------------------------
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
16
__test__/verify-fetch-filter.sh
Executable file
16
__test__/verify-fetch-filter.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Verify .git folder
|
||||
if [ ! -d "./fetch-filter/.git" ]; then
|
||||
echo "Expected ./fetch-filter/.git folder to exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify .git/config contains partialclonefilter
|
||||
|
||||
CLONE_FILTER=$(git -C fetch-filter config --local --get remote.origin.partialclonefilter)
|
||||
|
||||
if [ "$CLONE_FILTER" != "blob:none" ]; then
|
||||
echo "Expected ./fetch-filter/.git/config to have 'remote.origin.partialclonefilter' set to 'blob:none'"
|
||||
exit 1
|
||||
fi
|
11
__test__/verify-lfs.sh
Executable file
11
__test__/verify-lfs.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f "./lfs/regular-file.txt" ]; then
|
||||
echo "Expected regular file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "./lfs/lfs-file.bin" ]; then
|
||||
echo "Expected lfs file does not exist"
|
||||
exit 1
|
||||
fi
|
17
__test__/verify-no-unstaged-changes.sh
Executable file
17
__test__/verify-no-unstaged-changes.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$(git status --porcelain)" != "" ]]; then
|
||||
echo ----------------------------------------
|
||||
echo git status
|
||||
echo ----------------------------------------
|
||||
git status
|
||||
echo ----------------------------------------
|
||||
echo git diff
|
||||
echo ----------------------------------------
|
||||
git diff
|
||||
echo ----------------------------------------
|
||||
echo Troubleshooting
|
||||
echo ----------------------------------------
|
||||
echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run format && npm run build"
|
||||
exit 1
|
||||
fi
|
11
__test__/verify-side-by-side.sh
Executable file
11
__test__/verify-side-by-side.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f "./side-by-side-1/side-by-side-test-file-1.txt" ]; then
|
||||
echo "Expected file 1 does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "./side-by-side-2/side-by-side-test-file-2.txt" ]; then
|
||||
echo "Expected file 2 does not exist"
|
||||
exit 1
|
||||
fi
|
51
__test__/verify-sparse-checkout-non-cone-mode.sh
Executable file
51
__test__/verify-sparse-checkout-non-cone-mode.sh
Executable file
@ -0,0 +1,51 @@
|
||||
#!/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
|
63
__test__/verify-sparse-checkout.sh
Executable file
63
__test__/verify-sparse-checkout.sh
Executable file
@ -0,0 +1,63 @@
|
||||
#!/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
|
11
__test__/verify-submodules-false.sh
Executable file
11
__test__/verify-submodules-false.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/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
|
26
__test__/verify-submodules-recursive.sh
Executable file
26
__test__/verify-submodules-recursive.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/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
|
26
__test__/verify-submodules-true.sh
Executable file
26
__test__/verify-submodules-true.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/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
|
117
action.yml
117
action.yml
@ -1,22 +1,109 @@
|
||||
name: 'Checkout'
|
||||
description: 'Get sources from a GitHub repository.'
|
||||
inputs:
|
||||
description: 'Checkout a Git repository at a particular version'
|
||||
inputs:
|
||||
repository:
|
||||
description: 'Repository name'
|
||||
description: 'Repository name with owner. For example, actions/checkout'
|
||||
default: ${{ github.repository }}
|
||||
ref:
|
||||
description: 'Ref to checkout (SHA, branch, tag)'
|
||||
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: 'Access token for clone repository'
|
||||
clean:
|
||||
description: 'If true, execute `execute git clean -ffdx && git reset --hard HEAD` before fetching'
|
||||
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
|
||||
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
|
||||
submodules:
|
||||
description: 'Directory containing files to upload'
|
||||
lfs:
|
||||
description: 'Whether to download Git-LFS files; defaults to false'
|
||||
fetch-depth:
|
||||
description: 'The depth of commits to ask Git to fetch; defaults to no limit'
|
||||
path:
|
||||
description: 'Optional path to check out source code'
|
||||
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
|
||||
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:
|
||||
description: 'Number of commits to fetch. 0 indicates all history for all branches and tags.'
|
||||
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:
|
||||
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
|
||||
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
|
||||
outputs:
|
||||
ref:
|
||||
description: 'The branch, tag or SHA that was checked out'
|
||||
commit:
|
||||
description: 'The commit SHA that was checked out'
|
||||
runs:
|
||||
plugin: 'checkout'
|
||||
using: node20
|
||||
main: dist/index.js
|
||||
post: dist/index.js
|
||||
|
274
adrs/0153-checkout-v2.md
Normal file
274
adrs/0153-checkout-v2.md
Normal file
@ -0,0 +1,274 @@
|
||||
# ADR 0153: Checkout v2
|
||||
|
||||
**日期**: 2019-10-21
|
||||
|
||||
**状态**: 已接受
|
||||
|
||||
## 背景
|
||||
|
||||
本 ADR 详细说明了 `actions/checkout@v2` 的行为。
|
||||
|
||||
新操作将用 TypeScript 编写。我们将不再使用运行器插件操作。
|
||||
|
||||
我们希望借此机会从 v1 进行行为变更。本文档的范围仅限于这些差异。
|
||||
|
||||
## 决策
|
||||
|
||||
### 输入参数
|
||||
|
||||
```yaml
|
||||
repository:
|
||||
描述: '仓库名称及所有者。例如,actions/checkout'
|
||||
默认值: ${{ github.repository }}
|
||||
ref:
|
||||
描述: >
|
||||
要检出的分支、标签或 SHA。当检出触发工作流的仓库时,默认为该事件的引用或 SHA。
|
||||
否则,使用默认分支。
|
||||
token:
|
||||
描述: >
|
||||
用于获取仓库的个人访问令牌 (PAT)。该 PAT 会配置到本地 git 配置中,使您的脚本可以运行经过身份验证的 git 命令。
|
||||
作业后步骤会移除该 PAT。
|
||||
|
||||
我们建议使用具有最低必要权限的服务账户。
|
||||
此外,在生成新的 PAT 时,请选择最低必要的范围。
|
||||
|
||||
[了解有关创建和使用加密机密的更多信息](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
|
||||
默认值: ${{ github.token }}
|
||||
ssh-key:
|
||||
描述: >
|
||||
用于获取仓库的 SSH 密钥。该 SSH 密钥会配置到本地 git 配置中,使您的脚本可以运行经过身份验证的 git 命令。
|
||||
作业后步骤会移除该 SSH 密钥。
|
||||
|
||||
我们建议使用具有最低必要权限的服务账户。
|
||||
|
||||
[了解有关创建和使用加密机密的更多信息](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
|
||||
ssh-known-hosts:
|
||||
描述: >
|
||||
除用户和全局主机密钥数据库之外的已知主机。可以使用 `ssh-keyscan` 工具获取主机的公共 SSH 密钥。例如,`ssh-keyscan github.com`。
|
||||
github.com 的公钥始终会隐式添加。
|
||||
ssh-strict:
|
||||
描述: >
|
||||
是否执行严格的主机密钥检查。当为 true 时,会在 SSH 命令行中添加 `StrictHostKeyChecking=yes` 和 `CheckHostIP=no` 选项。
|
||||
使用 `ssh-known-hosts` 输入来配置其他主机。
|
||||
默认值: true
|
||||
persist-credentials:
|
||||
描述: '是否将令牌或 SSH 密钥配置到本地 git 配置中'
|
||||
默认值: true
|
||||
path:
|
||||
描述: '在 $GITHUB_WORKSPACE 下放置仓库的相对路径'
|
||||
clean:
|
||||
描述: '是否在获取前执行 `git clean -ffdx && git reset --hard HEAD`'
|
||||
默认值: true
|
||||
fetch-depth:
|
||||
描述: '要获取的提交数量。0 表示所有分支和标签的所有历史记录。'
|
||||
默认值: 1
|
||||
lfs:
|
||||
描述: '是否下载 Git-LFS 文件'
|
||||
默认值: false
|
||||
submodules:
|
||||
描述: >
|
||||
是否检出子模块:`true` 表示检出子模块,`recursive` 表示递归检出子模块。
|
||||
|
||||
当未提供 `ssh-key` 输入时,以 `git@github.com:` 开头的 SSH URL 会被转换为 HTTPS。
|
||||
默认值: false
|
||||
```
|
||||
|
||||
注意:
|
||||
- 新增 SSH 支持
|
||||
- 新增 `persist-credentials` 参数
|
||||
- `path` 行为不同(详见下文 [path](#path))
|
||||
|
||||
### 回退到 GitHub API
|
||||
|
||||
当 PATH 中没有足够版本的 git 时,回退到 [Web API](https://developer.github.com/v3/repos/contents/#get-archive-link) 下载 tarball/zipball。
|
||||
|
||||
注意:
|
||||
- 存档中不包含 LFS 文件。因此,如果 LFS 设置为 true,则会失败。
|
||||
- 存档中也不包含子模块。
|
||||
|
||||
### 持久化凭据
|
||||
|
||||
凭据将持久化到磁盘。这将允许用户编写经过身份验证的 git 命令脚本,如 `git fetch`。
|
||||
|
||||
作业后脚本将移除凭据(自托管环境的清理)。
|
||||
|
||||
用户可以通过指定 `persist-credentials: false` 选择退出
|
||||
|
||||
注意:
|
||||
- 编写 `git commit` 脚本的用户可能需要设置用户名和电子邮件。服务不会提供任何合理的默认值。用户可以添加 `git config user.name <NAME>` 和 `git config user.email <EMAIL>`。我们将记录此指导。
|
||||
|
||||
#### PAT
|
||||
|
||||
使用 `${{github.token}}` 或 PAT 时,令牌将持久化到本地 git 配置中。配置键 `http.https://github.com/.extraheader` 允许在所有经过身份验证的命令上指定 auth header `AUTHORIZATION: basic <BASE64_U:P>`。
|
||||
|
||||
注意:
|
||||
- auth header 的范围限定在所有 github `http.https://github.com/.extraheader`
|
||||
- 额外的公共远程仓库也能正常工作。
|
||||
- 如果用户需要对额外的私有远程仓库进行身份验证,他们应提供 `token` 输入。
|
||||
|
||||
#### SSH 密钥
|
||||
|
||||
SSH 密钥将写入 `$RUNNER_TEMP` 目录下的磁盘。SSH 密钥将由操作的作业后钩子移除。此外,RUNNER_TEMP 在作业之间会被运行器清除。
|
||||
|
||||
SSH 密钥必须以严格的文件权限写入。SSH 客户端要求文件对用户可读/写,且其他用户无法访问。
|
||||
|
||||
用户主机密钥数据库 (`~/.ssh/known_hosts`) 将被复制到 `$RUNNER_TEMP` 下的唯一文件中。输入 `ssh-known-hosts` 的值将被添加到该文件中。
|
||||
|
||||
SSH 命令将覆盖本地 git 配置:
|
||||
|
||||
```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"'
|
||||
```
|
||||
|
||||
当输入 `ssh-strict` 设置为 `false` 时,选项 `CheckHostIP` 和 `StrictHostKeyChecking` 将不会被覆盖。
|
||||
|
||||
注意:
|
||||
- 当 `ssh-strict` 设置为 `true`(默认值)时,可以安全地禁用 SSH 选项 `CheckHostIP`。
|
||||
严格的主机检查会验证服务器的公钥。因此,IP 验证是不必要的,并且会产生噪音。例如:
|
||||
> 警告:已将 RSA 主机密钥永久添加到已知主机列表中,IP 地址为 '140.82.113.4'。
|
||||
- 由于 GIT_SSH_COMMAND 会覆盖 core.sshCommand,因此在获取仓库时会临时设置环境变量。当凭据被持久化时,core.sshCommand 将被利用以避免多个检出步骤相互覆盖。
|
||||
- 修改 actions/runner 以挂载 RUNNER_TEMP,从而允许从容器操作中编写经过身份验证的 git 命令脚本。
|
||||
- 有关 SSH 配置详细信息,请参阅 [这里](https://linux.die.net/man/5/ssh_config)。
|
||||
|
||||
### 获取行为
|
||||
|
||||
仅获取正在构建的 SHA 并将深度设置为 1。这显著减少了大型仓库的获取时间。
|
||||
|
||||
如果 SHA 不可用(例如多仓库),则仅获取指定的 ref,深度为 1。
|
||||
|
||||
可以使用输入 `fetch-depth` 控制深度。
|
||||
|
||||
注意:
|
||||
- Git 线路协议版本 2 支持获取单个提交。git 客户端默认使用协议版本 0。可以在 git 配置中或在 fetch 命令行调用时覆盖所需的协议版本(`-c protocol.version=2`)。我们将在 fetch 命令行上覆盖,以确保透明性。
|
||||
- Git 客户端版本 2.18+(2018 年 6 月发布)需要线路协议版本 2。
|
||||
|
||||
### 检出行为
|
||||
|
||||
对于 CI,检出将创建一个设置了上游的本地引用。这允许用户像平常一样编写 git 脚本。
|
||||
|
||||
对于 PR,继续检出分离的 HEAD。PR 分支是特殊的 - 分支和合并提交由服务器创建。它不匹配用户的本地工作流。
|
||||
|
||||
注意:
|
||||
- 如果有助于避免冲突,可以考虑在清理时删除所有本地引用。需要更多测试。
|
||||
|
||||
### 路径
|
||||
|
||||
对于主线场景,磁盘布局行为保持不变。
|
||||
|
||||
记住,对于仓库 `johndoe/foo`,主线磁盘布局如下:
|
||||
|
||||
```
|
||||
GITHUB_WORKSPACE=/home/runner/work/foo/foo
|
||||
RUNNER_WORKSPACE=/home/runner/work/foo
|
||||
```
|
||||
|
||||
V2 对检出路径引入了新的约束。位置现在必须在 `github.workspace` 下。而 checkout@v1 的约束是在上一级,位于 `runner.workspace` 下。
|
||||
|
||||
V2 不再更改 `github.workspace` 以跟随自仓库的检出位置。
|
||||
|
||||
这些行为变化更好地与容器操作对齐。[记录的文件系统契约](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/virtual-environments-for-github-hosted-runners#docker-container-filesystem) 是:
|
||||
|
||||
- `/github/home`
|
||||
- `/github/workspace` - 注意:GitHub Actions 必须由默认的 Docker 用户(root)运行。确保您的 Dockerfile 没有设置 USER 指令,否则您将无法访问 `GITHUB_WORKSPACE`。
|
||||
- `/github/workflow`
|
||||
|
||||
注意:
|
||||
- 跟踪配置不会更新以反映工作流仓库的路径。
|
||||
- 当检出路径更改时,任何现有的工作流仓库都不会被移动。实际上,一些客户希望并行检出工作流仓库到不同分支的旁边。
|
||||
- 需要仅针对自仓库的根目录操作的 Actions 应公开一个 `path` 输入。
|
||||
|
||||
#### `path` 输入的默认值
|
||||
|
||||
`path` 输入将默认为 `./`,相对于 `github.workspace`。
|
||||
|
||||
此默认值非常适合主线场景:单一检出
|
||||
|
||||
对于多检出,用户必须至少为一个仓库指定 `path` 输入。
|
||||
|
||||
注意:
|
||||
- 另一种选择是自仓库默认为 `./`,其他仓库默认为 `<REPO_NAME>`。然而,嵌套布局是一种非典型的 git 布局,因此不是好的默认值。用户应提供路径信息。
|
||||
|
||||
#### 示例 - 嵌套布局
|
||||
|
||||
以下示例检出两个仓库并创建嵌套布局。
|
||||
|
||||
```yaml
|
||||
自仓库 - 检出到 $GITHUB_WORKSPACE
|
||||
• uses: checkout@v2
|
||||
|
||||
|
||||
其他仓库 - 检出到 $GITHUB_WORKSPACE/myscripts
|
||||
• uses: checkout@v2
|
||||
|
||||
with:
|
||||
repository: myorg/myscripts
|
||||
path: myscripts
|
||||
```
|
||||
|
||||
#### 示例 - 并排布局
|
||||
|
||||
以下示例检出两个仓库并创建并排布局。
|
||||
|
||||
```yaml
|
||||
自仓库 - 检出到 $GITHUB_WORKSPACE/foo
|
||||
• uses: checkout@v2
|
||||
|
||||
with:
|
||||
path: foo
|
||||
|
||||
其他仓库 - 检出到 $GITHUB_WORKSPACE/myscripts
|
||||
• uses: checkout@v2
|
||||
|
||||
with:
|
||||
repository: myorg/myscripts
|
||||
path: myscripts
|
||||
```
|
||||
|
||||
#### 路径对问题匹配器的影响
|
||||
|
||||
问题匹配器将源文件与注释关联。
|
||||
|
||||
目前,运行器验证源文件是否在 `github.workspace` 下。否则,源文件属性将被丢弃。
|
||||
|
||||
多检出使问题复杂化。然而,即使是现在,子模块也可能导致此启发式不准确。
|
||||
|
||||
更好的解决方案是:
|
||||
|
||||
给定源文件路径,向上遍历目录,直到找到第一个 `.git/config`。检查它是否与自仓库匹配(`url = https://github.com/OWNER/REPO`)。如果不匹配,则丢弃源文件路径。
|
||||
|
||||
### 子模块
|
||||
|
||||
有了 PAT 和 SSH 密钥支持,我们应该能够为子模块场景提供无缝支持:递归、非递归、相对子模块路径。
|
||||
|
||||
获取子模块时,遵循 `fetch-depth` 设置。
|
||||
|
||||
此外,在获取子模块时,如果未提供 `ssh-key` 输入,则将 SSH URL 转换为 HTTPS:`-c url."https://github.com/".insteadOf "git@github.com:"`
|
||||
|
||||
凭据也将持久化到子模块的本地 git 配置中。
|
||||
|
||||
### 转换为 TypeScript
|
||||
|
||||
出于以下原因,检出操作应该是 GitHub graph 上的 TypeScript 操作:
|
||||
- 使客户能够 fork 检出仓库并进行修改
|
||||
- 作为客户的示例
|
||||
- 澄清检出操作清单
|
||||
- 简化运行器
|
||||
- 减少需要移植的运行器代码量(如果我们曾经这样做)
|
||||
|
||||
注意:
|
||||
- 这意味着作业容器镜像需要在 PATH 中有 git,以便进行检出。
|
||||
|
||||
### 分支策略和发布标签
|
||||
|
||||
- 为 V1 创建一个维护分支:`releases/v1`
|
||||
- 将更改合并到默认分支
|
||||
- 使用新标签 `preview` 发布
|
||||
- 稳定后,使用新标签 `v2` 发布
|
||||
|
||||
## 后果
|
||||
|
||||
- 更新检出操作和自述文件
|
||||
- 更新示例以使用 `actions/checkout@v2`
|
||||
- 作业容器现在需要 PATH 中有 git 才能进行检出,否则回退到 REST API
|
||||
- 最低 git 版本 2.18
|
||||
- 更新问题匹配器逻辑,关于源文件验证(运行器)
|
38218
dist/index.js
vendored
Normal file
38218
dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
dist/problem-matcher.json
vendored
Normal file
13
dist/problem-matcher.json
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "checkout-git",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(fatal|error): (.*)$",
|
||||
"message": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
12
images/test-ubuntu-git.Dockerfile
Normal file
12
images/test-ubuntu-git.Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
# Defines the test-ubuntu-git Container Image.
|
||||
# Consumed by actions/checkout CI/CD validation workflows.
|
||||
|
||||
FROM ubuntu:latest
|
||||
|
||||
RUN apt update
|
||||
RUN apt install -y git
|
||||
|
||||
LABEL org.opencontainers.image.title="Ubuntu + git (validation image)"
|
||||
LABEL org.opencontainers.image.description="Ubuntu image with git pre-installed. Intended primarily for testing `actions/checkout` during CI/CD workflows."
|
||||
LABEL org.opencontainers.image.documentation="https://github.com/actions/checkout/tree/main/images/test-ubuntu-git.md"
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
15
images/test-ubuntu-git.md
Normal file
15
images/test-ubuntu-git.md
Normal file
@ -0,0 +1,15 @@
|
||||
# `test-ubuntu-git` Container Image
|
||||
|
||||
[](https://github.com/actions/checkout/actions/workflows/update-test-ubuntu-git.yml)
|
||||
|
||||
## Purpose
|
||||
|
||||
`test-ubuntu-git` is a container image hosted on the GitHub Container Registry, `ghcr.io`.
|
||||
|
||||
It is intended primarily for testing the [`actions/checkout` repository](https://github.com/actions/checkout) as part of `actions/checkout`'s CI/CD workflows.
|
||||
|
||||
The composition of `test-ubuntu-git` is intentionally minimal. It is comprised of [git](https://git-scm.com/) installed on top of a [base-level ubuntu image](https://hub.docker.com/_/ubuntu/tags).
|
||||
|
||||
# License
|
||||
|
||||
`test-ubuntu-git` is released under the [MIT License](/LICENSE).
|
12
jest.config.js
Normal file
12
jest.config.js
Normal file
@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
fakeTimers: {},
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
testRunner: 'jest-circus/runner',
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
7212
package-lock.json
generated
Normal file
7212
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
package.json
Normal file
55
package.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "checkout",
|
||||
"version": "4.2.2",
|
||||
"description": "checkout action",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc && ncc build && node lib/misc/generate-docs.js",
|
||||
"format": "prettier --write '**/*.ts'",
|
||||
"format-check": "prettier --check '**/*.ts'",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"test": "jest",
|
||||
"licensed-check": "src/misc/licensed-check.sh",
|
||||
"licensed-generate": "src/misc/licensed-generate.sh"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/actions/checkout.git"
|
||||
},
|
||||
"keywords": [
|
||||
"github",
|
||||
"actions",
|
||||
"checkout"
|
||||
],
|
||||
"author": "GitHub",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/actions/checkout/issues"
|
||||
},
|
||||
"homepage": "https://github.com/actions/checkout#readme",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
"@actions/io": "^1.1.3",
|
||||
"@actions/tool-cache": "^2.0.1",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
||||
"@typescript-eslint/parser": "^7.9.0",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-github": "^4.10.2",
|
||||
"eslint-plugin-jest": "^28.8.2",
|
||||
"jest": "^29.7.0",
|
||||
"jest-circus": "^29.7.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "^3.3.3",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
83
src/fs-helper.ts
Normal file
83
src/fs-helper.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import * as fs from 'fs'
|
||||
|
||||
export function directoryExistsSync(path: string, required?: boolean): boolean {
|
||||
if (!path) {
|
||||
throw new Error("Arg 'path' must not be empty")
|
||||
}
|
||||
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = fs.statSync(path)
|
||||
} catch (error) {
|
||||
if ((error as any)?.code === 'ENOENT') {
|
||||
if (!required) {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(`Directory '${path}' does not exist`)
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Encountered an error when checking whether path '${path}' exists: ${
|
||||
(error as any)?.message ?? error
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
return true
|
||||
} else if (!required) {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(`Directory '${path}' does not exist`)
|
||||
}
|
||||
|
||||
export function existsSync(path: string): boolean {
|
||||
if (!path) {
|
||||
throw new Error("Arg 'path' must not be empty")
|
||||
}
|
||||
|
||||
try {
|
||||
fs.statSync(path)
|
||||
} catch (error) {
|
||||
if ((error as any)?.code === 'ENOENT') {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Encountered an error when checking whether path '${path}' exists: ${
|
||||
(error as any)?.message ?? error
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function fileExistsSync(path: string): boolean {
|
||||
if (!path) {
|
||||
throw new Error("Arg 'path' must not be empty")
|
||||
}
|
||||
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = fs.statSync(path)
|
||||
} catch (error) {
|
||||
if ((error as any)?.code === 'ENOENT') {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Encountered an error when checking whether path '${path}' exists: ${
|
||||
(error as any)?.message ?? error
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
374
src/git-auth-helper.ts
Normal file
374
src/git-auth-helper.ts
Normal file
@ -0,0 +1,374 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
635
src/git-command-manager.ts
Normal file
635
src/git-command-manager.ts
Normal file
@ -0,0 +1,635 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as exec from '@actions/exec'
|
||||
import * as fs from 'fs'
|
||||
import * as fshelper from './fs-helper'
|
||||
import * as io from '@actions/io'
|
||||
import * as path from 'path'
|
||||
import * as refHelper from './ref-helper'
|
||||
import * as regexpHelper from './regexp-helper'
|
||||
import * as retryHelper from './retry-helper'
|
||||
import {GitVersion} from './git-version'
|
||||
|
||||
// Auth header not supported before 2.9
|
||||
// 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 MinimumGitSparseCheckoutVersion = new GitVersion('2.28')
|
||||
|
||||
export interface IGitCommandManager {
|
||||
branchDelete(remote: boolean, branch: string): Promise<void>
|
||||
branchExists(remote: boolean, pattern: string): Promise<boolean>
|
||||
branchList(remote: boolean): Promise<string[]>
|
||||
disableSparseCheckout(): Promise<void>
|
||||
sparseCheckout(sparseCheckout: string[]): Promise<void>
|
||||
sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void>
|
||||
checkout(ref: string, startPoint: string): Promise<void>
|
||||
checkoutDetach(): Promise<void>
|
||||
config(
|
||||
configKey: string,
|
||||
configValue: string,
|
||||
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
|
||||
init(): Promise<void>
|
||||
isDetached(): Promise<boolean>
|
||||
lfsFetch(ref: string): Promise<void>
|
||||
lfsInstall(): Promise<void>
|
||||
log1(format?: string): Promise<string>
|
||||
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>
|
||||
tryClean(): Promise<boolean>
|
||||
tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
|
||||
tryDisableAutomaticGarbageCollection(): Promise<boolean>
|
||||
tryGetFetchUrl(): Promise<string>
|
||||
tryReset(): Promise<boolean>
|
||||
version(): Promise<GitVersion>
|
||||
}
|
||||
|
||||
export async function createCommandManager(
|
||||
workingDirectory: string,
|
||||
lfs: boolean,
|
||||
doSparseCheckout: boolean
|
||||
): Promise<IGitCommandManager> {
|
||||
return await GitCommandManager.createCommandManager(
|
||||
workingDirectory,
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
}
|
||||
|
||||
class GitCommandManager {
|
||||
private gitEnv = {
|
||||
GIT_TERMINAL_PROMPT: '0', // Disable git prompt
|
||||
GCM_INTERACTIVE: 'Never' // Disable prompting for git credential manager
|
||||
}
|
||||
private gitPath = ''
|
||||
private lfs = false
|
||||
private doSparseCheckout = false
|
||||
private workingDirectory = ''
|
||||
private gitVersion: GitVersion = new GitVersion()
|
||||
|
||||
// Private constructor; use createCommandManager()
|
||||
private constructor() {}
|
||||
|
||||
async branchDelete(remote: boolean, branch: string): Promise<void> {
|
||||
const args = ['branch', '--delete', '--force']
|
||||
if (remote) {
|
||||
args.push('--remote')
|
||||
}
|
||||
args.push(branch)
|
||||
|
||||
await this.execGit(args)
|
||||
}
|
||||
|
||||
async branchExists(remote: boolean, pattern: string): Promise<boolean> {
|
||||
const args = ['branch', '--list']
|
||||
if (remote) {
|
||||
args.push('--remote')
|
||||
}
|
||||
args.push(pattern)
|
||||
|
||||
const output = await this.execGit(args)
|
||||
return !!output.stdout.trim()
|
||||
}
|
||||
|
||||
async branchList(remote: boolean): Promise<string[]> {
|
||||
const result: string[] = []
|
||||
|
||||
// Note, this implementation uses "rev-parse --symbolic-full-name" because the output from
|
||||
// "branch --list" is more difficult when in a detached HEAD state.
|
||||
|
||||
// TODO(https://github.com/actions/checkout/issues/786): this implementation uses
|
||||
// "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) {
|
||||
args.push('--remotes=origin')
|
||||
} else {
|
||||
args.push('--branches')
|
||||
}
|
||||
|
||||
const stderr: string[] = []
|
||||
const errline: string[] = []
|
||||
const stdout: string[] = []
|
||||
const stdline: string[] = []
|
||||
|
||||
const listeners = {
|
||||
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()
|
||||
if (!branch) {
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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> {
|
||||
const args = ['checkout', '--progress', '--force']
|
||||
if (startPoint) {
|
||||
args.push('-B', ref, startPoint)
|
||||
} else {
|
||||
args.push(ref)
|
||||
}
|
||||
|
||||
await this.execGit(args)
|
||||
}
|
||||
|
||||
async checkoutDetach(): Promise<void> {
|
||||
const args = ['checkout', '--detach']
|
||||
await this.execGit(args)
|
||||
}
|
||||
|
||||
async config(
|
||||
configKey: string,
|
||||
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(
|
||||
configKey: string,
|
||||
globalConfig?: boolean
|
||||
): Promise<boolean> {
|
||||
const pattern = regexpHelper.escape(configKey)
|
||||
const output = await this.execGit(
|
||||
[
|
||||
'config',
|
||||
globalConfig ? '--global' : '--local',
|
||||
'--name-only',
|
||||
'--get-regexp',
|
||||
pattern
|
||||
],
|
||||
true
|
||||
)
|
||||
return output.exitCode === 0
|
||||
}
|
||||
|
||||
async fetch(
|
||||
refSpec: string[],
|
||||
options: {
|
||||
filter?: string
|
||||
fetchDepth?: number
|
||||
fetchTags?: boolean
|
||||
showProgress?: boolean
|
||||
}
|
||||
): Promise<void> {
|
||||
const args = ['-c', 'protocol.version=2', 'fetch']
|
||||
if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
|
||||
args.push('--no-tags')
|
||||
}
|
||||
|
||||
args.push('--prune', '--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 (
|
||||
fshelper.fileExistsSync(
|
||||
path.join(this.workingDirectory, '.git', 'shallow')
|
||||
)
|
||||
) {
|
||||
args.push('--unshallow')
|
||||
}
|
||||
|
||||
args.push('origin')
|
||||
for (const arg of refSpec) {
|
||||
args.push(arg)
|
||||
}
|
||||
|
||||
const that = this
|
||||
await retryHelper.execute(async () => {
|
||||
await that.execGit(args)
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
return this.workingDirectory
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.execGit(['init', this.workingDirectory])
|
||||
}
|
||||
|
||||
async isDetached(): Promise<boolean> {
|
||||
// Note, "branch --show-current" would be simpler but isn't available until Git 2.22
|
||||
const output = await this.execGit(
|
||||
['rev-parse', '--symbolic-full-name', '--verify', '--quiet', 'HEAD'],
|
||||
true
|
||||
)
|
||||
return !output.stdout.trim().startsWith('refs/heads/')
|
||||
}
|
||||
|
||||
async lfsFetch(ref: string): Promise<void> {
|
||||
const args = ['lfs', 'fetch', 'origin', ref]
|
||||
|
||||
const that = this
|
||||
await retryHelper.execute(async () => {
|
||||
await that.execGit(args)
|
||||
})
|
||||
}
|
||||
|
||||
async lfsInstall(): Promise<void> {
|
||||
await this.execGit(['lfs', 'install', '--local'])
|
||||
}
|
||||
|
||||
async log1(format?: string): Promise<string> {
|
||||
const args = format ? ['log', '-1', format] : ['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> {
|
||||
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> {
|
||||
const output = await this.execGit(['tag', '--list', pattern])
|
||||
return !!output.stdout.trim()
|
||||
}
|
||||
|
||||
async tryClean(): Promise<boolean> {
|
||||
const output = await this.execGit(['clean', '-ffdx'], true)
|
||||
return output.exitCode === 0
|
||||
}
|
||||
|
||||
async tryConfigUnset(
|
||||
configKey: string,
|
||||
globalConfig?: boolean
|
||||
): Promise<boolean> {
|
||||
const output = await this.execGit(
|
||||
[
|
||||
'config',
|
||||
globalConfig ? '--global' : '--local',
|
||||
'--unset-all',
|
||||
configKey
|
||||
],
|
||||
true
|
||||
)
|
||||
return output.exitCode === 0
|
||||
}
|
||||
|
||||
async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
|
||||
const output = await this.execGit(
|
||||
['config', '--local', 'gc.auto', '0'],
|
||||
true
|
||||
)
|
||||
return output.exitCode === 0
|
||||
}
|
||||
|
||||
async tryGetFetchUrl(): Promise<string> {
|
||||
const output = await this.execGit(
|
||||
['config', '--local', '--get', 'remote.origin.url'],
|
||||
true
|
||||
)
|
||||
|
||||
if (output.exitCode !== 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const stdout = output.stdout.trim()
|
||||
if (stdout.includes('\n')) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return stdout
|
||||
}
|
||||
|
||||
async tryReset(): Promise<boolean> {
|
||||
const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
|
||||
return output.exitCode === 0
|
||||
}
|
||||
|
||||
async version(): Promise<GitVersion> {
|
||||
return this.gitVersion
|
||||
}
|
||||
|
||||
static async createCommandManager(
|
||||
workingDirectory: string,
|
||||
lfs: boolean,
|
||||
doSparseCheckout: boolean
|
||||
): Promise<GitCommandManager> {
|
||||
const result = new GitCommandManager()
|
||||
await result.initializeCommandManager(
|
||||
workingDirectory,
|
||||
lfs,
|
||||
doSparseCheckout
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
private async execGit(
|
||||
args: string[],
|
||||
allowAllExitCodes = false,
|
||||
silent = false,
|
||||
customListeners = {}
|
||||
): Promise<GitOutput> {
|
||||
fshelper.directoryExistsSync(this.workingDirectory, true)
|
||||
|
||||
const result = new GitOutput()
|
||||
|
||||
const env = {}
|
||||
for (const key of Object.keys(process.env)) {
|
||||
env[key] = process.env[key]
|
||||
}
|
||||
for (const key of Object.keys(this.gitEnv)) {
|
||||
env[key] = this.gitEnv[key]
|
||||
}
|
||||
|
||||
const defaultListener = {
|
||||
stdout: (data: Buffer) => {
|
||||
stdout.push(data.toString())
|
||||
}
|
||||
}
|
||||
|
||||
const mergedListeners = {...defaultListener, ...customListeners}
|
||||
|
||||
const stdout: string[] = []
|
||||
const options = {
|
||||
cwd: this.workingDirectory,
|
||||
env,
|
||||
silent,
|
||||
ignoreReturnCode: allowAllExitCodes,
|
||||
listeners: mergedListeners
|
||||
}
|
||||
|
||||
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
|
||||
result.stdout = stdout.join('')
|
||||
|
||||
core.debug(result.exitCode.toString())
|
||||
core.debug(result.stdout)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private async initializeCommandManager(
|
||||
workingDirectory: string,
|
||||
lfs: boolean,
|
||||
doSparseCheckout: boolean
|
||||
): Promise<void> {
|
||||
this.workingDirectory = workingDirectory
|
||||
|
||||
// Git-lfs will try to pull down assets if any of the local/user/system setting exist.
|
||||
// If the user didn't enable `LFS` in their pipeline definition, disable LFS fetch/checkout.
|
||||
this.lfs = lfs
|
||||
if (!this.lfs) {
|
||||
this.gitEnv['GIT_LFS_SKIP_SMUDGE'] = '1'
|
||||
}
|
||||
|
||||
this.gitPath = await io.which('git', true)
|
||||
|
||||
// Git version
|
||||
core.debug('Getting git version')
|
||||
this.gitVersion = new GitVersion()
|
||||
let gitOutput = await this.execGit(['version'])
|
||||
let stdout = gitOutput.stdout.trim()
|
||||
if (!stdout.includes('\n')) {
|
||||
const match = stdout.match(/\d+\.\d+(\.\d+)?/)
|
||||
if (match) {
|
||||
this.gitVersion = new GitVersion(match[0])
|
||||
}
|
||||
}
|
||||
if (!this.gitVersion.isValid()) {
|
||||
throw new Error('Unable to determine git version')
|
||||
}
|
||||
|
||||
// Minimum git version
|
||||
if (!this.gitVersion.checkMinimum(MinimumGitVersion)) {
|
||||
throw new Error(
|
||||
`Minimum required git version is ${MinimumGitVersion}. Your git ('${this.gitPath}') is ${this.gitVersion}`
|
||||
)
|
||||
}
|
||||
|
||||
if (this.lfs) {
|
||||
// Git-lfs version
|
||||
core.debug('Getting git-lfs version')
|
||||
let gitLfsVersion = new GitVersion()
|
||||
const gitLfsPath = await io.which('git-lfs', true)
|
||||
gitOutput = await this.execGit(['lfs', 'version'])
|
||||
stdout = gitOutput.stdout.trim()
|
||||
if (!stdout.includes('\n')) {
|
||||
const match = stdout.match(/\d+\.\d+(\.\d+)?/)
|
||||
if (match) {
|
||||
gitLfsVersion = new GitVersion(match[0])
|
||||
}
|
||||
}
|
||||
if (!gitLfsVersion.isValid()) {
|
||||
throw new Error('Unable to determine git-lfs version')
|
||||
}
|
||||
|
||||
// Minimum git-lfs version
|
||||
// Note:
|
||||
// - Auth header not supported before 2.1
|
||||
const minimumGitLfsVersion = new GitVersion('2.1')
|
||||
if (!gitLfsVersion.checkMinimum(minimumGitLfsVersion)) {
|
||||
throw new Error(
|
||||
`Minimum required git-lfs version is ${minimumGitLfsVersion}. Your git-lfs ('${gitLfsPath}') is ${gitLfsVersion}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
const gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
|
||||
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
|
||||
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
|
||||
}
|
||||
}
|
||||
|
||||
class GitOutput {
|
||||
stdout = ''
|
||||
exitCode = 0
|
||||
}
|
125
src/git-directory-helper.ts
Normal file
125
src/git-directory-helper.ts
Normal file
@ -0,0 +1,125 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
353
src/git-source-provider.ts
Normal file
353
src/git-source-provider.ts
Normal file
@ -0,0 +1,353 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fsHelper from './fs-helper'
|
||||
import * as gitAuthHelper from './git-auth-helper'
|
||||
import * as gitCommandManager from './git-command-manager'
|
||||
import * as gitDirectoryHelper from './git-directory-helper'
|
||||
import * as githubApiHelper from './github-api-helper'
|
||||
import * as io from '@actions/io'
|
||||
import * as path from 'path'
|
||||
import * as refHelper from './ref-helper'
|
||||
import * as stateHelper from './state-helper'
|
||||
import * as urlHelper from './url-helper'
|
||||
import {
|
||||
MinimumGitSparseCheckoutVersion,
|
||||
IGitCommandManager
|
||||
} from './git-command-manager'
|
||||
import {IGitSourceSettings} from './git-source-settings'
|
||||
|
||||
export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
// Repository URL
|
||||
core.info(
|
||||
`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
|
||||
)
|
||||
const repositoryUrl = urlHelper.getFetchUrl(settings)
|
||||
|
||||
// Remove conflicting file path
|
||||
if (fsHelper.fileExistsSync(settings.repositoryPath)) {
|
||||
await io.rmRF(settings.repositoryPath)
|
||||
}
|
||||
|
||||
// Create directory
|
||||
let isExisting = true
|
||||
if (!fsHelper.directoryExistsSync(settings.repositoryPath)) {
|
||||
isExisting = false
|
||||
await io.mkdirP(settings.repositoryPath)
|
||||
}
|
||||
|
||||
// Git command manager
|
||||
core.startGroup('Getting Git version info')
|
||||
const git = await getGitCommandManager(settings)
|
||||
core.endGroup()
|
||||
|
||||
let authHelper: gitAuthHelper.IGitAuthHelper | null = null
|
||||
try {
|
||||
if (git) {
|
||||
authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
if (settings.setSafeDirectory) {
|
||||
// Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail
|
||||
// Otherwise all git commands we run in a container fail
|
||||
await authHelper.configureTempGlobalConfig()
|
||||
core.info(
|
||||
`Adding repository directory to the temporary git global config as a safe directory`
|
||||
)
|
||||
|
||||
await git
|
||||
.config('safe.directory', settings.repositoryPath, true, true)
|
||||
.catch(error => {
|
||||
core.info(
|
||||
`Failed to initialize safe directory with error: ${error}`
|
||||
)
|
||||
})
|
||||
|
||||
stateHelper.setSafeDirectory()
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare existing directory, otherwise recreate
|
||||
if (isExisting) {
|
||||
await gitDirectoryHelper.prepareExistingDirectory(
|
||||
git,
|
||||
settings.repositoryPath,
|
||||
repositoryUrl,
|
||||
settings.clean,
|
||||
settings.ref
|
||||
)
|
||||
}
|
||||
|
||||
if (!git) {
|
||||
// Downloading using REST API
|
||||
core.info(`The repository will be downloaded using the GitHub REST API`)
|
||||
core.info(
|
||||
`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
|
||||
)
|
||||
if (settings.submodules) {
|
||||
throw new Error(
|
||||
`Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
|
||||
)
|
||||
} else if (settings.sshKey) {
|
||||
throw new Error(
|
||||
`Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
|
||||
)
|
||||
}
|
||||
|
||||
await githubApiHelper.downloadRepository(
|
||||
settings.authToken,
|
||||
settings.repositoryOwner,
|
||||
settings.repositoryName,
|
||||
settings.ref,
|
||||
settings.commit,
|
||||
settings.repositoryPath,
|
||||
settings.githubServerUrl
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Save state for POST action
|
||||
stateHelper.setRepositoryPath(settings.repositoryPath)
|
||||
|
||||
// Initialize the repository
|
||||
if (
|
||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||
) {
|
||||
core.startGroup('Initializing the repository')
|
||||
await git.init()
|
||||
await git.remoteAdd('origin', repositoryUrl)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
// Disable automatic garbage collection
|
||||
core.startGroup('Disabling automatic garbage collection')
|
||||
if (!(await git.tryDisableAutomaticGarbageCollection())) {
|
||||
core.warning(
|
||||
`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
|
||||
)
|
||||
}
|
||||
core.endGroup()
|
||||
|
||||
// If we didn't initialize it above, do it now
|
||||
if (!authHelper) {
|
||||
authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
}
|
||||
// Configure auth
|
||||
core.startGroup('Setting up auth')
|
||||
await authHelper.configureAuth()
|
||||
core.endGroup()
|
||||
|
||||
// Determine the default branch
|
||||
if (!settings.ref && !settings.commit) {
|
||||
core.startGroup('Determining the default branch')
|
||||
if (settings.sshKey) {
|
||||
settings.ref = await git.getDefaultBranch(repositoryUrl)
|
||||
} else {
|
||||
settings.ref = await githubApiHelper.getDefaultBranch(
|
||||
settings.authToken,
|
||||
settings.repositoryOwner,
|
||||
settings.repositoryName,
|
||||
settings.githubServerUrl
|
||||
)
|
||||
}
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
// LFS install
|
||||
if (settings.lfs) {
|
||||
await git.lfsInstall()
|
||||
}
|
||||
|
||||
// Fetch
|
||||
core.startGroup('Fetching the repository')
|
||||
const fetchOptions: {
|
||||
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
|
||||
core.startGroup('Determining the checkout info')
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
settings.ref,
|
||||
settings.commit
|
||||
)
|
||||
core.endGroup()
|
||||
|
||||
// LFS fetch
|
||||
// Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
|
||||
// Explicit lfs fetch will fetch lfs objects in parallel.
|
||||
// For sparse checkouts, let `checkout` fetch the needed objects lazily.
|
||||
if (settings.lfs && !settings.sparseCheckout) {
|
||||
core.startGroup('Fetching LFS objects')
|
||||
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
|
||||
core.startGroup('Checking out the ref')
|
||||
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
|
||||
core.endGroup()
|
||||
|
||||
// Submodules
|
||||
if (settings.submodules) {
|
||||
// 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
|
||||
const commitSHA = await git.log1('--format=%H')
|
||||
core.setOutput('commit', commitSHA.trim())
|
||||
|
||||
// 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> {
|
||||
// Repo exists?
|
||||
if (
|
||||
!repositoryPath ||
|
||||
!fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
let git: IGitCommandManager
|
||||
try {
|
||||
git = await gitCommandManager.createCommandManager(
|
||||
repositoryPath,
|
||||
false,
|
||||
false
|
||||
)
|
||||
} 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(
|
||||
settings: IGitSourceSettings
|
||||
): Promise<IGitCommandManager | undefined> {
|
||||
core.info(`Working directory is '${settings.repositoryPath}'`)
|
||||
try {
|
||||
return await gitCommandManager.createCommandManager(
|
||||
settings.repositoryPath,
|
||||
settings.lfs,
|
||||
settings.sparseCheckout != null
|
||||
)
|
||||
} catch (err) {
|
||||
// Git is required for LFS
|
||||
if (settings.lfs) {
|
||||
throw err
|
||||
}
|
||||
|
||||
// Otherwise fallback to REST API
|
||||
return undefined
|
||||
}
|
||||
}
|
121
src/git-source-settings.ts
Normal file
121
src/git-source-settings.ts
Normal file
@ -0,0 +1,121 @@
|
||||
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
|
||||
}
|
77
src/git-version.ts
Normal file
77
src/git-version.ts
Normal file
@ -0,0 +1,77 @@
|
||||
export class GitVersion {
|
||||
private readonly major: number = NaN
|
||||
private readonly minor: number = NaN
|
||||
private readonly patch: number = NaN
|
||||
|
||||
/**
|
||||
* Used for comparing the version of git and git-lfs against the minimum required version
|
||||
* @param version the version string, e.g. 1.2 or 1.2.3
|
||||
*/
|
||||
constructor(version?: string) {
|
||||
if (version) {
|
||||
const match = version.match(/^(\d+)\.(\d+)(\.(\d+))?$/)
|
||||
if (match) {
|
||||
this.major = Number(match[1])
|
||||
this.minor = Number(match[2])
|
||||
if (match[4]) {
|
||||
this.patch = Number(match[4])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the instance against a minimum required version
|
||||
* @param minimum Minimum version
|
||||
*/
|
||||
checkMinimum(minimum: GitVersion): boolean {
|
||||
if (!minimum.isValid()) {
|
||||
throw new Error('Arg minimum is not a valid version')
|
||||
}
|
||||
|
||||
// Major is insufficient
|
||||
if (this.major < minimum.major) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Major is equal
|
||||
if (this.major === minimum.major) {
|
||||
// Minor is insufficient
|
||||
if (this.minor < minimum.minor) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Minor is equal
|
||||
if (this.minor === minimum.minor) {
|
||||
// Patch is insufficient
|
||||
if (this.patch && this.patch < (minimum.patch || 0)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the instance was constructed from a valid version string
|
||||
*/
|
||||
isValid(): boolean {
|
||||
return !isNaN(this.major)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version as a string, e.g. 1.2 or 1.2.3
|
||||
*/
|
||||
toString(): string {
|
||||
let result = ''
|
||||
if (this.isValid()) {
|
||||
result = `${this.major}.${this.minor}`
|
||||
if (!isNaN(this.patch)) {
|
||||
result += `.${this.patch}`
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
145
src/github-api-helper.ts
Normal file
145
src/github-api-helper.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import * as assert from 'assert'
|
||||
import * as core from '@actions/core'
|
||||
import * as fs from 'fs'
|
||||
import * as github from '@actions/github'
|
||||
import * as io from '@actions/io'
|
||||
import * as path from 'path'
|
||||
import * as retryHelper from './retry-helper'
|
||||
import * as toolCache from '@actions/tool-cache'
|
||||
import {v4 as uuid} from 'uuid'
|
||||
import {getServerApiUrl} from './url-helper'
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
|
||||
export async function downloadRepository(
|
||||
authToken: string,
|
||||
owner: string,
|
||||
repo: string,
|
||||
ref: string,
|
||||
commit: string,
|
||||
repositoryPath: string,
|
||||
baseUrl?: string
|
||||
): 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
|
||||
let archiveData = await retryHelper.execute(async () => {
|
||||
core.info('Downloading the archive')
|
||||
return await downloadArchive(authToken, owner, repo, ref, commit, baseUrl)
|
||||
})
|
||||
|
||||
// Write archive to disk
|
||||
core.info('Writing archive to disk')
|
||||
const uniqueId = uuid()
|
||||
const archivePath = IS_WINDOWS
|
||||
? path.join(repositoryPath, `${uniqueId}.zip`)
|
||||
: path.join(repositoryPath, `${uniqueId}.tar.gz`)
|
||||
await fs.promises.writeFile(archivePath, archiveData)
|
||||
archiveData = Buffer.from('') // Free memory
|
||||
|
||||
// Extract archive
|
||||
core.info('Extracting the archive')
|
||||
const extractPath = path.join(repositoryPath, uniqueId)
|
||||
await io.mkdirP(extractPath)
|
||||
if (IS_WINDOWS) {
|
||||
await toolCache.extractZip(archivePath, extractPath)
|
||||
} else {
|
||||
await toolCache.extractTar(archivePath, extractPath)
|
||||
}
|
||||
await io.rmRF(archivePath)
|
||||
|
||||
// Determine the path of the repository content. The archive contains
|
||||
// a top-level folder and the repository content is inside.
|
||||
const archiveFileNames = await fs.promises.readdir(extractPath)
|
||||
assert.ok(
|
||||
archiveFileNames.length == 1,
|
||||
'Expected exactly one directory inside archive'
|
||||
)
|
||||
const archiveVersion = archiveFileNames[0] // The top-level folder name includes the short SHA
|
||||
core.info(`Resolved version ${archiveVersion}`)
|
||||
const tempRepositoryPath = path.join(extractPath, archiveVersion)
|
||||
|
||||
// Move the files
|
||||
for (const fileName of await fs.promises.readdir(tempRepositoryPath)) {
|
||||
const sourcePath = path.join(tempRepositoryPath, fileName)
|
||||
const targetPath = path.join(repositoryPath, fileName)
|
||||
if (IS_WINDOWS) {
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
authToken: string,
|
||||
owner: string,
|
||||
repo: string,
|
||||
ref: string,
|
||||
commit: string,
|
||||
baseUrl?: string
|
||||
): Promise<Buffer> {
|
||||
const octokit = github.getOctokit(authToken, {
|
||||
baseUrl: getServerApiUrl(baseUrl)
|
||||
})
|
||||
const download = IS_WINDOWS
|
||||
? octokit.rest.repos.downloadZipballArchive
|
||||
: octokit.rest.repos.downloadTarballArchive
|
||||
const response = await download({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
ref: commit || ref
|
||||
})
|
||||
return Buffer.from(response.data as ArrayBuffer) // response.data is ArrayBuffer
|
||||
}
|
165
src/input-helper.ts
Normal file
165
src/input-helper.ts
Normal file
@ -0,0 +1,165 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fsHelper from './fs-helper'
|
||||
import * as github from '@actions/github'
|
||||
import * as path from 'path'
|
||||
import * as workflowContextHelper from './workflow-context-helper'
|
||||
import {IGitSourceSettings} from './git-source-settings'
|
||||
|
||||
export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
const result = {} as unknown as IGitSourceSettings
|
||||
|
||||
// GitHub workspace
|
||||
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
|
||||
if (!githubWorkspacePath) {
|
||||
throw new Error('GITHUB_WORKSPACE not defined')
|
||||
}
|
||||
githubWorkspacePath = path.resolve(githubWorkspacePath)
|
||||
core.debug(`GITHUB_WORKSPACE = '${githubWorkspacePath}'`)
|
||||
fsHelper.directoryExistsSync(githubWorkspacePath, true)
|
||||
|
||||
// Qualified repository
|
||||
const qualifiedRepository =
|
||||
core.getInput('repository') ||
|
||||
`${github.context.repo.owner}/${github.context.repo.repo}`
|
||||
core.debug(`qualified repository = '${qualifiedRepository}'`)
|
||||
const splitRepository = qualifiedRepository.split('/')
|
||||
if (
|
||||
splitRepository.length !== 2 ||
|
||||
!splitRepository[0] ||
|
||||
!splitRepository[1]
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid repository '${qualifiedRepository}'. Expected format {owner}/{repo}.`
|
||||
)
|
||||
}
|
||||
result.repositoryOwner = splitRepository[0]
|
||||
result.repositoryName = splitRepository[1]
|
||||
|
||||
// Repository path
|
||||
result.repositoryPath = core.getInput('path') || '.'
|
||||
result.repositoryPath = path.resolve(
|
||||
githubWorkspacePath,
|
||||
result.repositoryPath
|
||||
)
|
||||
if (
|
||||
!(result.repositoryPath + path.sep).startsWith(
|
||||
githubWorkspacePath + path.sep
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Repository path '${result.repositoryPath}' is not under '${githubWorkspacePath}'`
|
||||
)
|
||||
}
|
||||
|
||||
// Workflow repository?
|
||||
const isWorkflowRepository =
|
||||
qualifiedRepository.toUpperCase() ===
|
||||
`${github.context.repo.owner}/${github.context.repo.repo}`.toUpperCase()
|
||||
|
||||
// Source branch, source version
|
||||
result.ref = core.getInput('ref')
|
||||
if (!result.ref) {
|
||||
if (isWorkflowRepository) {
|
||||
result.ref = github.context.ref
|
||||
result.commit = github.context.sha
|
||||
|
||||
// Some events have an unqualifed ref. For example when a PR is merged (pull_request closed event),
|
||||
// the ref is unqualifed like "main" instead of "refs/heads/main".
|
||||
if (result.commit && result.ref && !result.ref.startsWith('refs/')) {
|
||||
result.ref = `refs/heads/${result.ref}`
|
||||
}
|
||||
}
|
||||
}
|
||||
// SHA?
|
||||
else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) {
|
||||
result.commit = result.ref
|
||||
result.ref = ''
|
||||
}
|
||||
core.debug(`ref = '${result.ref}'`)
|
||||
core.debug(`commit = '${result.commit}'`)
|
||||
|
||||
// Clean
|
||||
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
|
||||
core.debug(`clean = ${result.clean}`)
|
||||
|
||||
// Filter
|
||||
const filter = core.getInput('filter')
|
||||
if (filter) {
|
||||
result.filter = filter
|
||||
}
|
||||
|
||||
core.debug(`filter = ${result.filter}`)
|
||||
|
||||
// Sparse checkout
|
||||
const sparseCheckout = core.getMultilineInput('sparse-checkout')
|
||||
if (sparseCheckout.length) {
|
||||
result.sparseCheckout = sparseCheckout
|
||||
core.debug(`sparse checkout = ${result.sparseCheckout}`)
|
||||
}
|
||||
|
||||
result.sparseCheckoutConeMode =
|
||||
(core.getInput('sparse-checkout-cone-mode') || 'true').toUpperCase() ===
|
||||
'TRUE'
|
||||
|
||||
// Fetch depth
|
||||
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))
|
||||
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {
|
||||
result.fetchDepth = 0
|
||||
}
|
||||
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
|
||||
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'
|
||||
core.debug(`lfs = ${result.lfs}`)
|
||||
|
||||
// Submodules
|
||||
result.submodules = false
|
||||
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
|
||||
}
|
47
src/main.ts
Normal file
47
src/main.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as coreCommand from '@actions/core/lib/command'
|
||||
import * as gitSourceProvider from './git-source-provider'
|
||||
import * as inputHelper from './input-helper'
|
||||
import * as path from 'path'
|
||||
import * as stateHelper from './state-helper'
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const sourceSettings = await inputHelper.getInputs()
|
||||
|
||||
try {
|
||||
// Register problem matcher
|
||||
coreCommand.issueCommand(
|
||||
'add-matcher',
|
||||
{},
|
||||
path.join(__dirname, 'problem-matcher.json')
|
||||
)
|
||||
|
||||
// Get sources
|
||||
await gitSourceProvider.getSource(sourceSettings)
|
||||
core.setOutput('ref', sourceSettings.ref)
|
||||
} finally {
|
||||
// Unregister problem matcher
|
||||
coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '')
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(`${(error as any)?.message ?? error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup(): Promise<void> {
|
||||
try {
|
||||
await gitSourceProvider.cleanup(stateHelper.RepositoryPath)
|
||||
} catch (error) {
|
||||
core.warning(`${(error as any)?.message ?? error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Main
|
||||
if (!stateHelper.IsPost) {
|
||||
run()
|
||||
}
|
||||
// Post
|
||||
else {
|
||||
cleanup()
|
||||
}
|
126
src/misc/generate-docs.ts
Normal file
126
src/misc/generate-docs.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import * as yaml from 'js-yaml'
|
||||
|
||||
//
|
||||
// SUMMARY
|
||||
//
|
||||
// This script rebuilds the usage section in the README.md to be consistent with the action.yml
|
||||
|
||||
function updateUsage(
|
||||
actionReference: string,
|
||||
actionYamlPath = 'action.yml',
|
||||
readmePath = 'README.md',
|
||||
startToken = '<!-- start usage -->',
|
||||
endToken = '<!-- end usage -->'
|
||||
): void {
|
||||
if (!actionReference) {
|
||||
throw new Error('Parameter actionReference must not be empty')
|
||||
}
|
||||
|
||||
// Load the action.yml
|
||||
const actionYaml = yaml.load(fs.readFileSync(actionYamlPath).toString())
|
||||
|
||||
// Load the README
|
||||
const originalReadme = fs.readFileSync(readmePath).toString()
|
||||
|
||||
// Find the start token
|
||||
const startTokenIndex = originalReadme.indexOf(startToken)
|
||||
if (startTokenIndex < 0) {
|
||||
throw new Error(`Start token '${startToken}' not found`)
|
||||
}
|
||||
|
||||
// Find the end token
|
||||
const endTokenIndex = originalReadme.indexOf(endToken)
|
||||
if (endTokenIndex < 0) {
|
||||
throw new Error(`End token '${endToken}' not found`)
|
||||
} else if (endTokenIndex < startTokenIndex) {
|
||||
throw new Error('Start token must appear before end token')
|
||||
}
|
||||
|
||||
// Build the new README
|
||||
const newReadme: string[] = []
|
||||
|
||||
// Append the beginning
|
||||
newReadme.push(originalReadme.substr(0, startTokenIndex + startToken.length))
|
||||
|
||||
// Build the new usage section
|
||||
newReadme.push('```yaml', `- uses: ${actionReference}`, ' with:')
|
||||
const inputs = actionYaml.inputs
|
||||
let firstInput = true
|
||||
for (const key of Object.keys(inputs)) {
|
||||
const input = inputs[key]
|
||||
|
||||
// Line break between inputs
|
||||
if (!firstInput) {
|
||||
newReadme.push('')
|
||||
}
|
||||
|
||||
// Constrain the width of the description
|
||||
const width = 80
|
||||
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) {
|
||||
// Longer than width? Find a space to break apart
|
||||
let segment: string = description
|
||||
if (description.length > width) {
|
||||
segment = description.substr(0, width + 1)
|
||||
while (!segment.endsWith(' ') && !segment.endsWith('\n') && segment) {
|
||||
segment = segment.substr(0, segment.length - 1)
|
||||
}
|
||||
|
||||
// Trimmed too much?
|
||||
if (segment.length < width * 0.67) {
|
||||
segment = description
|
||||
}
|
||||
} else {
|
||||
segment = description
|
||||
}
|
||||
|
||||
// Check for newline
|
||||
const newlineIndex = segment.indexOf('\n')
|
||||
if (newlineIndex >= 0) {
|
||||
segment = segment.substr(0, newlineIndex + 1)
|
||||
}
|
||||
|
||||
// Append segment
|
||||
newReadme.push(` # ${segment}`.trimRight())
|
||||
|
||||
// Remaining
|
||||
description = description.substr(segment.length)
|
||||
}
|
||||
|
||||
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}`)
|
||||
}
|
||||
|
||||
// Input name
|
||||
newReadme.push(` ${key}: ''`)
|
||||
|
||||
firstInput = false
|
||||
}
|
||||
|
||||
newReadme.push('```')
|
||||
|
||||
// Append the end
|
||||
newReadme.push(originalReadme.substr(endTokenIndex))
|
||||
|
||||
// Write the new README
|
||||
fs.writeFileSync(readmePath, newReadme.join(os.EOL))
|
||||
}
|
||||
|
||||
updateUsage(
|
||||
'actions/checkout@v4',
|
||||
path.join(__dirname, '..', '..', 'action.yml'),
|
||||
path.join(__dirname, '..', '..', 'README.md')
|
||||
)
|
8
src/misc/licensed-check.sh
Executable file
8
src/misc/licensed-check.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
src/misc/licensed-download.sh
|
||||
|
||||
echo 'Running: licensed cached'
|
||||
_temp/licensed-3.6.0/licensed status
|
24
src/misc/licensed-download.sh
Executable file
24
src/misc/licensed-download.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ ! -f _temp/licensed-3.6.0.done ]; then
|
||||
echo 'Clearing temp'
|
||||
rm -rf _temp/licensed-3.6.0 || true
|
||||
|
||||
echo 'Downloading licensed'
|
||||
mkdir -p _temp/licensed-3.6.0
|
||||
pushd _temp/licensed-3.6.0
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.6.0/licensed-3.6.0-darwin-x64.tar.gz
|
||||
else
|
||||
curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.6.0/licensed-3.6.0-linux-x64.tar.gz
|
||||
fi
|
||||
|
||||
echo 'Extracting licenesed'
|
||||
tar -xzf licensed.tar.gz
|
||||
popd
|
||||
touch _temp/licensed-3.6.0.done
|
||||
else
|
||||
echo 'Licensed already downloaded'
|
||||
fi
|
8
src/misc/licensed-generate.sh
Executable file
8
src/misc/licensed-generate.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
src/misc/licensed-download.sh
|
||||
|
||||
echo 'Running: licensed cached'
|
||||
_temp/licensed-3.6.0/licensed cache
|
287
src/ref-helper.ts
Normal file
287
src/ref-helper.ts
Normal file
@ -0,0 +1,287 @@
|
||||
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 {
|
||||
ref: string
|
||||
startPoint: string
|
||||
}
|
||||
|
||||
export async function getCheckoutInfo(
|
||||
git: IGitCommandManager,
|
||||
ref: string,
|
||||
commit: string
|
||||
): Promise<ICheckoutInfo> {
|
||||
if (!git) {
|
||||
throw new Error('Arg git cannot be empty')
|
||||
}
|
||||
|
||||
if (!ref && !commit) {
|
||||
throw new Error('Args ref and commit cannot both be empty')
|
||||
}
|
||||
|
||||
const result = {} as unknown as ICheckoutInfo
|
||||
const upperRef = (ref || '').toUpperCase()
|
||||
|
||||
// SHA only
|
||||
if (!ref) {
|
||||
result.ref = commit
|
||||
}
|
||||
// refs/heads/
|
||||
else if (upperRef.startsWith('REFS/HEADS/')) {
|
||||
const branch = ref.substring('refs/heads/'.length)
|
||||
result.ref = branch
|
||||
result.startPoint = `refs/remotes/origin/${branch}`
|
||||
}
|
||||
// refs/pull/
|
||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||
const branch = ref.substring('refs/pull/'.length)
|
||||
result.ref = `refs/remotes/pull/${branch}`
|
||||
}
|
||||
// refs/tags/
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
result.ref = ref
|
||||
}
|
||||
// refs/
|
||||
else if (upperRef.startsWith('REFS/')) {
|
||||
result.ref = commit ? commit : ref
|
||||
}
|
||||
// Unqualified ref, check for a matching branch or tag
|
||||
else {
|
||||
if (await git.branchExists(true, `origin/${ref}`)) {
|
||||
result.ref = ref
|
||||
result.startPoint = `refs/remotes/origin/${ref}`
|
||||
} else if (await git.tagExists(`${ref}`)) {
|
||||
result.ref = `refs/tags/${ref}`
|
||||
} else {
|
||||
throw new Error(
|
||||
`A branch or tag with the name '${ref}' could not be found`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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[] {
|
||||
if (!ref && !commit) {
|
||||
throw new Error('Args ref and commit cannot both be empty')
|
||||
}
|
||||
|
||||
const upperRef = (ref || '').toUpperCase()
|
||||
|
||||
// SHA
|
||||
if (commit) {
|
||||
// refs/heads
|
||||
if (upperRef.startsWith('REFS/HEADS/')) {
|
||||
const branch = ref.substring('refs/heads/'.length)
|
||||
return [`+${commit}:refs/remotes/origin/${branch}`]
|
||||
}
|
||||
// refs/pull/
|
||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||
const branch = ref.substring('refs/pull/'.length)
|
||||
return [`+${commit}:refs/remotes/pull/${branch}`]
|
||||
}
|
||||
// refs/tags/
|
||||
else if (upperRef.startsWith('REFS/TAGS/')) {
|
||||
return [`+${commit}:${ref}`]
|
||||
}
|
||||
// Otherwise no destination ref
|
||||
else {
|
||||
return [commit]
|
||||
}
|
||||
}
|
||||
// Unqualified ref, check for a matching branch or tag
|
||||
else if (!upperRef.startsWith('REFS/')) {
|
||||
return [
|
||||
`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`,
|
||||
`+refs/tags/${ref}*:refs/tags/${ref}*`
|
||||
]
|
||||
}
|
||||
// refs/heads/
|
||||
else if (upperRef.startsWith('REFS/HEADS/')) {
|
||||
const branch = ref.substring('refs/heads/'.length)
|
||||
return [`+${ref}:refs/remotes/origin/${branch}`]
|
||||
}
|
||||
// refs/pull/
|
||||
else if (upperRef.startsWith('REFS/PULL/')) {
|
||||
const branch = ref.substring('refs/pull/'.length)
|
||||
return [`+${ref}:refs/remotes/pull/${branch}`]
|
||||
}
|
||||
// refs/tags/
|
||||
else {
|
||||
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))
|
||||
}
|
5
src/regexp-helper.ts
Normal file
5
src/regexp-helper.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function escape(value: string): string {
|
||||
return value.replace(/[^a-zA-Z0-9_]/g, x => {
|
||||
return `\\${x}`
|
||||
})
|
||||
}
|
61
src/retry-helper.ts
Normal file
61
src/retry-helper.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import * as core from '@actions/core'
|
||||
|
||||
const defaultMaxAttempts = 3
|
||||
const defaultMinSeconds = 10
|
||||
const defaultMaxSeconds = 20
|
||||
|
||||
export class RetryHelper {
|
||||
private maxAttempts: number
|
||||
private minSeconds: number
|
||||
private maxSeconds: number
|
||||
|
||||
constructor(
|
||||
maxAttempts: number = defaultMaxAttempts,
|
||||
minSeconds: number = defaultMinSeconds,
|
||||
maxSeconds: number = defaultMaxSeconds
|
||||
) {
|
||||
this.maxAttempts = maxAttempts
|
||||
this.minSeconds = Math.floor(minSeconds)
|
||||
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> {
|
||||
let attempt = 1
|
||||
while (attempt < this.maxAttempts) {
|
||||
// Try
|
||||
try {
|
||||
return await action()
|
||||
} catch (err) {
|
||||
core.info((err as any)?.message)
|
||||
}
|
||||
|
||||
// Sleep
|
||||
const seconds = this.getSleepAmount()
|
||||
core.info(`Waiting ${seconds} seconds before trying again`)
|
||||
await this.sleep(seconds)
|
||||
attempt++
|
||||
}
|
||||
|
||||
// Last attempt
|
||||
return await action()
|
||||
}
|
||||
|
||||
private getSleepAmount(): number {
|
||||
return (
|
||||
Math.floor(Math.random() * (this.maxSeconds - this.minSeconds + 1)) +
|
||||
this.minSeconds
|
||||
)
|
||||
}
|
||||
|
||||
private async sleep(seconds: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
|
||||
}
|
||||
}
|
||||
|
||||
export async function execute<T>(action: () => Promise<T>): Promise<T> {
|
||||
const retryHelper = new RetryHelper()
|
||||
return await retryHelper.execute(action)
|
||||
}
|
60
src/state-helper.ts
Normal file
60
src/state-helper.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import * as core from '@actions/core'
|
||||
|
||||
/**
|
||||
* Indicates whether the POST action is running
|
||||
*/
|
||||
export const IsPost = !!core.getState('isPost')
|
||||
|
||||
/**
|
||||
* The repository path for the POST action. The value is empty during the MAIN action.
|
||||
*/
|
||||
export const RepositoryPath = core.getState('repositoryPath')
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function setRepositoryPath(repositoryPath: string) {
|
||||
core.saveState('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.
|
||||
// This is necessary since we don't have a separate entry point.
|
||||
if (!IsPost) {
|
||||
core.saveState('isPost', 'true')
|
||||
}
|
81
src/url-helper.ts
Normal file
81
src/url-helper.ts
Normal file
@ -0,0 +1,81 @@
|
||||
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 resolvedUrl = process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
if (hasContent(url, WhitespaceMode.Trim)) {
|
||||
resolvedUrl = url!
|
||||
}
|
||||
|
||||
return new URL(resolvedUrl)
|
||||
}
|
||||
|
||||
export function getServerApiUrl(url?: string): string {
|
||||
if (hasContent(url, WhitespaceMode.Trim)) {
|
||||
let serverUrl = getServerUrl(url)
|
||||
if (isGhes(url)) {
|
||||
serverUrl.pathname = 'api/v3'
|
||||
} else {
|
||||
serverUrl.hostname = 'api.' + serverUrl.hostname
|
||||
}
|
||||
|
||||
return pruneSuffix(serverUrl.toString(), '/')
|
||||
}
|
||||
|
||||
return process.env['GITHUB_API_URL'] || 'https://api.github.com'
|
||||
}
|
||||
|
||||
export function isGhes(url?: string): boolean {
|
||||
const ghUrl = new URL(
|
||||
url || process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
)
|
||||
|
||||
const hostname = ghUrl.hostname.trimEnd().toUpperCase()
|
||||
const isGitHubHost = hostname === 'GITHUB.COM'
|
||||
const isGitHubEnterpriseCloudHost = hostname.endsWith('.GHE.COM')
|
||||
const isLocalHost = hostname.endsWith('.LOCALHOST')
|
||||
|
||||
return !isGitHubHost && !isGitHubEnterpriseCloudHost && !isLocalHost
|
||||
}
|
||||
|
||||
function pruneSuffix(text: string, suffix: string) {
|
||||
if (hasContent(suffix, WhitespaceMode.Preserve) && text?.endsWith(suffix)) {
|
||||
return text.substring(0, text.length - suffix.length)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
enum WhitespaceMode {
|
||||
Trim,
|
||||
Preserve
|
||||
}
|
||||
|
||||
function hasContent(
|
||||
text: string | undefined,
|
||||
whitespaceMode: WhitespaceMode
|
||||
): boolean {
|
||||
let refinedText = text ?? ''
|
||||
if (whitespaceMode == WhitespaceMode.Trim) {
|
||||
refinedText = refinedText.trim()
|
||||
}
|
||||
return refinedText.length > 0
|
||||
}
|
31
src/workflow-context-helper.ts
Normal file
31
src/workflow-context-helper.ts
Normal file
@ -0,0 +1,31 @@
|
||||
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
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user