Compare commits

..

1 Commits

Author SHA1 Message Date
d8d55467a3 Revert "Upgrade checkout to v1.1.0 to better support scripting git. (#56)"
This reverts commit 0b496e91ec7ae4428c3ed2eeb4c3a40df431f2cc.
2019-11-04 14:40:15 -05:00
37 changed files with 31 additions and 25647 deletions

View File

@ -1,3 +0,0 @@
dist/
lib/
node_modules/

View File

@ -1,58 +0,0 @@
{
"plugins": ["jest", "@typescript-eslint"],
"extends": ["plugin:github/es6"],
"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",
"@typescript-eslint/ban-ts-ignore": "error",
"camelcase": "off",
"@typescript-eslint/camelcase": "error",
"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
"@typescript-eslint/func-call-spacing": ["error", "never"],
"@typescript-eslint/generic-type-naming": ["error", "^[A-Z][A-Za-z]*$"],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extraneous-class": "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-object-literal-type-assertion": "error",
"@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-interface": "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
}
}

View File

@ -1,101 +0,0 @@
name: Build and Test
on:
pull_request:
push:
branches:
- master
- releases/*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1 # todo: switch to v2
- run: npm ci
- run: npm run build
- run: npm run format-check
- run: npm run lint
- run: npm run pack
- run: npm run gendocs
- 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@v2
# Basic checkout
- name: Basic checkout
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: Clean checkout
uses: ./
with:
ref: test-data/v2/basic
path: basic
- name: Verify clean
shell: bash
run: __test__/verify-clean.sh
# Side by side
- name: Side by side checkout 1
uses: ./
with:
ref: test-data/v2/side-by-side-1
path: side-by-side-1
- name: Side by side checkout 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
# LFS
- name: LFS checkout
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
test-job-container:
runs-on: ubuntu-latest
container: alpine:latest
steps:
# Clone this repo
- name: Checkout
uses: actions/checkout@v2
# Basic checkout
- name: Basic checkout
uses: ./
with:
ref: test-data/v2/basic
path: basic
- name: Verify basic
run: __test__/verify-basic.sh --archive

2
.gitignore vendored
View File

@ -1,2 +0,0 @@
lib/
node_modules/

View File

@ -1,3 +0,0 @@
dist/
lib/
node_modules/

View File

@ -1,11 +0,0 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid",
"parser": "typescript"
}

View File

@ -1,23 +0,0 @@
# Changelog
## v2 (beta)
- Improved fetch performance
- The default behavior now fetches only the SHA being checked-out
- Script authenticated git commands
- Persists `with.token` in the local git config
- Enables your scripts to run authenticated git commands
- Post-job cleanup removes the token
- Coming soon: Opt out by setting `with.persist-credentials` to `false`
- Creates a local branch
- No longer detached HEAD when checking out a branch
- A local branch is created with the corresponding upstream branch set
- Improved layout
- `with.path` is always relative to `github.workspace`
- Aligns better with container actions, where `github.workspace` gets mapped in
- Removed input `submodules`
## v1
Refer [here](https://github.com/actions/checkout/blob/v1/CHANGELOG.md) for the V1 changelog

103
README.md
View File

@ -1,104 +1,35 @@
<p align="center"> # checkout
<a href="https://github.com/actions/checkout"><img alt="GitHub Actions status" src="https://github.com/actions/checkout/workflows/test-local/badge.svg"></a>
</p>
# Checkout V2 This action checks out your repository to `$GITHUB_WORKSPACE`, so that your workflow can access the contents of your repository.
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it. By default, this is equivalent to running `git fetch` and `git checkout $GITHUB_SHA`, so that you'll always have your repo contents at the version that triggered the workflow.
See [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn what `$GITHUB_SHA` is for different kinds of events.
By default, the repository that triggered the workflow is checked-out, for the ref/SHA that triggered the event.
Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events.
# What's new
- Improved fetch performance
- The default behavior now fetches only the commit being checked-out
- Script authenticated git commands
- Persists the input `token` in the local git config
- Enables your scripts to run authenticated git commands
- Post-job cleanup removes the token
- Opt out by setting the input `persist-credentials: false`
- Creates a local branch
- No longer detached HEAD when checking out a branch
- A local branch is created with the corresponding upstream branch set
- Improved layout
- The input `path` is always relative to $GITHUB_WORKSPACE
- Aligns better with container actions, where $GITHUB_WORKSPACE gets mapped in
- Fallback to REST API download
- When Git 2.18 or higher is not in the PATH, the REST API will be used to download the files
- Removed input `submodules`
Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous versions.
# Usage # Usage
<!-- start usage --> See [action.yml](action.yml)
Basic:
```yaml ```yaml
- uses: actions/checkout@v2 steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with: with:
# Repository name with owner. For example, actions/checkout node-version: 10.x
# Default: ${{ github.repository }} - run: npm install
repository: '' - run: npm test
# 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, defaults to `master`.
ref: ''
# Auth token used to fetch the repository. The token is stored in the local git
# config, which enables your scripts to run authenticated git commands. The
# post-job step removes the token from the git config.
# Default: ${{ github.token }}
token: ''
# Whether to persist the token in the git config
# Default: true
persist-credentials: ''
# Relative path under $GITHUB_WORKSPACE to place the repository
path: ''
# Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching
# Default: true
clean: ''
# Number of commits to fetch. 0 indicates all history.
# Default: 1
fetch-depth: ''
# Whether to download Git-LFS files
# Default: false
lfs: ''
``` ```
<!-- end usage -->
## Checkout a different branch By default, the branch or tag ref that triggered the workflow will be checked out. If you wish to check out a different branch, specify that using `with.ref`:
```yaml ```yaml
- uses: actions/checkout@v2 - uses: actions/checkout@master
with: with:
ref: some-branch ref: some-branch
``` ```
## Checkout a different, private repository For more details, see [Contexts and expression syntax for GitHub Actions](https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions)
```yaml
- uses: actions/checkout@v2
with:
repository: myAccount/myRepository
ref: refs/heads/master
token: ${{ secrets.GitHub_PAT }} # `GitHub_PAT` is a secret that contains your PAT
```
> - `${{ github.token }}` is scoped to the current repository, so if you want to checkout another repository that is private you will need to provide your own [PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line).
## Checkout the HEAD commit of a PR, rather than the merge commit
```yaml
- uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
```
# License # License

View File

@ -1,45 +0,0 @@
import {GitVersion} from '../lib/git-version'
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()
})
})

View File

@ -1,120 +0,0 @@
import * as assert from 'assert'
import * as path from 'path'
import {ISourceSettings} from '../lib/git-source-provider'
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
// Late bind
let inputHelper: any
// Mock @actions/core
let inputs = {} as any
const mockCore = jest.genMockFromModule('@actions/core') as any
mockCore.getInput = (name: string) => {
return inputs[name]
}
// Mock @actions/github
const mockGitHub = jest.genMockFromModule('@actions/github') as any
mockGitHub.context = {
repo: {
owner: 'some-owner',
repo: 'some-repo'
},
ref: 'refs/heads/some-ref',
sha: '1234567890123456789012345678901234567890'
}
// Mock ./fs-helper
const mockFSHelper = jest.genMockFromModule('../lib/fs-helper') as any
mockFSHelper.directoryExistsSync = (path: string) => path == gitHubWorkspace
describe('input-helper tests', () => {
beforeAll(() => {
// GitHub workspace
process.env['GITHUB_WORKSPACE'] = gitHubWorkspace
// Mocks
jest.setMock('@actions/core', mockCore)
jest.setMock('@actions/github', mockGitHub)
jest.setMock('../lib/fs-helper', mockFSHelper)
// Now import
inputHelper = require('../lib/input-helper')
})
beforeEach(() => {
// Reset inputs
inputs = {}
})
afterAll(() => {
// Reset GitHub workspace
delete process.env['GITHUB_WORKSPACE']
if (originalGitHubWorkspace) {
process.env['GITHUB_WORKSPACE'] = originalGitHubWorkspace
}
// Reset modules
jest.resetModules()
})
it('sets defaults', () => {
const settings: ISourceSettings = 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.fetchDepth).toBe(1)
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)
})
it('requires qualified repo', () => {
inputs.repository = 'some-unqualified-repo'
assert.throws(() => {
inputHelper.getInputs()
}, /Invalid repository 'some-unqualified-repo'/)
})
it('roots path', () => {
inputs.path = 'some-directory/some-subdirectory'
const settings: ISourceSettings = inputHelper.getInputs()
expect(settings.repositoryPath).toBe(
path.join(gitHubWorkspace, 'some-directory', 'some-subdirectory')
)
})
it('sets correct default ref/sha for other repo', () => {
inputs.repository = 'some-owner/some-other-repo'
const settings: ISourceSettings = inputHelper.getInputs()
expect(settings.ref).toBe('refs/heads/master')
expect(settings.commit).toBeFalsy()
})
it('sets ref to empty when explicit sha', () => {
inputs.ref = '1111111111222222222233333333334444444444'
const settings: ISourceSettings = inputHelper.getInputs()
expect(settings.ref).toBeFalsy()
expect(settings.commit).toBe('1111111111222222222233333333334444444444')
})
it('sets sha to empty when explicit ref', () => {
inputs.ref = 'refs/heads/some-other-ref'
const settings: ISourceSettings = inputHelper.getInputs()
expect(settings.ref).toBe('refs/heads/some-other-ref')
expect(settings.commit).toBeFalsy()
})
it('gives good error message for submodules input', () => {
inputs.submodules = 'true'
assert.throws(() => {
inputHelper.getInputs()
}, /The input 'submodules' is not supported/)
})
})

View File

@ -1,10 +0,0 @@
#!/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

View File

@ -1,168 +0,0 @@
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.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.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 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.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')
})
})

View File

@ -1,88 +0,0 @@
const mockCore = jest.genMockFromModule('@actions/core') as any
mockCore.info = (message: string) => {
info.push(message)
}
let info: string[]
let retryHelper: any
describe('retry-helper tests', () => {
beforeAll(() => {
// Mocks
jest.setMock('@actions/core', mockCore)
// Now import
const retryHelperModule = require('../lib/retry-helper')
retryHelper = new retryHelperModule.RetryHelper(3, 0, 0)
})
beforeEach(() => {
// Reset info
info = []
})
afterAll(() => {
// Reset modules
jest.resetModules()
})
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
}
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/)
})
})

View File

@ -1,24 +0,0 @@
#!/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 auth token
cd basic
git fetch --no-tags --depth=1 origin +refs/heads/master:refs/remotes/origin/master
fi

View File

@ -1,13 +0,0 @@
#!/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

View File

@ -1,11 +0,0 @@
#!/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

View File

@ -1,17 +0,0 @@
#!/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 all"
exit 1
fi

View File

@ -1,11 +0,0 @@
#!/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

View File

@ -1,11 +0,0 @@
#!/bin/bash
if [ ! -f "./submodules-not-checked-out/regular-file.txt" ]; then
echo "Expected regular file does not exist"
exit 1
fi
if [ -f "./submodules-not-checked-out/submodule-level-1/submodule-file.txt" ]; then
echo "Unexpected submodule file exists"
exit 1
fi

View File

@ -1,35 +1,23 @@
name: 'Checkout' name: 'Checkout'
description: 'Checkout a Git repository at a particular version' description: 'Checkout a Git repository.'
inputs: inputs:
repository: repository:
description: 'Repository name with owner. For example, actions/checkout' description: 'Repository name'
default: ${{ github.repository }}
ref: ref:
description: > description: 'Ref to checkout (SHA, branch, tag)'
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, defaults to `master`.
token: token:
description: > description: 'Access token for clone repository'
Auth token used to fetch the repository. The token is stored in the local
git config, which enables your scripts to run authenticated git commands.
The post-job step removes the token from the git config.
default: ${{ github.token }}
persist-credentials:
description: 'Whether to persist the token in the git config'
default: true
path:
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
clean: clean:
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching' description: 'If true, execute `execute git clean -ffdx && git reset --hard HEAD` before fetching'
default: true default: true
fetch-depth: submodules:
description: 'Number of commits to fetch. 0 indicates all history.' description: 'Whether to include submodules: false to exclude submodules, true to include only one level of submodules, or recursive to recursively clone submodules; defaults to false'
default: 1
lfs: lfs:
description: 'Whether to download Git-LFS files' description: 'Whether to download Git-LFS files; defaults to false'
default: 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'
runs: runs:
using: node12 # Plugins live on the runner and are only available to a certain set of first party actions.
main: dist/index.js plugin: 'checkout'
post: dist/index.js

16294
dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,13 +0,0 @@
{
"problemMatcher": [
{
"owner": "checkout-git",
"pattern": [
{
"regexp": "^(fatal|error): (.*)$",
"message": 2
}
]
}
]
}

View File

@ -1,11 +0,0 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}

7053
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,55 +0,0 @@
{
"name": "checkout",
"version": "2.0.0",
"description": "checkout action",
"main": "lib/main.js",
"scripts": {
"build": "tsc",
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts",
"lint": "eslint src/**/*.ts",
"pack": "ncc build",
"gendocs": "node lib/misc/generate-docs.js",
"test": "jest",
"all": "npm run build && npm run format && npm run lint && npm run pack && npm run gendocs && npm test"
},
"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.1.3",
"@actions/exec": "^1.0.1",
"@actions/github": "^2.0.0",
"@actions/io": "^1.0.1",
"@actions/tool-cache": "^1.1.2",
"uuid": "^3.3.3"
},
"devDependencies": {
"@types/jest": "^24.0.23",
"@types/node": "^12.7.12",
"@types/uuid": "^3.4.6",
"@typescript-eslint/parser": "^2.8.0",
"@zeit/ncc": "^0.20.5",
"eslint": "^5.16.0",
"eslint-plugin-github": "^2.0.0",
"eslint-plugin-jest": "^22.21.0",
"jest": "^24.9.0",
"jest-circus": "^24.9.0",
"js-yaml": "^3.13.1",
"prettier": "^1.19.1",
"ts-jest": "^24.2.0",
"typescript": "^3.6.4"
}
}

View File

@ -1,77 +0,0 @@
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.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.message}`
)
}
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.code === 'ENOENT') {
return false
}
throw new Error(
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
)
}
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.code === 'ENOENT') {
return false
}
throw new Error(
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
)
}
if (!stats.isDirectory()) {
return true
}
return false
}

View File

@ -1,369 +0,0 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as fshelper from './fs-helper'
import * as io from '@actions/io'
import * as path from 'path'
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
export const MinimumGitVersion = new GitVersion('2.18')
export interface IGitCommandManager {
branchDelete(remote: boolean, branch: string): Promise<void>
branchExists(remote: boolean, pattern: string): Promise<boolean>
branchList(remote: boolean): Promise<string[]>
checkout(ref: string, startPoint: string): Promise<void>
checkoutDetach(): Promise<void>
config(configKey: string, configValue: string): Promise<void>
configExists(configKey: string): Promise<boolean>
fetch(fetchDepth: number, refSpec: string[]): Promise<void>
getWorkingDirectory(): string
init(): Promise<void>
isDetached(): Promise<boolean>
lfsFetch(ref: string): Promise<void>
lfsInstall(): Promise<void>
log1(): Promise<void>
remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
tagExists(pattern: string): Promise<boolean>
tryClean(): Promise<boolean>
tryConfigUnset(configKey: string): Promise<boolean>
tryDisableAutomaticGarbageCollection(): Promise<boolean>
tryGetFetchUrl(): Promise<string>
tryReset(): Promise<boolean>
}
export async function CreateCommandManager(
workingDirectory: string,
lfs: boolean
): Promise<IGitCommandManager> {
return await GitCommandManager.createCommandManager(workingDirectory, lfs)
}
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 workingDirectory = ''
// 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" because the output from
// "branch --list" is more difficult when in a detached HEAD state.
const args = ['rev-parse', '--symbolic']
if (remote) {
args.push('--remotes=origin')
} else {
args.push('--branches')
}
const output = await this.execGit(args)
for (let branch of output.stdout.trim().split('\n')) {
branch = branch.trim()
if (branch) {
result.push(branch)
}
}
return result
}
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): Promise<void> {
await this.execGit(['config', '--local', configKey, configValue])
}
async configExists(configKey: string): Promise<boolean> {
const pattern = configKey.replace(/[^a-zA-Z0-9_]/g, x => {
return `\\${x}`
})
const output = await this.execGit(
['config', '--local', '--name-only', '--get-regexp', pattern],
true
)
return output.exitCode === 0
}
async fetch(fetchDepth: number, refSpec: string[]): Promise<void> {
const args = [
'-c',
'protocol.version=2',
'fetch',
'--no-tags',
'--prune',
'--progress',
'--no-recurse-submodules'
]
if (fetchDepth > 0) {
args.push(`--depth=${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)
})
}
getWorkingDirectory(): string {
return this.workingDirectory
}
async init(): Promise<void> {
await this.execGit(['init', this.workingDirectory])
}
async isDetached(): Promise<boolean> {
// Note, this implementation uses "branch --show-current" because
// "rev-parse --symbolic-full-name HEAD" can fail on a new repo
// with nothing checked out.
const output = await this.execGit(['branch', '--show-current'])
return output.stdout.trim() === ''
}
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(): Promise<void> {
await this.execGit(['log', '-1'])
}
async remoteAdd(remoteName: string, remoteUrl: string): Promise<void> {
await this.execGit(['remote', 'add', remoteName, remoteUrl])
}
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): Promise<boolean> {
const output = await this.execGit(
['config', '--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
}
static async createCommandManager(
workingDirectory: string,
lfs: boolean
): Promise<GitCommandManager> {
const result = new GitCommandManager()
await result.initializeCommandManager(workingDirectory, lfs)
return result
}
private async execGit(
args: string[],
allowAllExitCodes = false
): 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 stdout: string[] = []
const options = {
cwd: this.workingDirectory,
env,
ignoreReturnCode: allowAllExitCodes,
listeners: {
stdout: (data: Buffer) => {
stdout.push(data.toString())
}
}
}
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
result.stdout = stdout.join('')
return result
}
private async initializeCommandManager(
workingDirectory: string,
lfs: 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')
let 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) {
gitVersion = new GitVersion(match[0])
}
}
if (!gitVersion.isValid()) {
throw new Error('Unable to determine git version')
}
// Minimum git version
if (!gitVersion.checkMinimum(MinimumGitVersion)) {
throw new Error(
`Minimum required git version is ${MinimumGitVersion}. Your git ('${this.gitPath}') is ${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}`
)
}
}
// Set the user agent
const gitHttpUserAgent = `git/${gitVersion} (github-actions-checkout)`
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
}
}
class GitOutput {
stdout = ''
exitCode = 0
}

View File

@ -1,303 +0,0 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import * as fsHelper from './fs-helper'
import * as gitCommandManager from './git-command-manager'
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 {IGitCommandManager} from './git-command-manager'
const authConfigKey = `http.https://github.com/.extraheader`
export interface ISourceSettings {
repositoryPath: string
repositoryOwner: string
repositoryName: string
ref: string
commit: string
clean: boolean
fetchDepth: number
lfs: boolean
authToken: string
persistCredentials: boolean
}
export async function getSource(settings: ISourceSettings): Promise<void> {
// Repository URL
core.info(
`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
)
const repositoryUrl = `https://github.com/${encodeURIComponent(
settings.repositoryOwner
)}/${encodeURIComponent(settings.repositoryName)}`
// 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
const git = await getGitCommandManager(settings)
// Prepare existing directory, otherwise recreate
if (isExisting) {
await prepareExistingDirectory(
git,
settings.repositoryPath,
repositoryUrl,
settings.clean
)
}
if (!git) {
// Downloading using REST API
core.info(`The repository will be downloaded using the GitHub REST API`)
core.info(
`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
)
await githubApiHelper.downloadRepository(
settings.authToken,
settings.repositoryOwner,
settings.repositoryName,
settings.ref,
settings.commit,
settings.repositoryPath
)
} else {
// Save state for POST action
stateHelper.setRepositoryPath(settings.repositoryPath)
// Initialize the repository
if (
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
) {
await git.init()
await git.remoteAdd('origin', repositoryUrl)
}
// Disable automatic garbage collection
if (!(await git.tryDisableAutomaticGarbageCollection())) {
core.warning(
`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
)
}
// Remove possible previous extraheader
await removeGitConfig(git, authConfigKey)
try {
// Config auth token
await configureAuthToken(git, settings.authToken)
// LFS install
if (settings.lfs) {
await git.lfsInstall()
}
// Fetch
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
await git.fetch(settings.fetchDepth, refSpec)
// Checkout info
const checkoutInfo = await refHelper.getCheckoutInfo(
git,
settings.ref,
settings.commit
)
// 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.
if (settings.lfs) {
await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
}
// Checkout
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
// Dump some info about the checked out commit
await git.log1()
} finally {
if (!settings.persistCredentials) {
await removeGitConfig(git, authConfigKey)
}
}
}
}
export async function cleanup(repositoryPath: string): Promise<void> {
// Repo exists?
if (!fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))) {
return
}
fsHelper.directoryExistsSync(repositoryPath, true)
// Remove the config key
const git = await gitCommandManager.CreateCommandManager(
repositoryPath,
false
)
await removeGitConfig(git, authConfigKey)
}
async function getGitCommandManager(
settings: ISourceSettings
): Promise<IGitCommandManager> {
core.info(`Working directory is '${settings.repositoryPath}'`)
let git = (null as unknown) as IGitCommandManager
try {
return await gitCommandManager.CreateCommandManager(
settings.repositoryPath,
settings.lfs
)
} catch (err) {
// Git is required for LFS
if (settings.lfs) {
throw err
}
// Otherwise fallback to REST API
return (null as unknown) as IGitCommandManager
}
}
async function prepareExistingDirectory(
git: IGitCommandManager,
repositoryPath: string,
repositoryUrl: string,
clean: boolean
): Promise<void> {
let remove = false
// Check whether using git or REST API
if (!git) {
remove = true
}
// Fetch URL does not match
else if (
!fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) ||
repositoryUrl !== (await git.tryGetFetchUrl())
) {
remove = true
} else {
// Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process
const lockPaths = [
path.join(repositoryPath, '.git', 'index.lock'),
path.join(repositoryPath, '.git', 'shallow.lock')
]
for (const lockPath of lockPaths) {
try {
await io.rmRF(lockPath)
} catch (error) {
core.debug(`Unable to delete '${lockPath}'. ${error.message}`)
}
}
try {
// Checkout detached HEAD
if (!(await git.isDetached())) {
await git.checkoutDetach()
}
// Remove all refs/heads/*
let branches = await git.branchList(false)
for (const branch of branches) {
await git.branchDelete(false, branch)
}
// Remove all refs/remotes/origin/* to avoid conflicts
branches = await git.branchList(true)
for (const branch of branches) {
await git.branchDelete(true, branch)
}
// Clean
if (clean) {
if (!(await git.tryClean())) {
core.debug(
`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`
)
remove = true
} else if (!(await git.tryReset())) {
remove = true
}
if (remove) {
core.warning(
`Unable to clean or reset the repository. The repository will be recreated instead.`
)
}
}
} catch (error) {
core.warning(
`Unable to prepare the existing repository. The repository will be recreated instead.`
)
remove = true
}
}
if (remove) {
// Delete the contents of the directory. Don't delete the directory itself
// since it might be the current working directory.
core.info(`Deleting the contents of '${repositoryPath}'`)
for (const file of await fs.promises.readdir(repositoryPath)) {
await io.rmRF(path.join(repositoryPath, file))
}
}
}
async function configureAuthToken(
git: IGitCommandManager,
authToken: string
): Promise<void> {
// 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 placeholder = `AUTHORIZATION: basic ***`
await git.config(authConfigKey, placeholder)
// Determine the basic credential value
const basicCredential = Buffer.from(
`x-access-token:${authToken}`,
'utf8'
).toString('base64')
core.setSecret(basicCredential)
// Replace the value in the config file
const configPath = path.join(git.getWorkingDirectory(), '.git', 'config')
let content = (await fs.promises.readFile(configPath)).toString()
const placeholderIndex = content.indexOf(placeholder)
if (
placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(placeholder)
) {
throw new Error('Unable to replace auth placeholder in .git/config')
}
content = content.replace(
placeholder,
`AUTHORIZATION: basic ${basicCredential}`
)
await fs.promises.writeFile(configPath, content)
}
async function removeGitConfig(
git: IGitCommandManager,
configKey: string
): Promise<void> {
if (
(await git.configExists(configKey)) &&
!(await git.tryConfigUnset(configKey))
) {
// Load the config contents
core.warning(`Failed to remove '${configKey}' from the git config`)
}
}

View File

@ -1,77 +0,0 @@
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
}
}

View File

@ -1,92 +0,0 @@
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 {default as uuid} from 'uuid/v4'
import {ReposGetArchiveLinkParams} from '@octokit/rest'
const IS_WINDOWS = process.platform === 'win32'
export async function downloadRepository(
authToken: string,
owner: string,
repo: string,
ref: string,
commit: string,
repositoryPath: string
): Promise<void> {
// Download the archive
let archiveData = await retryHelper.execute(async () => {
core.info('Downloading the archive')
return await downloadArchive(authToken, owner, repo, ref, commit)
})
// Write archive to disk
core.info('Writing archive to disk')
const uniqueId = uuid()
const archivePath = 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)
}
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)
}
}
io.rmRF(extractPath)
}
async function downloadArchive(
authToken: string,
owner: string,
repo: string,
ref: string,
commit: string
): Promise<Buffer> {
const octokit = new github.GitHub(authToken)
const params: ReposGetArchiveLinkParams = {
owner: owner,
repo: repo,
archive_format: IS_WINDOWS ? 'zipball' : 'tarball',
ref: commit || ref
}
const response = await octokit.repos.getArchiveLink(params)
if (response.status != 200) {
throw new Error(
`Unexpected response from GitHub API. Status: ${response.status}, Data: ${response.data}`
)
}
return Buffer.from(response.data) // response.data is ArrayBuffer
}

View File

@ -1,108 +0,0 @@
import * as core from '@actions/core'
import * as fsHelper from './fs-helper'
import * as github from '@actions/github'
import * as path from 'path'
import {ISourceSettings} from './git-source-provider'
export function getInputs(): ISourceSettings {
const result = ({} as unknown) as ISourceSettings
// 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
}
if (!result.ref && !result.commit) {
result.ref = 'refs/heads/master'
}
}
// 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}`)
// Submodules
if (core.getInput('submodules')) {
throw new Error(
"The input 'submodules' is not supported in actions/checkout@v2"
)
}
// 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}`)
// LFS
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'
core.debug(`lfs = ${result.lfs}`)
// Auth token
result.authToken = core.getInput('token')
// Persist credentials
result.persistCredentials =
(core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE'
return result
}

View File

@ -1,46 +0,0 @@
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 = inputHelper.getInputs()
try {
// Register problem matcher
coreCommand.issueCommand(
'add-matcher',
{},
path.join(__dirname, 'problem-matcher.json')
)
// Get sources
await gitSourceProvider.getSource(sourceSettings)
} finally {
// Unregister problem matcher
coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '')
}
} catch (error) {
core.setFailed(error.message)
}
}
async function cleanup(): Promise<void> {
try {
await gitSourceProvider.cleanup(stateHelper.RepositoryPath)
} catch (error) {
core.warning(error.message)
}
}
// Main
if (!stateHelper.IsPost) {
run()
}
// Post
else {
cleanup()
}

View File

@ -1,102 +0,0 @@
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: string = 'action.yml',
readmePath: string = 'README.md',
startToken: string = '<!-- start usage -->',
endToken: string = '<!-- end usage -->'
): void {
if (!actionReference) {
throw new Error('Parameter actionReference must not be empty')
}
// Load the action.yml
const actionYaml = yaml.safeLoad(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
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 = segment.substr(0, segment.length - 1)
}
} else {
segment = description
}
description = description.substr(segment.length) // Remaining
segment = segment.trimRight() // Trim the trailing space
newReadme.push(` # ${segment}`)
}
// Input and default
if (input.default !== undefined) {
newReadme.push(` # Default: ${input.default}`)
}
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@v2',
path.join(__dirname, '..', '..', 'action.yml'),
path.join(__dirname, '..', '..', 'README.md')
)

View File

@ -1,109 +0,0 @@
import {IGitCommandManager} from './git-command-manager'
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/')) {
result.ref = 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 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}`]
}
}

View File

@ -1,61 +0,0 @@
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.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)
}

View File

@ -1,30 +0,0 @@
import * as core from '@actions/core'
import * as coreCommand from '@actions/core/lib/command'
/**
* Indicates whether the POST action is running
*/
export const IsPost = !!process.env['STATE_isPost']
/**
* The repository path for the POST action. The value is empty during the MAIN action.
*/
export const RepositoryPath =
(process.env['STATE_repositoryPath'] as string) || ''
/**
* Save the repository path so the POST action can retrieve the value.
*/
export function setRepositoryPath(repositoryPath: string) {
coreCommand.issueCommand(
'save-state',
{name: 'repositoryPath'},
repositoryPath
)
}
// 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) {
coreCommand.issueCommand('save-state', {name: 'isPost'}, 'true')
}

View File

@ -1,16 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": [
"es6"
],
"outDir": "./lib",
"rootDir": "./src",
"declaration": true,
"strict": true,
"noImplicitAny": false,
"esModuleInterop": true
},
"exclude": ["__test__", "lib", "node_modules"]
}