Compare commits

..

1 Commits

Author SHA1 Message Date
eric sciple
e9fadf668a pr head commit scenario 2019-12-03 18:12:12 -05:00
20 changed files with 958 additions and 5696 deletions

View File

@@ -11,17 +11,13 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-node@v1 - uses: actions/checkout@v1 # todo: switch to v2
with:
node-version: 12.x
- uses: actions/checkout@v2
- run: npm ci - run: npm ci
- run: npm run build - run: npm run build
- run: npm run format-check - run: npm run format-check
- run: npm run lint - run: npm run lint
- run: npm run pack - run: npm run pack
- run: npm run gendocs - run: npm run gendocs
- run: npm test
- name: Verify no unstaged changes - name: Verify no unstaged changes
run: __test__/verify-no-unstaged-changes.sh run: __test__/verify-no-unstaged-changes.sh
@@ -34,7 +30,7 @@ jobs:
steps: steps:
# Clone this repo # Clone this repo
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v1 # todo: switch to V2
# Basic checkout # Basic checkout
- name: Basic checkout - name: Basic checkout
@@ -85,96 +81,3 @@ jobs:
- name: Verify LFS - name: Verify LFS
shell: bash shell: bash
run: __test__/verify-lfs.sh run: __test__/verify-lfs.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: Basic checkout 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: alpine/git:latest
options: --dns 127.0.0.1
services:
squid-proxy:
image: datadog/squid:latest
ports:
- 3128:3128
env:
https_proxy: http://squid-proxy:3128
steps:
# Clone this repo
- name: Checkout
uses: actions/checkout@v2
# Basic checkout using git
- name: Basic checkout
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@v2
# Basic checkout using git
- name: Basic checkout
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: Basic checkout using REST API
uses: ./
with:
ref: test-data/v2/basic
path: basic
- name: Verify basic
run: __test__/verify-basic.sh --archive

174
README.md
View File

@@ -2,30 +2,28 @@
<a href="https://github.com/actions/checkout"><img alt="GitHub Actions status" src="https://github.com/actions/checkout/workflows/test-local/badge.svg"></a> <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> </p>
# Checkout V2 # Checkout V2 beta
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it. This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. Set `fetch-depth` to fetch more history. Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events. By default, the repository that triggered the workflow is checked-out, for the ref/SHA that triggered the event.
The auth token is persisted in the local git config. This enables your scripts to run authenticated git commands. The token is removed during post-job cleanup. Set `persist-credentials: false` to opt-out. Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events.
When Git 2.18 or higher is not in your PATH, falls back to the REST API to download the files. Changes in V2:
- Improved fetch performance
# What's new - The default behavior now fetches only the SHA being checked-out
- Improved performance
- Fetches only a single commit by default
- Script authenticated git commands - Script authenticated git commands
- Auth token persisted in the local git config - 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 - Creates a local branch
- No longer detached HEAD when checking out a branch - No longer detached HEAD when checking out a branch
- A local branch is created with the corresponding upstream branch set
- Improved layout - Improved layout
- The input `path` is always relative to $GITHUB_WORKSPACE - `with.path` is always relative to `github.workspace`
- Aligns better with container actions, where $GITHUB_WORKSPACE gets mapped in - 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
- When using a job container, the container's PATH is used
- Removed input `submodules` - Removed input `submodules`
Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous versions. Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous versions.
@@ -34,28 +32,20 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
<!-- start usage --> <!-- start usage -->
```yaml ```yaml
- uses: actions/checkout@v2 - uses: actions/checkout@v2-beta
with: with:
# Repository name with owner. For example, actions/checkout # Repository name with owner. For example, actions/checkout
# Default: ${{ github.repository }} # Default: ${{ github.repository }}
repository: '' repository: ''
# The branch, tag or SHA to checkout. When checking out the repository that # Ref to checkout (SHA, branch, tag). For the repository that triggered the
# triggered a workflow, this defaults to the reference or SHA for that event. # workflow, defaults to the ref/SHA for the event. Otherwise defaults to master.
# Otherwise, defaults to `master`.
ref: '' ref: ''
# Auth token used to fetch the repository. The token is stored in the local git # Access token for clone repository
# config, which enables your scripts to run authenticated git commands. The
# post-job step removes the token from the git config. [Learn more about creating
# and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
# Default: ${{ github.token }} # Default: ${{ github.token }}
token: '' token: ''
# Whether to persist the token in the git config
# Default: true
persist-credentials: ''
# Relative path under $GITHUB_WORKSPACE to place the repository # Relative path under $GITHUB_WORKSPACE to place the repository
path: '' path: ''
@@ -73,141 +63,31 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
``` ```
<!-- end usage --> <!-- end usage -->
# Scenarios
- [Checkout a different branch](#Checkout-a-different-branch)
- [Checkout HEAD^](#Checkout-HEAD)
- [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side)
- [Checkout multiple repos (nested)](#Checkout-multiple-repos-nested)
- [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
- [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit)
- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
- [Checkout submodules](#Checkout-submodules)
- [Fetch all tags](#Fetch-all-tags)
- [Fetch all branches](#Fetch-all-branches)
- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches)
## Checkout a different branch ## Checkout a different branch
```yaml ```yaml
- uses: actions/checkout@v2 - uses: actions/checkout@v2-beta
with: with:
ref: my-branch ref: some-branch
``` ```
## Checkout HEAD^ ## Checkout a different, private repository
```yaml ```yaml
- uses: actions/checkout@v2 - uses: actions/checkout@v2-beta
with: with:
fetch-depth: 2 repository: myAccount/myRepository
- run: git checkout HEAD^ ref: refs/heads/master
```
## Checkout multiple repos (side by side)
```yaml
- name: Checkout
uses: actions/checkout@v2
with:
path: main
- name: Checkout tools repo
uses: actions/checkout@v2
with:
repository: my-org/my-tools
path: my-tools
```
## Checkout multiple repos (nested)
```yaml
- name: Checkout
uses: actions/checkout@v2
- name: Checkout tools repo
uses: actions/checkout@v2
with:
repository: my-org/my-tools
path: my-tools
```
## Checkout multiple repos (private)
```yaml
- name: Checkout
uses: actions/checkout@v2
with:
path: main
- name: Checkout private tools
uses: actions/checkout@v2
with:
repository: my-org/my-private-tools
token: ${{ secrets.GitHub_PAT }} # `GitHub_PAT` is a secret that contains your PAT token: ${{ secrets.GitHub_PAT }} # `GitHub_PAT` is a secret that contains your PAT
path: my-tools
``` ```
> - `${{ github.token }}` is scoped to the current repository, so if you want to checkout 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).
> - `${{ github.token }}` is scoped to the current repository, so if you want to checkout a different repository that is private you will need to provide your own [PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line). ## Checkout the HEAD commit of a PR, rather than the merge commit
## Checkout pull request HEAD commit instead of merge commit
```yaml ```yaml
- uses: actions/checkout@v2 - uses: actions/checkout@v2-beta
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: refs/pull/${{ github.event.number }}/head
```
## Checkout pull request on closed event
```yaml
on:
pull_request:
branches: [master]
types: [opened, synchronize, closed]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
```
## Checkout submodules
```yaml
- uses: actions/checkout@v2
- name: Checkout submodules
shell: bash
run: |
# If your submodules are configured to use SSH instead of HTTPS please uncomment the following line
# git config --global url."https://github.com/".insteadOf "git@github.com:"
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
git submodule sync --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
```
## Fetch all tags
```yaml
- uses: actions/checkout@v2
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
```
## Fetch all branches
```yaml
- uses: actions/checkout@v2
- run: |
git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
```
## Fetch all history for all tags and branches
```yaml
- uses: actions/checkout@v2
- run: |
git fetch --prune --unshallow
``` ```
# License # License

View File

@@ -1,44 +1,47 @@
import * as assert from 'assert' import * as assert from 'assert'
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 path from 'path'
import {ISourceSettings} from '../lib/git-source-provider' import {ISourceSettings} from '../lib/git-source-provider'
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE'] const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
const gitHubWorkspace = path.resolve('/checkout-tests/workspace') const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
// Inputs for mock @actions/core // Late bind
let inputs = {} as any let inputHelper: any
// Shallow clone original @actions/github context // Mock @actions/core
let originalContext = {...github.context} let inputs = {} as any
const mockCore = jest.genMockFromModule('@actions/core') as any
mockCore.getInput = (name: string) => {
return inputs[name]
}
// Mock @actions/github
const mockGitHub = jest.genMockFromModule('@actions/github') as any
mockGitHub.context = {
repo: {
owner: 'some-owner',
repo: 'some-repo'
},
ref: 'refs/heads/some-ref',
sha: '1234567890123456789012345678901234567890'
}
// Mock ./fs-helper
const mockFSHelper = jest.genMockFromModule('../lib/fs-helper') as any
mockFSHelper.directoryExistsSync = (path: string) => path == gitHubWorkspace
describe('input-helper tests', () => { describe('input-helper tests', () => {
beforeAll(() => { beforeAll(() => {
// Mock @actions/core getInput()
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
return inputs[name]
})
// Mock @actions/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)
// GitHub workspace // GitHub workspace
process.env['GITHUB_WORKSPACE'] = gitHubWorkspace process.env['GITHUB_WORKSPACE'] = gitHubWorkspace
// Mocks
jest.setMock('@actions/core', mockCore)
jest.setMock('@actions/github', mockGitHub)
jest.setMock('../lib/fs-helper', mockFSHelper)
// Now import
inputHelper = require('../lib/input-helper')
}) })
beforeEach(() => { beforeEach(() => {
@@ -47,24 +50,20 @@ describe('input-helper tests', () => {
}) })
afterAll(() => { afterAll(() => {
// Restore GitHub workspace // Reset GitHub workspace
delete process.env['GITHUB_WORKSPACE'] delete process.env['GITHUB_WORKSPACE']
if (originalGitHubWorkspace) { if (originalGitHubWorkspace) {
process.env['GITHUB_WORKSPACE'] = originalGitHubWorkspace process.env['GITHUB_WORKSPACE'] = originalGitHubWorkspace
} }
// Restore @actions/github context // Reset modules
github.context.ref = originalContext.ref jest.resetModules()
github.context.sha = originalContext.sha
// Restore
jest.restoreAllMocks()
}) })
it('sets defaults', () => { it('sets defaults', () => {
const settings: ISourceSettings = inputHelper.getInputs() const settings: ISourceSettings = inputHelper.getInputs()
expect(settings).toBeTruthy() expect(settings).toBeTruthy()
expect(settings.authToken).toBeFalsy() expect(settings.accessToken).toBeFalsy()
expect(settings.clean).toBe(true) expect(settings.clean).toBe(true)
expect(settings.commit).toBeTruthy() expect(settings.commit).toBeTruthy()
expect(settings.commit).toBe('1234567890123456789012345678901234567890') expect(settings.commit).toBe('1234567890123456789012345678901234567890')
@@ -76,19 +75,6 @@ describe('input-helper tests', () => {
expect(settings.repositoryPath).toBe(gitHubWorkspace) expect(settings.repositoryPath).toBe(gitHubWorkspace)
}) })
it('qualifies ref', () => {
let originalRef = github.context.ref
try {
github.context.ref = 'some-unqualified-ref'
const settings: ISourceSettings = 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', () => { it('requires qualified repo', () => {
inputs.repository = 'some-unqualified-repo' inputs.repository = 'some-unqualified-repo'
assert.throws(() => { assert.throws(() => {

View File

@@ -1,6 +0,0 @@
mkdir override-git-version
cd override-git-version
echo @echo override git version 1.2.3 > git.cmd
echo ::add-path::%CD%
cd ..

View File

@@ -1,9 +0,0 @@
#!/bin/sh
mkdir override-git-version
cd override-git-version
echo "#!/bin/sh" > git
echo "echo override git version 1.2.3" >> git
chmod +x git
echo "::add-path::$(pwd)"
cd ..

View File

@@ -1,87 +0,0 @@
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
}
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 +1,10 @@
#!/bin/sh #!/bin/bash
if [ ! -f "./basic/basic-file.txt" ]; then if [ ! -f "./basic/basic-file.txt" ]; then
echo "Expected basic file does not exist" echo "Expected basic file does not exist"
exit 1 exit 1
fi fi
if [ "$1" = "--archive" ]; then # Verify auth token
# Verify no .git folder cd basic
if [ -d "./basic/.git" ]; then git fetch
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,24 +1,16 @@
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 with owner. For example, actions/checkout'
default: ${{ github.repository }} default: ${{ github.repository }}
ref: ref:
description: > description: >
The branch, tag or SHA to checkout. When checking out the repository that Ref to checkout (SHA, branch, tag). For the repository that triggered the
triggered a workflow, this defaults to the reference or SHA for that workflow, defaults to the ref/SHA for the event. Otherwise defaults to master.
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. [Learn more about
creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
default: ${{ github.token }} default: ${{ github.token }}
persist-credentials:
description: 'Whether to persist the token in the git config'
default: true
path: path:
description: 'Relative path under $GITHUB_WORKSPACE to place the repository' description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
clean: clean:
@@ -33,4 +25,4 @@ inputs:
runs: runs:
using: node12 using: node12
main: dist/index.js main: dist/index.js
post: dist/index.js post: dist/index.js

View File

@@ -1,215 +0,0 @@
# ADR 0153: Checkout v2
**Date**: 2019-10-21
**Status**: Accepted
## Context
This ADR details the behavior for `actions/checkout@v2`.
The new action will be written in typescript. We are moving away from runner-plugin actions.
We want to take this opportunity to make behavioral changes, from v1. This document is scoped to those differences.
## Decision
### Inputs
```yaml
repository:
description: 'Repository name with owner. For example, actions/checkout'
default: ${{ github.repository }}
ref:
description: >
The branch, tag or SHA to checkout. When checking out the repository that
triggered a workflow, this defaults to the reference or SHA for that
event. Otherwise, defaults to `master`.
token:
description: >
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. [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 }}
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:
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
default: true
fetch-depth:
description: 'Number of commits to fetch. 0 indicates all history.'
default: 1
fetch-refs:
description: >
Additional refs to fetch: `branches`, `tags`, `pr-base`, or `all`.
Combinations are also accepted. For example: `branches, tags`
default: ''
lfs:
description: 'Whether to download Git-LFS files'
default: false
```
Note:
- `fetch-refs` is new
- `persist-credentials` is new
- `path` behavior is different (refer [below](#path) for details)
- `submodules` was removed (error if specified; add later if needed)
### Fallback to GitHub API
When a sufficient version of git is not in the PATH, fallback to the [web API](https://developer.github.com/v3/repos/contents/#get-archive-link) to download a tarball/zipball.
Note:
- LFS files are not included in the archive. Therefore fail if LFS is set to true.
- Submodules are also not included in the archive. However submodules are not supported by checkout v2 anyway.
### Persist credentials
Persist the token in the git config (http.extraheader). This will allow users to script authenticated git commands, like `git fetch`.
A post script will remove the credentials from the git config (cleanup for self-hosted).
Users may opt-out by specifying `persist-credentials: false`
Note:
- Users scripting `git commit` may need to set the username and email. The service does not provide any reasonable default value. Users can add `git config user.name <NAME>` and `git config user.email <EMAIL>`. We will document this guidance.
- The auth header (stored in the repo's git config), is scoped to all of github `http.https://github.com/.extraheader`
- Additional public remotes also just work.
- If users want to authenticate to an additional private remote, they should provide the `token` input.
- Lines up if we add submodule support in the future. Don't need to worry about calculating relative URLs. Just works, although needs to be persisted in each submodule git config.
- Users opt out of persisted credentials (`persist-credentials: false`), or can script the removal themselves (`git config --unset-all http.https://github.com/.extraheader`).
### Fetch behavior
Fetch only the SHA being built and set depth=1. This significantly reduces the fetch time for large repos.
If a SHA isn't available (e.g. multi repo), then fetch only the specified ref with depth=1.
The input `fetch-depth` can be used to control the depth.
The input `fetch-refs` can be used to fetch additional refs.
Note:
- Fetching a single commit is supported by Git wire protocol version 2. The git client uses protocol version 0 by default. The desired protocol version can be overridden in the git config or on the fetch command line invocation (`-c protocol.version=2`). We will override on the fetch command line, for transparency.
- Git client version 2.18+ (released June 2018) is required for wire protocol version 2.
### Checkout behavior
For CI, checkout will create a local ref with the upstream set. This allows users to script git as they normally would.
For PR, continue to checkout detached head. The PR branch is special - the branch and merge commit are created by the server. It doesn't match a users' local workflow.
Note:
- Consider deleting all local refs during cleanup if that helps avoid collisions. More testing required.
### Path
For the mainline scenario, the disk-layout behavior remains the same.
Remember, given the repo `johndoe/foo`, the mainline disk layout looks like:
```
GITHUB_WORKSPACE=/home/runner/work/foo/foo
RUNNER_WORKSPACE=/home/runner/work/foo
```
V2 introduces a new contraint on the checkout path. The location must now be under `github.workspace`. Whereas the checkout@v1 constraint was one level up, under `runner.workspace`.
V2 no longer changes `github.workspace` to follow wherever the self repo is checked-out.
These behavioral changes align better with container actions. The [documented filesystem contract](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/virtual-environments-for-github-hosted-runners#docker-container-filesystem) is:
- `/github/home`
- `/github/workspace` - Note: GitHub Actions must be run by the default Docker user (root). Ensure your Dockerfile does not set the USER instruction, otherwise you will not be able to access `GITHUB_WORKSPACE`.
- `/github/workflow`
Note:
- The tracking config will not be updated to reflect the path of the workflow repo.
- Any existing workflow repo will not be moved when the checkout path changes. In fact some customers want to checkout the workflow repo twice, side by side against different branches.
- Actions that need to operate only against the root of the self repo, should expose a `path` input.
#### Default value for `path` input
The `path` input will default to `./` which is rooted against `github.workspace`.
This default fits the mainline scenario well: single checkout
For multi-checkout, users must specify the `path` input for at least one of the repositories.
Note:
- An alternative is for the self repo to default to `./` and other repos default to `<REPO_NAME>`. However nested layout is an atypical git layout and therefore is not a good default. Users should supply the path info.
#### Example - Nested layout
The following example checks-out two repositories and creates a nested layout.
```yaml
# Self repo - Checkout to $GITHUB_WORKSPACE
- uses: checkout@v2
# Other repo - Checkout to $GITHUB_WORKSPACE/myscripts
- uses: checkout@v2
with:
repository: myorg/myscripts
path: myscripts
```
#### Example - Side by side layout
The following example checks-out two repositories and creates a side-by-side layout.
```yaml
# Self repo - Checkout to $GITHUB_WORKSPACE/foo
- uses: checkout@v2
with:
path: foo
# Other repo - Checkout to $GITHUB_WORKSPACE/myscripts
- uses: checkout@v2
with:
repository: myorg/myscripts
path: myscripts
```
#### Path impact to problem matchers
Problem matchers associate the source files with annotations.
Today the runner verifies the source file is under the `github.workspace`. Otherwise the source file property is dropped.
Multi-checkout complicates the matter. However even today submodules may cause this heuristic to be inaccurate.
A better solution is:
Given a source file path, walk up the directories until the first `.git/config` is found. Check if it matches the self repo (`url = https://github.com/OWNER/REPO`). If not, drop the source file path.
### Port to typescript
The checkout action should be a typescript action on the GitHub graph, for the following reasons:
- Enables customers to fork the checkout repo and modify
- Serves as an example for customers
- Demystifies the checkout action manifest
- Simplifies the runner
- Reduce the amount of runner code to port (if we ever do)
Note:
- This means job-container images will need git in the PATH, for checkout.
### Branching strategy and release tags
- Create a servicing branch for V1: `releases/v1`
- Merge the changes into `master`
- Release using a new tag `preview`
- When stable, release using a new tag `v2`
## Consequences
- Update the checkout action and readme
- Update samples to consume `actions/checkout@v2`
- Job containers now require git in the PATH for checkout, otherwise fallback to REST API
- Minimum git version 2.18
- Update problem matcher logic regarding source file verification (runner)

5066
dist/index.js vendored

File diff suppressed because one or more lines are too long

177
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "checkout", "name": "checkout",
"version": "2.0.2", "version": "2.0.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -15,55 +15,19 @@
"integrity": "sha512-nvFkxwiicvpzNiCBF4wFBDfnBvi7xp/as7LE1hBxBxKG2L29+gkIPBiLKMVORL+Hg3JNf07AKRfl0V5djoypjQ==" "integrity": "sha512-nvFkxwiicvpzNiCBF4wFBDfnBvi7xp/as7LE1hBxBxKG2L29+gkIPBiLKMVORL+Hg3JNf07AKRfl0V5djoypjQ=="
}, },
"@actions/github": { "@actions/github": {
"version": "2.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-2.1.0.tgz", "resolved": "https://registry.npmjs.org/@actions/github/-/github-1.1.0.tgz",
"integrity": "sha512-G4ncMlh4pLLAvNgHUYUtpWQ1zPf/VYqmRH9oshxLabdaOOnp7i1hgSgzr2xne2YUaSND3uqemd3YYTIsm2f/KQ==", "integrity": "sha512-cHf6PyoNMdei13jEdGPhKprIMFmjVVW/dnM5/9QmQDJ1ZTaGVyezUSCUIC/ySNLRvDUpeFwPYMdThSEJldSbUw==",
"requires": { "requires": {
"@actions/http-client": "^1.0.3", "@octokit/graphql": "^2.0.1",
"@octokit/graphql": "^4.3.1",
"@octokit/rest": "^16.15.0" "@octokit/rest": "^16.15.0"
} }
}, },
"@actions/http-client": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.3.tgz",
"integrity": "sha512-wFwh1U4adB/Zsk4cc9kVqaBOHoknhp/pJQk+aWTocbAZWpIl4Zx/At83WFRLXvxB+5HVTWOACM6qjULMZfQSfw==",
"requires": {
"tunnel": "0.0.6"
},
"dependencies": {
"tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
}
}
},
"@actions/io": { "@actions/io": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.1.tgz",
"integrity": "sha512-rhq+tfZukbtaus7xyUtwKfuiCRXd1hWSfmJNEpFgBQJ4woqPEpsBw04awicjwz9tyG2/MVhAEMfVn664Cri5zA==" "integrity": "sha512-rhq+tfZukbtaus7xyUtwKfuiCRXd1hWSfmJNEpFgBQJ4woqPEpsBw04awicjwz9tyG2/MVhAEMfVn664Cri5zA=="
}, },
"@actions/tool-cache": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-1.1.2.tgz",
"integrity": "sha512-IJczPaZr02ECa3Lgws/TJEVco9tjOujiQSZbO3dHuXXjhd5vrUtfOgGwhmz3/f97L910OraPZ8SknofUk6RvOQ==",
"requires": {
"@actions/core": "^1.1.0",
"@actions/exec": "^1.0.1",
"@actions/io": "^1.0.1",
"semver": "^6.1.0",
"typed-rest-client": "^1.4.0",
"uuid": "^3.3.2"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"@babel/code-frame": { "@babel/code-frame": {
"version": "7.5.5", "version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
@@ -613,66 +577,73 @@
"@types/yargs": "^13.0.0" "@types/yargs": "^13.0.0"
} }
}, },
"@octokit/auth-token": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.0.tgz",
"integrity": "sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg==",
"requires": {
"@octokit/types": "^2.0.0"
}
},
"@octokit/endpoint": { "@octokit/endpoint": {
"version": "5.5.1", "version": "5.4.0",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.4.0.tgz",
"integrity": "sha512-nBFhRUb5YzVTCX/iAK1MgQ4uWo89Gu0TH00qQHoYRCsE12dWcG1OiLd7v2EIo2+tpUKPMOQ62QFy9hy9Vg2ULg==", "integrity": "sha512-DWTNgEKg5KXzvNjKTzcFTnkZiL7te6pQxxumvxPjyjDpcY5V3xzywnNu1WVqySY3Ct1flF/kAoyDdZos6acq3Q==",
"requires": { "requires": {
"@octokit/types": "^2.0.0",
"is-plain-object": "^3.0.0", "is-plain-object": "^3.0.0",
"universal-user-agent": "^4.0.0" "universal-user-agent": "^4.0.0"
},
"dependencies": {
"universal-user-agent": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz",
"integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==",
"requires": {
"os-name": "^3.1.0"
}
}
} }
}, },
"@octokit/graphql": { "@octokit/graphql": {
"version": "4.3.1", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-2.1.3.tgz",
"integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==", "integrity": "sha512-XoXJqL2ondwdnMIW3wtqJWEwcBfKk37jO/rYkoxNPEVeLBDGsGO1TCWggrAlq3keGt/O+C/7VepXnukUxwt5vA==",
"requires": { "requires": {
"@octokit/request": "^5.3.0", "@octokit/request": "^5.0.0",
"@octokit/types": "^2.0.0", "universal-user-agent": "^2.0.3"
"universal-user-agent": "^4.0.0"
} }
}, },
"@octokit/request": { "@octokit/request": {
"version": "5.3.1", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.1.0.tgz",
"integrity": "sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==", "integrity": "sha512-I15T9PwjFs4tbWyhtFU2Kq7WDPidYMvRB7spmxoQRZfxSmiqullG+Nz+KbSmpkfnlvHwTr1e31R5WReFRKMXjg==",
"requires": { "requires": {
"@octokit/endpoint": "^5.5.0", "@octokit/endpoint": "^5.1.0",
"@octokit/request-error": "^1.0.1", "@octokit/request-error": "^1.0.1",
"@octokit/types": "^2.0.0",
"deprecation": "^2.0.0", "deprecation": "^2.0.0",
"is-plain-object": "^3.0.0", "is-plain-object": "^3.0.0",
"node-fetch": "^2.3.0", "node-fetch": "^2.3.0",
"once": "^1.4.0", "once": "^1.4.0",
"universal-user-agent": "^4.0.0" "universal-user-agent": "^4.0.0"
},
"dependencies": {
"universal-user-agent": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz",
"integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==",
"requires": {
"os-name": "^3.1.0"
}
}
} }
}, },
"@octokit/request-error": { "@octokit/request-error": {
"version": "1.2.0", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.0.4.tgz",
"integrity": "sha512-DNBhROBYjjV/I9n7A8kVkmQNkqFAMem90dSxqvPq57e2hBr7mNTX98y3R2zDpqMQHVRpBDjsvsfIGgBzy+4PAg==", "integrity": "sha512-L4JaJDXn8SGT+5G0uX79rZLv0MNJmfGa4vb4vy1NnpjSnWDLJRy6m90udGwvMmavwsStgbv2QNkPzzTCMmL+ig==",
"requires": { "requires": {
"@octokit/types": "^2.0.0",
"deprecation": "^2.0.0", "deprecation": "^2.0.0",
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"@octokit/rest": { "@octokit/rest": {
"version": "16.38.1", "version": "16.33.0",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.38.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.33.0.tgz",
"integrity": "sha512-zyNFx+/Bd1EXt7LQjfrc6H4wryBQ/oDuZeZhGMBSFr1eMPFDmpEweFQR3R25zjKwBQpDY7L5GQO6A3XSaOfV1w==", "integrity": "sha512-t4jMR+odsfooQwmHiREoTQixVTX2DfdbSaO+lKrW9R5XBuk0DW+5T/JdfwtxAGUAHgvDDpWY/NVVDfEPTzxD6g==",
"requires": { "requires": {
"@octokit/auth-token": "^2.4.0", "@octokit/request": "^5.0.0",
"@octokit/request": "^5.2.0",
"@octokit/request-error": "^1.0.2", "@octokit/request-error": "^1.0.2",
"atob-lite": "^2.0.0", "atob-lite": "^2.0.0",
"before-after-hook": "^2.0.0", "before-after-hook": "^2.0.0",
@@ -684,14 +655,16 @@
"octokit-pagination-methods": "^1.1.0", "octokit-pagination-methods": "^1.1.0",
"once": "^1.4.0", "once": "^1.4.0",
"universal-user-agent": "^4.0.0" "universal-user-agent": "^4.0.0"
} },
}, "dependencies": {
"@octokit/types": { "universal-user-agent": {
"version": "2.1.1", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.1.1.tgz", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz",
"integrity": "sha512-89LOYH+d/vsbDX785NOfLxTW88GjNd0lWRz1DVPVsZgg9Yett5O+3MOvwo7iHgvUwbFz0mf/yPIjBkUbs4kxoQ==", "integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==",
"requires": { "requires": {
"@types/node": ">= 8" "os-name": "^3.1.0"
}
}
} }
}, },
"@types/babel__core": { "@types/babel__core": {
@@ -784,7 +757,8 @@
"@types/node": { "@types/node": {
"version": "12.7.12", "version": "12.7.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz",
"integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ==" "integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ==",
"dev": true
}, },
"@types/stack-utils": { "@types/stack-utils": {
"version": "1.0.1", "version": "1.0.1",
@@ -792,15 +766,6 @@
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==",
"dev": true "dev": true
}, },
"@types/uuid": {
"version": "3.4.6",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.6.tgz",
"integrity": "sha512-cCdlC/1kGEZdEglzOieLDYBxHsvEOIg7kp/2FYyVR9Pxakq+Qf/inL3RKQ+PA8gOlI/NnL+fXmQH12nwcGzsHw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/yargs": { "@types/yargs": {
"version": "13.0.3", "version": "13.0.3",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.3.tgz",
@@ -6638,11 +6603,6 @@
"tslib": "^1.8.1" "tslib": "^1.8.1"
} }
}, },
"tunnel": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz",
"integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM="
},
"tunnel-agent": { "tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -6667,15 +6627,6 @@
"prelude-ls": "~1.1.2" "prelude-ls": "~1.1.2"
} }
}, },
"typed-rest-client": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.5.0.tgz",
"integrity": "sha512-DVZRlmsfnTjp6ZJaatcdyvvwYwbWvR4YDNFDqb+qdTxpvaVP99YCpBkA8rxsLtAPjBVoDe4fNsnMIdZTiPuKWg==",
"requires": {
"tunnel": "0.0.4",
"underscore": "1.8.3"
}
},
"typescript": { "typescript": {
"version": "3.6.4", "version": "3.6.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz",
@@ -6702,11 +6653,6 @@
} }
} }
}, },
"underscore": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI="
},
"union-value": { "union-value": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -6720,11 +6666,11 @@
} }
}, },
"universal-user-agent": { "universal-user-agent": {
"version": "4.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-2.1.0.tgz",
"integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==", "integrity": "sha512-8itiX7G05Tu3mGDTdNY2fB4KJ8MgZLS54RdG6PkkfwMAavrXu1mV/lls/GABx9O3Rw4PnTtasxrvbMQoBYY92Q==",
"requires": { "requires": {
"os-name": "^3.1.0" "os-name": "^3.0.0"
} }
}, },
"unset-value": { "unset-value": {
@@ -6807,7 +6753,8 @@
"uuid": { "uuid": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==",
"dev": true
}, },
"validate-npm-package-license": { "validate-npm-package-license": {
"version": "3.0.4", "version": "3.0.4",

View File

@@ -1,6 +1,6 @@
{ {
"name": "checkout", "name": "checkout",
"version": "2.0.2", "version": "2.0.0",
"description": "checkout action", "description": "checkout action",
"main": "lib/main.js", "main": "lib/main.js",
"scripts": { "scripts": {
@@ -31,15 +31,12 @@
"dependencies": { "dependencies": {
"@actions/core": "^1.1.3", "@actions/core": "^1.1.3",
"@actions/exec": "^1.0.1", "@actions/exec": "^1.0.1",
"@actions/github": "^2.0.2", "@actions/github": "^1.1.0",
"@actions/io": "^1.0.1", "@actions/io": "^1.0.1"
"@actions/tool-cache": "^1.1.2",
"uuid": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^24.0.23", "@types/jest": "^24.0.23",
"@types/node": "^12.7.12", "@types/node": "^12.7.12",
"@types/uuid": "^3.4.6",
"@typescript-eslint/parser": "^2.8.0", "@typescript-eslint/parser": "^2.8.0",
"@zeit/ncc": "^0.20.5", "@zeit/ncc": "^0.20.5",
"eslint": "^5.16.0", "eslint": "^5.16.0",

View File

@@ -3,13 +3,8 @@ import * as exec from '@actions/exec'
import * as fshelper from './fs-helper' import * as fshelper from './fs-helper'
import * as io from '@actions/io' import * as io from '@actions/io'
import * as path from 'path' import * as path from 'path'
import * as retryHelper from './retry-helper'
import {GitVersion} from './git-version' 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 { export interface IGitCommandManager {
branchDelete(remote: boolean, branch: string): Promise<void> branchDelete(remote: boolean, branch: string): Promise<void>
branchExists(remote: boolean, pattern: string): Promise<boolean> branchExists(remote: boolean, pattern: string): Promise<boolean>
@@ -77,12 +72,10 @@ class GitCommandManager {
async branchList(remote: boolean): Promise<string[]> { async branchList(remote: boolean): Promise<string[]> {
const result: string[] = [] const result: string[] = []
// Note, this implementation uses "rev-parse --symbolic-full-name" because the output from // Note, this implementation uses "rev-parse --symbolic" because the output from
// "branch --list" is more difficult when in a detached HEAD state. // "branch --list" is more difficult when in a detached HEAD state.
// Note, 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.
const args = ['rev-parse', '--symbolic-full-name'] const args = ['rev-parse', '--symbolic']
if (remote) { if (remote) {
args.push('--remotes=origin') args.push('--remotes=origin')
} else { } else {
@@ -94,12 +87,6 @@ class GitCommandManager {
for (let branch of output.stdout.trim().split('\n')) { for (let branch of output.stdout.trim().split('\n')) {
branch = branch.trim() branch = branch.trim()
if (branch) { if (branch) {
if (branch.startsWith('refs/heads/')) {
branch = branch.substr('refs/heads/'.length)
} else if (branch.startsWith('refs/remotes/')) {
branch = branch.substr('refs/remotes/'.length)
}
result.push(branch) result.push(branch)
} }
} }
@@ -124,7 +111,7 @@ class GitCommandManager {
} }
async config(configKey: string, configValue: string): Promise<void> { async config(configKey: string, configValue: string): Promise<void> {
await this.execGit(['config', '--local', configKey, configValue]) await this.execGit(['config', configKey, configValue])
} }
async configExists(configKey: string): Promise<boolean> { async configExists(configKey: string): Promise<boolean> {
@@ -132,7 +119,7 @@ class GitCommandManager {
return `\\${x}` return `\\${x}`
}) })
const output = await this.execGit( const output = await this.execGit(
['config', '--local', '--name-only', '--get-regexp', pattern], ['config', '--name-only', '--get-regexp', pattern],
true true
) )
return output.exitCode === 0 return output.exitCode === 0
@@ -163,10 +150,22 @@ class GitCommandManager {
args.push(arg) args.push(arg)
} }
const that = this let attempt = 1
await retryHelper.execute(async () => { const maxAttempts = 3
await that.execGit(args) while (attempt <= maxAttempts) {
}) const allowAllExitCodes = attempt < maxAttempts
const output = await this.execGit(args, allowAllExitCodes)
if (output.exitCode === 0) {
break
}
const seconds = this.getRandomIntInclusive(1, 10)
core.warning(
`Git fetch failed with exit code ${output.exitCode}. Waiting ${seconds} seconds before trying again.`
)
await this.sleep(seconds * 1000)
attempt++
}
} }
getWorkingDirectory(): string { getWorkingDirectory(): string {
@@ -178,21 +177,33 @@ class GitCommandManager {
} }
async isDetached(): Promise<boolean> { async isDetached(): Promise<boolean> {
// Note, "branch --show-current" would be simpler but isn't available until Git 2.22 // Note, this implementation uses "branch --show-current" because
const output = await this.execGit( // "rev-parse --symbolic-full-name HEAD" can fail on a new repo
['rev-parse', '--symbolic-full-name', '--verify', '--quiet', 'HEAD'], // with nothing checked out.
true
) const output = await this.execGit(['branch', '--show-current'])
return !output.stdout.trim().startsWith('refs/heads/') return output.stdout.trim() === ''
} }
async lfsFetch(ref: string): Promise<void> { async lfsFetch(ref: string): Promise<void> {
const args = ['lfs', 'fetch', 'origin', ref] const args = ['lfs', 'fetch', 'origin', ref]
const that = this let attempt = 1
await retryHelper.execute(async () => { const maxAttempts = 3
await that.execGit(args) while (attempt <= maxAttempts) {
}) const allowAllExitCodes = attempt < maxAttempts
const output = await this.execGit(args, allowAllExitCodes)
if (output.exitCode === 0) {
break
}
const seconds = this.getRandomIntInclusive(1, 10)
core.warning(
`Git lfs fetch failed with exit code ${output.exitCode}. Waiting ${seconds} seconds before trying again.`
)
await this.sleep(seconds * 1000)
attempt++
}
} }
async lfsInstall(): Promise<void> { async lfsInstall(): Promise<void> {
@@ -219,23 +230,20 @@ class GitCommandManager {
async tryConfigUnset(configKey: string): Promise<boolean> { async tryConfigUnset(configKey: string): Promise<boolean> {
const output = await this.execGit( const output = await this.execGit(
['config', '--local', '--unset-all', configKey], ['config', '--unset-all', configKey],
true true
) )
return output.exitCode === 0 return output.exitCode === 0
} }
async tryDisableAutomaticGarbageCollection(): Promise<boolean> { async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
const output = await this.execGit( const output = await this.execGit(['config', 'gc.auto', '0'], true)
['config', '--local', 'gc.auto', '0'],
true
)
return output.exitCode === 0 return output.exitCode === 0
} }
async tryGetFetchUrl(): Promise<string> { async tryGetFetchUrl(): Promise<string> {
const output = await this.execGit( const output = await this.execGit(
['config', '--local', '--get', 'remote.origin.url'], ['config', '--get', 'remote.origin.url'],
true true
) )
@@ -330,9 +338,13 @@ class GitCommandManager {
} }
// Minimum git version // Minimum git version
if (!gitVersion.checkMinimum(MinimumGitVersion)) { // Note:
// - Auth header not supported before 2.9
// - Wire protocol v2 not supported before 2.18
const minimumGitVersion = new GitVersion('2.18')
if (!gitVersion.checkMinimum(minimumGitVersion)) {
throw new Error( throw new Error(
`Minimum required git version is ${MinimumGitVersion}. Your git ('${this.gitPath}') is ${gitVersion}` `Minimum required git version is ${minimumGitVersion}. Your git ('${this.gitPath}') is ${gitVersion}`
) )
} }
@@ -369,6 +381,16 @@ class GitCommandManager {
core.debug(`Set git useragent to: ${gitHttpUserAgent}`) core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
} }
private getRandomIntInclusive(minimum: number, maximum: number): number {
minimum = Math.floor(minimum)
maximum = Math.floor(maximum)
return Math.floor(Math.random() * (maximum - minimum + 1)) + minimum
}
private async sleep(milliseconds): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
} }
class GitOutput { class GitOutput {

View File

@@ -1,16 +1,14 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as coreCommand from '@actions/core/lib/command'
import * as fs from 'fs' import * as fs from 'fs'
import * as fsHelper from './fs-helper' import * as fsHelper from './fs-helper'
import * as gitCommandManager from './git-command-manager' import * as gitCommandManager from './git-command-manager'
import * as githubApiHelper from './github-api-helper'
import * as io from '@actions/io' import * as io from '@actions/io'
import * as path from 'path' import * as path from 'path'
import * as refHelper from './ref-helper' import * as refHelper from './ref-helper'
import * as stateHelper from './state-helper'
import {IGitCommandManager} from './git-command-manager' import {IGitCommandManager} from './git-command-manager'
const serverUrl = 'https://github.com/' const authConfigKey = `http.https://github.com/.extraheader`
const authConfigKey = `http.${serverUrl}.extraheader`
export interface ISourceSettings { export interface ISourceSettings {
repositoryPath: string repositoryPath: string
@@ -21,12 +19,10 @@ export interface ISourceSettings {
clean: boolean clean: boolean
fetchDepth: number fetchDepth: number
lfs: boolean lfs: boolean
authToken: string accessToken: string
persistCredentials: boolean
} }
export async function getSource(settings: ISourceSettings): Promise<void> { export async function getSource(settings: ISourceSettings): Promise<void> {
// Repository URL
core.info( core.info(
`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}` `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
) )
@@ -47,252 +43,182 @@ export async function getSource(settings: ISourceSettings): Promise<void> {
} }
// Git command manager // Git command manager
const git = await getGitCommandManager(settings) core.info(`Working directory is '${settings.repositoryPath}'`)
const git = await gitCommandManager.CreateCommandManager(
settings.repositoryPath,
settings.lfs
)
// Prepare existing directory, otherwise recreate // Try prepare existing directory, otherwise recreate
if (isExisting) { if (
await prepareExistingDirectory( isExisting &&
!(await tryPrepareExistingDirectory(
git, git,
settings.repositoryPath, settings.repositoryPath,
repositoryUrl, repositoryUrl,
settings.clean settings.clean
))
) {
// Delete the contents of the directory. Don't delete the directory itself
// since it may be the current working directory.
core.info(`Deleting the contents of '${settings.repositoryPath}'`)
for (const file of await fs.promises.readdir(settings.repositoryPath)) {
await io.rmRF(path.join(settings.repositoryPath, file))
}
}
// 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.`
) )
} }
if (!git) { // Remove possible previous extraheader
// Downloading using REST API await removeGitConfig(git, authConfigKey)
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 // Add extraheader (auth)
if ( const base64Credentials = Buffer.from(
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git')) `x-access-token:${settings.accessToken}`,
) { 'utf8'
await git.init() ).toString('base64')
await git.remoteAdd('origin', repositoryUrl) core.setSecret(base64Credentials)
} const authConfigValue = `AUTHORIZATION: basic ${base64Credentials}`
await git.config(authConfigKey, authConfigValue)
// Disable automatic garbage collection // LFS install
if (!(await git.tryDisableAutomaticGarbageCollection())) { if (settings.lfs) {
core.warning( await git.lfsInstall()
`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 extraheader
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)
}
}
} }
// 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()
// Set intra-task state for cleanup
coreCommand.issueCommand(
'save-state',
{name: 'repositoryPath'},
settings.repositoryPath
)
} }
export async function cleanup(repositoryPath: string): Promise<void> { export async function cleanup(repositoryPath: string): Promise<void> {
// Repo exists? // Repo exists?
if ( if (!fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))) {
!repositoryPath ||
!fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))
) {
return return
} }
fsHelper.directoryExistsSync(repositoryPath, true)
let git: IGitCommandManager // Remove the config key
try { const git = await gitCommandManager.CreateCommandManager(
git = await gitCommandManager.CreateCommandManager(repositoryPath, false) repositoryPath,
} catch { false
return )
}
// Remove extraheader
await removeGitConfig(git, authConfigKey) await removeGitConfig(git, authConfigKey)
} }
async function getGitCommandManager( async function tryPrepareExistingDirectory(
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, git: IGitCommandManager,
repositoryPath: string, repositoryPath: string,
repositoryUrl: string, repositoryUrl: string,
clean: boolean clean: boolean
): Promise<void> { ): Promise<boolean> {
let remove = false
// Check whether using git or REST API
if (!git) {
remove = true
}
// Fetch URL does not match // Fetch URL does not match
else if ( if (
!fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) || !fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) ||
repositoryUrl !== (await git.tryGetFetchUrl()) repositoryUrl !== (await git.tryGetFetchUrl())
) { ) {
remove = true return false
} 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}`)
}
}
// 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 { try {
// Checkout detached HEAD await io.rmRF(lockPath)
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) { } catch (error) {
core.warning( core.debug(`Unable to delete '${lockPath}'. ${error.message}`)
`Unable to prepare the existing repository. The repository will be recreated instead.` }
}
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)
}
} catch (error) {
core.warning(
`Unable to prepare the existing repository. The repository will be recreated instead.`
)
return false
}
// Clean
if (clean) {
let succeeded = true
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 succeeded = false
} else if (!(await git.tryReset())) {
succeeded = false
} }
}
if (remove) { if (!succeeded) {
// Delete the contents of the directory. Don't delete the directory itself core.warning(
// since it might be the current working directory. `Unable to clean or reset the repository. The repository will be recreated instead.`
core.info(`Deleting the contents of '${repositoryPath}'`) )
for (const file of await fs.promises.readdir(repositoryPath)) {
await io.rmRF(path.join(repositoryPath, file))
} }
return succeeded
} }
}
async function configureAuthToken( return true
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( async function removeGitConfig(
@@ -304,6 +230,21 @@ async function removeGitConfig(
!(await git.tryConfigUnset(configKey)) !(await git.tryConfigUnset(configKey))
) { ) {
// Load the config contents // Load the config contents
core.warning(`Failed to remove '${configKey}' from the git config`) core.warning(
`Failed to remove '${configKey}' from the git config. Attempting to remove the config value by editing the file directly.`
)
const configPath = path.join(git.getWorkingDirectory(), '.git', 'config')
fsHelper.fileExistsSync(configPath)
let contents = fs.readFileSync(configPath).toString() || ''
// Filter - only includes lines that do not contain the config key
const upperConfigKey = configKey.toUpperCase()
const split = contents
.split('\n')
.filter(x => !x.toUpperCase().includes(upperConfigKey))
contents = split.join('\n')
// Rewrite the config file
fs.writeFileSync(configPath, contents)
} }
} }

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

@@ -61,12 +61,6 @@ export function getInputs(): ISourceSettings {
if (isWorkflowRepository) { if (isWorkflowRepository) {
result.ref = github.context.ref result.ref = github.context.ref
result.commit = github.context.sha result.commit = github.context.sha
// Some events have an unqualifed ref. For example when a PR is merged (pull_request closed event),
// the ref is unqualifed like "master" instead of "refs/heads/master".
if (result.commit && result.ref && !result.ref.startsWith('refs/')) {
result.ref = `refs/heads/${result.ref}`
}
} }
if (!result.ref && !result.commit) { if (!result.ref && !result.commit) {
@@ -103,12 +97,8 @@ export function getInputs(): ISourceSettings {
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE' result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'
core.debug(`lfs = ${result.lfs}`) core.debug(`lfs = ${result.lfs}`)
// Auth token // Access token
result.authToken = core.getInput('token') result.accessToken = core.getInput('token')
// Persist credentials
result.persistCredentials =
(core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE'
return result return result
} }

View File

@@ -3,7 +3,8 @@ import * as coreCommand from '@actions/core/lib/command'
import * as gitSourceProvider from './git-source-provider' import * as gitSourceProvider from './git-source-provider'
import * as inputHelper from './input-helper' import * as inputHelper from './input-helper'
import * as path from 'path' import * as path from 'path'
import * as stateHelper from './state-helper'
const cleanupRepositoryPath = process.env['STATE_repositoryPath'] as string
async function run(): Promise<void> { async function run(): Promise<void> {
try { try {
@@ -30,14 +31,14 @@ async function run(): Promise<void> {
async function cleanup(): Promise<void> { async function cleanup(): Promise<void> {
try { try {
await gitSourceProvider.cleanup(stateHelper.RepositoryPath) await gitSourceProvider.cleanup(cleanupRepositoryPath)
} catch (error) { } catch (error) {
core.warning(error.message) core.warning(error.message)
} }
} }
// Main // Main
if (!stateHelper.IsPost) { if (!cleanupRepositoryPath) {
run() run()
} }
// Post // Post

View File

@@ -65,14 +65,9 @@ function updateUsage(
let segment: string = description let segment: string = description
if (description.length > width) { if (description.length > width) {
segment = description.substr(0, width + 1) segment = description.substr(0, width + 1)
while (!segment.endsWith(' ') && segment) { while (!segment.endsWith(' ')) {
segment = segment.substr(0, segment.length - 1) segment = segment.substr(0, segment.length - 1)
} }
// Trimmed too much?
if (segment.length < width * 0.67) {
segment = description
}
} else { } else {
segment = description segment = description
} }
@@ -101,7 +96,7 @@ function updateUsage(
} }
updateUsage( updateUsage(
'actions/checkout@v2', 'actions/checkout@v2-beta',
path.join(__dirname, '..', '..', 'action.yml'), path.join(__dirname, '..', '..', 'action.yml'),
path.join(__dirname, '..', '..', 'README.md') path.join(__dirname, '..', '..', 'README.md')
) )

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')
}