mirror of
				https://github.com/actions/checkout.git
				synced 2025-10-26 07:38:22 +08:00 
			
		
		
		
	Compare commits
	
		
			29 Commits
		
	
	
		
			v1.2.0
			...
			users/eric
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 8c9b201842 | ||
|   | f858c22e96 | ||
|   | 77904fd431 | ||
|   | 06218e4404 | ||
|   | 61fd8fd0c7 | ||
|   | f95f2a3856 | ||
|   | f90c7b395d | ||
|   | 090d9c9dfd | ||
|   | db41740e12 | ||
|   | bc50a995b8 | ||
|   | dfd70d4a2d | ||
|   | ae525b2262 | ||
|   | f466b96953 | ||
|   | c85684db76 | ||
|   | 299dd5064e | ||
|   | 722adc63f1 | ||
|   | 3537747199 | ||
|   | a6747255bd | ||
|   | c170eefc26 | ||
|   | a572f640b0 | ||
|   | cab31617d8 | ||
|   | 5881116d18 | ||
|   | 7990b10a0c | ||
|   | 01a434328a | ||
|   | 4817b449b0 | ||
|   | 689bf84be4 | ||
|   | cc70598ce8 | ||
|   | 8461dbfed3 | ||
|   | e347bba93b | 
							
								
								
									
										3
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| dist/ | ||||
| lib/ | ||||
| node_modules/ | ||||
							
								
								
									
										58
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| { | ||||
|   "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 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										177
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										177
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,19 +1,180 @@ | ||||
| name: "test-local" | ||||
| name: Build and Test | ||||
|  | ||||
| on: | ||||
|   pull_request: | ||||
|   push: | ||||
|     branches: | ||||
|       - master | ||||
|       - 'releases/*' | ||||
|       - releases/* | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/setup-node@v1 | ||||
|         with: | ||||
|           node-version: 12.x | ||||
|       - uses: actions/checkout@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: | ||||
|         os: [windows-latest, ubuntu-latest, macOS-latest] | ||||
|     runs-on: ${{ matrix.os }} | ||||
|         runs-on: [ubuntu-latest, macos-latest, windows-latest] | ||||
|     runs-on: ${{ matrix.runs-on }} | ||||
|  | ||||
|     steps: | ||||
|     - uses: actions/checkout@master | ||||
|     - uses: ./ | ||||
|       with: | ||||
|         ref: master | ||||
|       # 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 | ||||
|  | ||||
|       # 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 | ||||
|   | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| lib/ | ||||
| node_modules/ | ||||
							
								
								
									
										3
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| dist/ | ||||
| lib/ | ||||
| node_modules/ | ||||
							
								
								
									
										11
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| { | ||||
|   "printWidth": 80, | ||||
|   "tabWidth": 2, | ||||
|   "useTabs": false, | ||||
|   "semi": false, | ||||
|   "singleQuote": true, | ||||
|   "trailingComma": "none", | ||||
|   "bracketSpacing": false, | ||||
|   "arrowParens": "avoid", | ||||
|   "parser": "typescript" | ||||
| } | ||||
							
								
								
									
										26
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,13 +1,23 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## Unreleased Changes | ||||
| - N/A | ||||
| ## v2 (beta) | ||||
|  | ||||
| ## v1.2.0 | ||||
| - Reverted the breaking behavior change in v1.1.0 that broke custom authentication flows | ||||
| - 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.1.0 (Not reccomended for use, this functionality will be ported to the 2.0 update) | ||||
| - Persist `with.token` or `${{ github.token }}` into checkout repository's git config as `http.https://github.com/.extraheader=AUTHORIZATION: basic ***` to better support scripting git | ||||
|  | ||||
| ## v1.0.0 | ||||
| - Initial Release of the checkout action | ||||
| ## v1 | ||||
|  | ||||
| Refer [here](https://github.com/actions/checkout/blob/v1/CHANGELOG.md) for the V1 changelog | ||||
|   | ||||
							
								
								
									
										218
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										218
									
								
								README.md
									
									
									
									
									
								
							| @@ -2,59 +2,213 @@ | ||||
|   <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 | ||||
| # 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. | ||||
| Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. Set `fetch-depth` to fetch more history. Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events. | ||||
|  | ||||
| The auth token is persisted in the local git config. This enables your scripts to run authenticated git commands. The token is removed during post-job cleanup. Set `persist-credentials: false` to opt-out. | ||||
|  | ||||
| When Git 2.18 or higher is not in your PATH, falls back to the REST API to download the files. | ||||
|  | ||||
| # What's new | ||||
|  | ||||
| - Improved performance | ||||
|   - Fetches only a single commit by default | ||||
| - Script authenticated git commands | ||||
|   - Auth token persisted in the local git config | ||||
| - Creates a local branch | ||||
|   - No longer detached HEAD when checking out a branch | ||||
| - 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 | ||||
|   - When using a job container, the container's PATH is used | ||||
| - Removed input `submodules` | ||||
|  | ||||
| Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous versions. | ||||
|  | ||||
| # Usage | ||||
|  | ||||
| See [action.yml](action.yml) | ||||
| <!-- start usage --> | ||||
| ```yaml | ||||
| - uses: actions/checkout@v2 | ||||
|   with: | ||||
|     # Repository name with owner. For example, actions/checkout | ||||
|     # Default: ${{ github.repository }} | ||||
|     repository: '' | ||||
|  | ||||
| Basic: | ||||
|     # 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. [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 }} | ||||
|     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 --> | ||||
|  | ||||
| # 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 | ||||
|  | ||||
| ```yaml | ||||
| steps: | ||||
| - uses: actions/checkout@v1 | ||||
| - uses: actions/setup-node@v1 | ||||
| - uses: actions/checkout@v2 | ||||
|   with: | ||||
|     node-version: 10.x  | ||||
| - run: npm install | ||||
| - run: npm test | ||||
|     ref: my-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, a different repository or use different token to checkout, specify that using `with.ref`, `with.repository` and `with.token`. | ||||
| ## Checkout HEAD^ | ||||
|  | ||||
| ## Checkout different branch from the workflow repository | ||||
| ```yaml | ||||
| - uses: actions/checkout@v1 | ||||
| - uses: actions/checkout@v2 | ||||
|   with: | ||||
|     ref: some-branch | ||||
|     fetch-depth: 2 | ||||
| - run: git checkout HEAD^ | ||||
| ``` | ||||
|  | ||||
| ## Checkout different private repository | ||||
| ## Checkout multiple repos (side by side) | ||||
|  | ||||
| ```yaml | ||||
| - uses: actions/checkout@v1 | ||||
| - name: Checkout | ||||
|   uses: actions/checkout@v2 | ||||
|   with: | ||||
|     repository: myAccount/myRepository | ||||
|     ref: refs/heads/master | ||||
|     token: ${{ secrets.GitHub_PAT }} # `GitHub_PAT` is a secret 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). | ||||
|     path: main | ||||
|  | ||||
| - name: Checkout tools repo | ||||
|   uses: actions/checkout@v2 | ||||
|   with: | ||||
|     repository: my-org/my-tools | ||||
|     path: my-tools | ||||
| ``` | ||||
|  | ||||
| ## Checkout multiple repos (nested) | ||||
|  | ||||
| ## Checkout private submodules | ||||
| ```yaml | ||||
| - uses: actions/checkout@v1 | ||||
|   with: | ||||
|     submodules: true # 'recursive' 'true' or 'false' | ||||
|     token: ${{ secrets.GitHub_PAT }} # `GitHub_PAT` is a secret contains your PAT. | ||||
| ``` | ||||
| > - Private submodules must be configured via `https` not `ssh`. | ||||
| > - `${{ github.token }}` only has permission to the workflow triggering repository. If the repository contains any submodules that come from private repositories, you will need to add your [PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) as secret and use the secret in `with.token` to make the `checkout` action work. | ||||
| - name: Checkout | ||||
|   uses: actions/checkout@v2 | ||||
|  | ||||
| For more details, see [Contexts and expression syntax for GitHub Actions](https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions) and [Creating and using encrypted secrets](https://help.github.com/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) | ||||
| - 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 | ||||
|     path: my-tools | ||||
| ``` | ||||
|  | ||||
| > - `${{ github.token }}` is scoped to the current repository, so if you want to checkout a different repository that is private you will need to provide your own [PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line). | ||||
|  | ||||
|  | ||||
| ## Checkout pull request HEAD commit instead of merge commit | ||||
|  | ||||
| ```yaml | ||||
| - uses: actions/checkout@v2 | ||||
|   with: | ||||
|     ref: ${{ github.event.pull_request.head.sha }} | ||||
| ``` | ||||
|  | ||||
| ## 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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										45
									
								
								__test__/git-version.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								__test__/git-version.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| 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() | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										134
									
								
								__test__/input-helper.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								__test__/input-helper.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| 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 {ISourceSettings} from '../lib/git-source-provider' | ||||
|  | ||||
| const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE'] | ||||
| const gitHubWorkspace = path.resolve('/checkout-tests/workspace') | ||||
|  | ||||
| // Inputs for mock @actions/core | ||||
| let inputs = {} as any | ||||
|  | ||||
| // Shallow clone original @actions/github context | ||||
| let originalContext = {...github.context} | ||||
|  | ||||
| describe('input-helper tests', () => { | ||||
|   beforeAll(() => { | ||||
|     // Mock @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 | ||||
|     process.env['GITHUB_WORKSPACE'] = gitHubWorkspace | ||||
|   }) | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     // Reset inputs | ||||
|     inputs = {} | ||||
|   }) | ||||
|  | ||||
|   afterAll(() => { | ||||
|     // Restore GitHub workspace | ||||
|     delete process.env['GITHUB_WORKSPACE'] | ||||
|     if (originalGitHubWorkspace) { | ||||
|       process.env['GITHUB_WORKSPACE'] = originalGitHubWorkspace | ||||
|     } | ||||
|  | ||||
|     // Restore @actions/github context | ||||
|     github.context.ref = originalContext.ref | ||||
|     github.context.sha = originalContext.sha | ||||
|  | ||||
|     // Restore | ||||
|     jest.restoreAllMocks() | ||||
|   }) | ||||
|  | ||||
|   it('sets defaults', () => { | ||||
|     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('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', () => { | ||||
|     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/) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										10
									
								
								__test__/modify-work-tree.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										10
									
								
								__test__/modify-work-tree.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| if [ ! -f "./basic/basic-file.txt" ]; then | ||||
|     echo "Expected basic file does not exist" | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| echo hello >> ./basic/basic-file.txt | ||||
| echo hello >> ./basic/new-file.txt | ||||
| git -C ./basic status | ||||
							
								
								
									
										6
									
								
								__test__/override-git-version.cmd
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										6
									
								
								__test__/override-git-version.cmd
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,6 @@ | ||||
|  | ||||
| mkdir override-git-version | ||||
| cd override-git-version | ||||
| echo @echo override git version 1.2.3 > git.cmd | ||||
| echo ::add-path::%CD% | ||||
| cd .. | ||||
							
								
								
									
										9
									
								
								__test__/override-git-version.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								__test__/override-git-version.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| #!/bin/sh | ||||
|  | ||||
| mkdir override-git-version | ||||
| cd override-git-version | ||||
| echo "#!/bin/sh" > git | ||||
| echo "echo override git version 1.2.3" >> git | ||||
| chmod +x git | ||||
| echo "::add-path::$(pwd)" | ||||
| cd .. | ||||
							
								
								
									
										168
									
								
								__test__/ref-helper.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								__test__/ref-helper.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,168 @@ | ||||
| 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') | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										87
									
								
								__test__/retry-helper.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								__test__/retry-helper.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| import * as core from '@actions/core' | ||||
| import {RetryHelper} from '../lib/retry-helper' | ||||
|  | ||||
| let info: string[] | ||||
| let retryHelper: any | ||||
|  | ||||
| describe('retry-helper tests', () => { | ||||
|   beforeAll(() => { | ||||
|     // Mock @actions/core info() | ||||
|     jest.spyOn(core, 'info').mockImplementation((message: string) => { | ||||
|       info.push(message) | ||||
|     }) | ||||
|  | ||||
|     retryHelper = new RetryHelper(3, 0, 0) | ||||
|   }) | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     // Reset info | ||||
|     info = [] | ||||
|   }) | ||||
|  | ||||
|   afterAll(() => { | ||||
|     // Restore | ||||
|     jest.restoreAllMocks() | ||||
|   }) | ||||
|  | ||||
|   it('first attempt succeeds', async () => { | ||||
|     const actual = await retryHelper.execute(async () => { | ||||
|       return 'some result' | ||||
|     }) | ||||
|     expect(actual).toBe('some result') | ||||
|     expect(info).toHaveLength(0) | ||||
|   }) | ||||
|  | ||||
|   it('second attempt succeeds', async () => { | ||||
|     let attempts = 0 | ||||
|     const actual = await retryHelper.execute(() => { | ||||
|       if (++attempts == 1) { | ||||
|         throw new Error('some error') | ||||
|       } | ||||
|  | ||||
|       return Promise.resolve('some result') | ||||
|     }) | ||||
|     expect(attempts).toBe(2) | ||||
|     expect(actual).toBe('some result') | ||||
|     expect(info).toHaveLength(2) | ||||
|     expect(info[0]).toBe('some error') | ||||
|     expect(info[1]).toMatch(/Waiting .+ seconds before trying again/) | ||||
|   }) | ||||
|  | ||||
|   it('third attempt succeeds', async () => { | ||||
|     let attempts = 0 | ||||
|     const actual = await retryHelper.execute(() => { | ||||
|       if (++attempts < 3) { | ||||
|         throw new Error(`some error ${attempts}`) | ||||
|       } | ||||
|  | ||||
|       return Promise.resolve('some result') | ||||
|     }) | ||||
|     expect(attempts).toBe(3) | ||||
|     expect(actual).toBe('some result') | ||||
|     expect(info).toHaveLength(4) | ||||
|     expect(info[0]).toBe('some error 1') | ||||
|     expect(info[1]).toMatch(/Waiting .+ seconds before trying again/) | ||||
|     expect(info[2]).toBe('some error 2') | ||||
|     expect(info[3]).toMatch(/Waiting .+ seconds before trying again/) | ||||
|   }) | ||||
|  | ||||
|   it('all attempts fail succeeds', async () => { | ||||
|     let attempts = 0 | ||||
|     let error: Error = (null as unknown) as Error | ||||
|     try { | ||||
|       await retryHelper.execute(() => { | ||||
|         throw new Error(`some error ${++attempts}`) | ||||
|       }) | ||||
|     } catch (err) { | ||||
|       error = err | ||||
|     } | ||||
|     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/) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										24
									
								
								__test__/verify-basic.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										24
									
								
								__test__/verify-basic.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| #!/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 | ||||
							
								
								
									
										13
									
								
								__test__/verify-clean.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										13
									
								
								__test__/verify-clean.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| if [[ "$(git -C ./basic status --porcelain)" != "" ]]; then | ||||
|     echo ---------------------------------------- | ||||
|     echo git status | ||||
|     echo ---------------------------------------- | ||||
|     git status | ||||
|     echo ---------------------------------------- | ||||
|     echo git diff | ||||
|     echo ---------------------------------------- | ||||
|     git diff | ||||
|     exit 1 | ||||
| fi | ||||
							
								
								
									
										11
									
								
								__test__/verify-lfs.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										11
									
								
								__test__/verify-lfs.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| if [ ! -f "./lfs/regular-file.txt" ]; then | ||||
|     echo "Expected regular file does not exist" | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| if [ ! -f "./lfs/lfs-file.bin" ]; then | ||||
|     echo "Expected lfs file does not exist" | ||||
|     exit 1 | ||||
| fi | ||||
							
								
								
									
										17
									
								
								__test__/verify-no-unstaged-changes.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										17
									
								
								__test__/verify-no-unstaged-changes.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| if [[ "$(git status --porcelain)" != "" ]]; then | ||||
|     echo ---------------------------------------- | ||||
|     echo git status | ||||
|     echo ---------------------------------------- | ||||
|     git status | ||||
|     echo ---------------------------------------- | ||||
|     echo git diff | ||||
|     echo ---------------------------------------- | ||||
|     git diff | ||||
|     echo ---------------------------------------- | ||||
|     echo Troubleshooting | ||||
|     echo ---------------------------------------- | ||||
|     echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run all" | ||||
|     exit 1 | ||||
| fi | ||||
							
								
								
									
										11
									
								
								__test__/verify-side-by-side.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										11
									
								
								__test__/verify-side-by-side.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| if [ ! -f "./side-by-side-1/side-by-side-test-file-1.txt" ]; then | ||||
|     echo "Expected file 1 does not exist" | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| if [ ! -f "./side-by-side-2/side-by-side-test-file-2.txt" ]; then | ||||
|     echo "Expected file 2 does not exist" | ||||
|     exit 1 | ||||
| fi | ||||
							
								
								
									
										11
									
								
								__test__/verify-submodules-not-checked-out.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										11
									
								
								__test__/verify-submodules-not-checked-out.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| if [ ! -f "./submodules-not-checked-out/regular-file.txt" ]; then | ||||
|     echo "Expected regular file does not exist" | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| if [ -f "./submodules-not-checked-out/submodule-level-1/submodule-file.txt" ]; then | ||||
|     echo "Unexpected submodule file exists" | ||||
|     exit 1 | ||||
| fi | ||||
							
								
								
									
										43
									
								
								action.yml
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								action.yml
									
									
									
									
									
								
							| @@ -1,23 +1,36 @@ | ||||
| name: 'Checkout' | ||||
| description: 'Checkout a Git repository.' | ||||
| description: 'Checkout a Git repository at a particular version' | ||||
| inputs:  | ||||
|   repository: | ||||
|     description: 'Repository name' | ||||
|     description: 'Repository name with owner. For example, actions/checkout' | ||||
|     default: ${{ github.repository }} | ||||
|   ref: | ||||
|     description: 'Ref to checkout (SHA, branch, tag)' | ||||
|     description: > | ||||
|       The branch, tag or SHA to checkout. When checking out the repository that | ||||
|       triggered a workflow, this defaults to the reference or SHA for that | ||||
|       event.  Otherwise, defaults to `master`. | ||||
|   token: | ||||
|     description: 'Access token for clone repository' | ||||
|   clean: | ||||
|     description: 'If true, execute `execute git clean -ffdx && git reset --hard HEAD` before fetching' | ||||
|     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 | ||||
|   submodules: | ||||
|     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' | ||||
|   lfs: | ||||
|     description: 'Whether to download Git-LFS files; defaults to false' | ||||
|   fetch-depth: | ||||
|     description: 'The depth of commits to ask Git to fetch; defaults to no limit'   | ||||
|   path: | ||||
|     description: 'Optional path to check out source code'   | ||||
|     description: 'Relative path under $GITHUB_WORKSPACE to place the repository' | ||||
|   clean: | ||||
|     description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching' | ||||
|     default: true | ||||
|   fetch-depth: | ||||
|     description: 'Number of commits to fetch. 0 indicates all history.' | ||||
|     default: 1 | ||||
|   lfs: | ||||
|     description: 'Whether to download Git-LFS files' | ||||
|     default: false | ||||
| runs: | ||||
|   # Plugins live on the runner and are only available to a certain set of first party actions. | ||||
|   plugin: 'checkout' | ||||
|   using: node12 | ||||
|   main: dist/index.js | ||||
|   post: dist/index.js | ||||
|   | ||||
							
								
								
									
										215
									
								
								adrs/0153-checkout-v2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								adrs/0153-checkout-v2.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | ||||
| # 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) | ||||
							
								
								
									
										17216
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										17216
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										13
									
								
								dist/problem-matcher.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								dist/problem-matcher.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| { | ||||
|     "problemMatcher": [ | ||||
|         { | ||||
|             "owner": "checkout-git", | ||||
|             "pattern": [ | ||||
|                 { | ||||
|                     "regexp": "^(fatal|error): (.*)$", | ||||
|                     "message": 2 | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										11
									
								
								jest.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								jest.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| module.exports = { | ||||
|   clearMocks: true, | ||||
|   moduleFileExtensions: ['js', 'ts'], | ||||
|   testEnvironment: 'node', | ||||
|   testMatch: ['**/*.test.ts'], | ||||
|   testRunner: 'jest-circus/runner', | ||||
|   transform: { | ||||
|     '^.+\\.ts$': 'ts-jest' | ||||
|   }, | ||||
|   verbose: true | ||||
| } | ||||
							
								
								
									
										7078
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7078
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										55
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| { | ||||
|   "name": "checkout", | ||||
|   "version": "2.0.2", | ||||
|   "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.2", | ||||
|     "@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" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										77
									
								
								src/fs-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/fs-helper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| 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 | ||||
| } | ||||
							
								
								
									
										377
									
								
								src/git-command-manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										377
									
								
								src/git-command-manager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,377 @@ | ||||
| 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-full-name" because the output from | ||||
|     // "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'] | ||||
|     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) { | ||||
|         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) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     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, "branch --show-current" would be simpler but isn't available until Git 2.22 | ||||
|     const output = await this.execGit( | ||||
|       ['rev-parse', '--symbolic-full-name', '--verify', '--quiet', 'HEAD'], | ||||
|       true | ||||
|     ) | ||||
|     return !output.stdout.trim().startsWith('refs/heads/') | ||||
|   } | ||||
|  | ||||
|   async lfsFetch(ref: string): Promise<void> { | ||||
|     const args = ['lfs', 'fetch', 'origin', ref] | ||||
|  | ||||
|     const that = this | ||||
|     await retryHelper.execute(async () => { | ||||
|       await that.execGit(args) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   async lfsInstall(): Promise<void> { | ||||
|     await this.execGit(['lfs', 'install', '--local']) | ||||
|   } | ||||
|  | ||||
|   async log1(): 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 | ||||
| } | ||||
							
								
								
									
										309
									
								
								src/git-source-provider.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										309
									
								
								src/git-source-provider.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,309 @@ | ||||
| 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 serverUrl = 'https://github.com/' | ||||
| const authConfigKey = `http.${serverUrl}.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 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) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| export async function cleanup(repositoryPath: string): Promise<void> { | ||||
|   // Repo exists? | ||||
|   if ( | ||||
|     !repositoryPath || | ||||
|     !fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config')) | ||||
|   ) { | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   let git: IGitCommandManager | ||||
|   try { | ||||
|     git = await gitCommandManager.CreateCommandManager(repositoryPath, false) | ||||
|   } catch { | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   // Remove extraheader | ||||
|   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`) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										77
									
								
								src/git-version.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/git-version.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| export class GitVersion { | ||||
|   private readonly major: number = NaN | ||||
|   private readonly minor: number = NaN | ||||
|   private readonly patch: number = NaN | ||||
|  | ||||
|   /** | ||||
|    * Used for comparing the version of git and git-lfs against the minimum required version | ||||
|    * @param version the version string, e.g. 1.2 or 1.2.3 | ||||
|    */ | ||||
|   constructor(version?: string) { | ||||
|     if (version) { | ||||
|       const match = version.match(/^(\d+)\.(\d+)(\.(\d+))?$/) | ||||
|       if (match) { | ||||
|         this.major = Number(match[1]) | ||||
|         this.minor = Number(match[2]) | ||||
|         if (match[4]) { | ||||
|           this.patch = Number(match[4]) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Compares the instance against a minimum required version | ||||
|    * @param minimum Minimum version | ||||
|    */ | ||||
|   checkMinimum(minimum: GitVersion): boolean { | ||||
|     if (!minimum.isValid()) { | ||||
|       throw new Error('Arg minimum is not a valid version') | ||||
|     } | ||||
|  | ||||
|     // Major is insufficient | ||||
|     if (this.major < minimum.major) { | ||||
|       return false | ||||
|     } | ||||
|  | ||||
|     // Major is equal | ||||
|     if (this.major === minimum.major) { | ||||
|       // Minor is insufficient | ||||
|       if (this.minor < minimum.minor) { | ||||
|         return false | ||||
|       } | ||||
|  | ||||
|       // Minor is equal | ||||
|       if (this.minor === minimum.minor) { | ||||
|         // Patch is insufficient | ||||
|         if (this.patch && this.patch < (minimum.patch || 0)) { | ||||
|           return false | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Indicates whether the instance was constructed from a valid version string | ||||
|    */ | ||||
|   isValid(): boolean { | ||||
|     return !isNaN(this.major) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Returns the version as a string, e.g. 1.2 or 1.2.3 | ||||
|    */ | ||||
|   toString(): string { | ||||
|     let result = '' | ||||
|     if (this.isValid()) { | ||||
|       result = `${this.major}.${this.minor}` | ||||
|       if (!isNaN(this.patch)) { | ||||
|         result += `.${this.patch}` | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return result | ||||
|   } | ||||
| } | ||||
							
								
								
									
										92
									
								
								src/github-api-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/github-api-helper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| 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 | ||||
| } | ||||
							
								
								
									
										114
									
								
								src/input-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/input-helper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| 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 | ||||
|  | ||||
|       // 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) { | ||||
|       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 | ||||
| } | ||||
							
								
								
									
										46
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| 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() | ||||
| } | ||||
							
								
								
									
										107
									
								
								src/misc/generate-docs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/misc/generate-docs.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| 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 = segment.substr(0, segment.length - 1) | ||||
|         } | ||||
|  | ||||
|         // Trimmed too much? | ||||
|         if (segment.length < width * 0.67) { | ||||
|           segment = description | ||||
|         } | ||||
|       } 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') | ||||
| ) | ||||
							
								
								
									
										109
									
								
								src/ref-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/ref-helper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| 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}`] | ||||
|   } | ||||
| } | ||||
							
								
								
									
										61
									
								
								src/retry-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/retry-helper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| import * as core from '@actions/core' | ||||
|  | ||||
| const defaultMaxAttempts = 3 | ||||
| const defaultMinSeconds = 10 | ||||
| const defaultMaxSeconds = 20 | ||||
|  | ||||
| export class RetryHelper { | ||||
|   private maxAttempts: number | ||||
|   private minSeconds: number | ||||
|   private maxSeconds: number | ||||
|  | ||||
|   constructor( | ||||
|     maxAttempts: number = defaultMaxAttempts, | ||||
|     minSeconds: number = defaultMinSeconds, | ||||
|     maxSeconds: number = defaultMaxSeconds | ||||
|   ) { | ||||
|     this.maxAttempts = maxAttempts | ||||
|     this.minSeconds = Math.floor(minSeconds) | ||||
|     this.maxSeconds = Math.floor(maxSeconds) | ||||
|     if (this.minSeconds > this.maxSeconds) { | ||||
|       throw new Error('min seconds should be less than or equal to max seconds') | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async execute<T>(action: () => Promise<T>): Promise<T> { | ||||
|     let attempt = 1 | ||||
|     while (attempt < this.maxAttempts) { | ||||
|       // Try | ||||
|       try { | ||||
|         return await action() | ||||
|       } catch (err) { | ||||
|         core.info(err.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) | ||||
| } | ||||
							
								
								
									
										30
									
								
								src/state-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/state-helper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| 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') | ||||
| } | ||||
							
								
								
									
										16
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "target": "es6", | ||||
|     "module": "commonjs", | ||||
|     "lib": [ | ||||
|       "es6" | ||||
|     ], | ||||
|     "outDir": "./lib", | ||||
|     "rootDir": "./src", | ||||
|     "declaration": true, | ||||
|     "strict": true, | ||||
|     "noImplicitAny": false, | ||||
|     "esModuleInterop": true | ||||
|   }, | ||||
|   "exclude": ["__test__", "lib", "node_modules"] | ||||
| } | ||||
		Reference in New Issue
	
	Block a user