├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── check-dist.yml │ ├── codeql-analysis.yml │ ├── licensed.yml │ ├── publish-immutable-actions.yml │ ├── test.yml │ ├── update-main-version.yml │ └── update-test-ubuntu-git.yml ├── .gitignore ├── .licensed.yml ├── .licenses └── npm │ ├── @actions │ ├── core.dep.yml │ ├── exec.dep.yml │ ├── github.dep.yml │ ├── http-client.dep.yml │ ├── io.dep.yml │ └── tool-cache.dep.yml │ ├── @fastify │ └── busboy.dep.yml │ ├── @octokit │ ├── auth-token.dep.yml │ ├── core.dep.yml │ ├── endpoint.dep.yml │ ├── graphql.dep.yml │ ├── openapi-types-20.0.0.dep.yml │ ├── openapi-types-22.1.0.dep.yml │ ├── plugin-paginate-rest.dep.yml │ ├── plugin-rest-endpoint-methods.dep.yml │ ├── request-error.dep.yml │ ├── request.dep.yml │ ├── types-12.6.0.dep.yml │ └── types-13.4.1.dep.yml │ ├── before-after-hook.dep.yml │ ├── deprecation.dep.yml │ ├── once.dep.yml │ ├── semver.dep.yml │ ├── tunnel.dep.yml │ ├── undici.dep.yml │ ├── universal-user-agent.dep.yml │ ├── uuid-3.4.0.dep.yml │ ├── uuid-8.3.2.dep.yml │ ├── uuid-9.0.1.dep.yml │ └── wrappy.dep.yml ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __test__ ├── git-auth-helper.test.ts ├── git-command-manager.test.ts ├── git-directory-helper.test.ts ├── git-version.test.ts ├── input-helper.test.ts ├── modify-work-tree.sh ├── override-git-version.cmd ├── override-git-version.sh ├── ref-helper.test.ts ├── retry-helper.test.ts ├── url-helper.test.ts ├── verify-basic.sh ├── verify-clean.sh ├── verify-fetch-filter.sh ├── verify-lfs.sh ├── verify-no-unstaged-changes.sh ├── verify-side-by-side.sh ├── verify-sparse-checkout-non-cone-mode.sh ├── verify-sparse-checkout.sh ├── verify-submodules-false.sh ├── verify-submodules-recursive.sh └── verify-submodules-true.sh ├── action.yml ├── adrs └── 0153-checkout-v2.md ├── dist ├── index.js └── problem-matcher.json ├── images ├── test-ubuntu-git.Dockerfile └── test-ubuntu-git.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── fs-helper.ts ├── git-auth-helper.ts ├── git-command-manager.ts ├── git-directory-helper.ts ├── git-source-provider.ts ├── git-source-settings.ts ├── git-version.ts ├── github-api-helper.ts ├── input-helper.ts ├── main.ts ├── misc │ ├── generate-docs.ts │ ├── licensed-check.sh │ ├── licensed-download.sh │ └── licensed-generate.sh ├── ref-helper.ts ├── regexp-helper.ts ├── retry-helper.ts ├── state-helper.ts ├── url-helper.ts └── workflow-context-helper.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest", "@typescript-eslint"], 3 | "extends": ["plugin:github/recommended"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "rules": { 11 | "eslint-comments/no-use": "off", 12 | "import/no-namespace": "off", 13 | "no-unused-vars": "off", 14 | "@typescript-eslint/no-unused-vars": "error", 15 | "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], 16 | "@typescript-eslint/no-require-imports": "error", 17 | "@typescript-eslint/array-type": "error", 18 | "@typescript-eslint/await-thenable": "error", 19 | "camelcase": "off", 20 | "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], 21 | "@typescript-eslint/func-call-spacing": ["error", "never"], 22 | "@typescript-eslint/no-array-constructor": "error", 23 | "@typescript-eslint/no-empty-interface": "error", 24 | "@typescript-eslint/no-explicit-any": "error", 25 | "@typescript-eslint/no-extraneous-class": "error", 26 | "@typescript-eslint/no-floating-promises": "error", 27 | "@typescript-eslint/no-for-in-array": "error", 28 | "@typescript-eslint/no-inferrable-types": "error", 29 | "@typescript-eslint/no-misused-new": "error", 30 | "@typescript-eslint/no-namespace": "error", 31 | "@typescript-eslint/no-non-null-assertion": "warn", 32 | "@typescript-eslint/no-unnecessary-qualifier": "error", 33 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 34 | "@typescript-eslint/no-useless-constructor": "error", 35 | "@typescript-eslint/no-var-requires": "error", 36 | "@typescript-eslint/prefer-for-of": "warn", 37 | "@typescript-eslint/prefer-function-type": "warn", 38 | "@typescript-eslint/prefer-includes": "error", 39 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 40 | "@typescript-eslint/promise-function-async": "error", 41 | "@typescript-eslint/require-array-sort-compare": "error", 42 | "@typescript-eslint/restrict-plus-operands": "error", 43 | "semi": "off", 44 | "@typescript-eslint/semi": ["error", "never"], 45 | "@typescript-eslint/type-annotation-spacing": "error", 46 | "@typescript-eslint/unbound-method": "error" 47 | }, 48 | "env": { 49 | "node": true, 50 | "es6": true, 51 | "jest/globals": true 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .licenses/** -diff linguist-generated=true -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: "npm" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | groups: 10 | minor-npm-dependencies: 11 | # NPM: Only group minor and patch updates (we want to carefully review major updates) 12 | update-types: [minor, patch] 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | groups: 18 | minor-actions-dependencies: 19 | # GitHub Actions: Only group minor and patch updates (we want to carefully review major updates) 20 | update-types: [minor, patch] 21 | -------------------------------------------------------------------------------- /.github/workflows/check-dist.yml: -------------------------------------------------------------------------------- 1 | # `dist/index.js` is a special file in Actions. 2 | # When you reference an action with `uses:` in a workflow, 3 | # `index.js` is the code that will run. 4 | # For our project, we generate this file through a build process 5 | # from other source files. 6 | # We need to make sure the checked-in `index.js` actually matches what we expect it to be. 7 | name: Check dist 8 | 9 | on: 10 | push: 11 | branches: 12 | - main 13 | paths-ignore: 14 | - '**.md' 15 | pull_request: 16 | paths-ignore: 17 | - '**.md' 18 | workflow_dispatch: 19 | 20 | jobs: 21 | check-dist: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4.1.6 26 | 27 | - name: Set Node.js 20.x 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: 20.x 31 | 32 | - name: Install dependencies 33 | run: npm ci 34 | 35 | - name: Rebuild the index.js file 36 | run: npm run build 37 | 38 | - name: Compare the expected and actual dist/ directories 39 | run: | 40 | if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then 41 | echo "Detected uncommitted changes after build. See status below:" 42 | git diff 43 | exit 1 44 | fi 45 | 46 | # If dist/ was different than expected, upload the expected version as an artifact 47 | - uses: actions/upload-artifact@v4 48 | if: ${{ failure() && steps.diff.conclusion == 'failure' }} 49 | with: 50 | name: dist 51 | path: dist/ 52 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '28 9 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4.1.6 43 | 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | - run: npm ci 54 | - run: npm run build 55 | - run: rm -rf dist # We want code scanning to analyze lib instead (individual .js files) 56 | 57 | - name: Perform CodeQL Analysis 58 | uses: github/codeql-action/analyze@v3 59 | -------------------------------------------------------------------------------- /.github/workflows/licensed.yml: -------------------------------------------------------------------------------- 1 | name: Licensed 2 | 3 | on: 4 | push: {branches: main} 5 | pull_request: {branches: main} 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | name: Check licenses 11 | steps: 12 | - uses: actions/checkout@v4.1.6 13 | - run: npm ci 14 | - run: npm run licensed-check -------------------------------------------------------------------------------- /.github/workflows/publish-immutable-actions.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish Immutable Action Version' 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | id-token: write 13 | packages: write 14 | 15 | steps: 16 | - name: Checking out 17 | uses: actions/checkout@v4 18 | - name: Publish 19 | id: publish 20 | uses: actions/publish-immutable-action@0.0.3 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | - releases/* 9 | 10 | 11 | # Note that when you see patterns like "ref: test-data/v2/basic" within this workflow, 12 | # these refer to "test-data" branches on this actions/checkout repo. 13 | # (For example, test-data/v2/basic -> https://github.com/actions/checkout/tree/test-data/v2/basic) 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 20.x 22 | - uses: actions/checkout@v4.1.6 23 | - run: npm ci 24 | - run: npm run build 25 | - run: npm run format-check 26 | - run: npm run lint 27 | - run: npm test 28 | - name: Verify no unstaged changes 29 | run: __test__/verify-no-unstaged-changes.sh 30 | 31 | test: 32 | strategy: 33 | matrix: 34 | runs-on: [ubuntu-latest, macos-latest, windows-latest] 35 | runs-on: ${{ matrix.runs-on }} 36 | 37 | steps: 38 | # Clone this repo 39 | - name: Checkout 40 | uses: actions/checkout@v4.1.6 41 | 42 | # Basic checkout 43 | - name: Checkout basic 44 | uses: ./ 45 | with: 46 | ref: test-data/v2/basic 47 | path: basic 48 | - name: Verify basic 49 | shell: bash 50 | run: __test__/verify-basic.sh 51 | 52 | # Clean 53 | - name: Modify work tree 54 | shell: bash 55 | run: __test__/modify-work-tree.sh 56 | - name: Checkout clean 57 | uses: ./ 58 | with: 59 | ref: test-data/v2/basic 60 | path: basic 61 | - name: Verify clean 62 | shell: bash 63 | run: __test__/verify-clean.sh 64 | 65 | # Side by side 66 | - name: Checkout side by side 1 67 | uses: ./ 68 | with: 69 | ref: test-data/v2/side-by-side-1 70 | path: side-by-side-1 71 | - name: Checkout side by side 2 72 | uses: ./ 73 | with: 74 | ref: test-data/v2/side-by-side-2 75 | path: side-by-side-2 76 | - name: Verify side by side 77 | shell: bash 78 | run: __test__/verify-side-by-side.sh 79 | 80 | # Filter 81 | - name: Fetch filter 82 | uses: ./ 83 | with: 84 | filter: 'blob:none' 85 | path: fetch-filter 86 | 87 | - name: Verify fetch filter 88 | run: __test__/verify-fetch-filter.sh 89 | 90 | # Sparse checkout 91 | - name: Sparse checkout 92 | uses: ./ 93 | with: 94 | sparse-checkout: | 95 | __test__ 96 | .github 97 | dist 98 | path: sparse-checkout 99 | 100 | - name: Verify sparse checkout 101 | run: __test__/verify-sparse-checkout.sh 102 | 103 | # Disabled sparse checkout in existing checkout 104 | - name: Disabled sparse checkout 105 | uses: ./ 106 | with: 107 | path: sparse-checkout 108 | 109 | - name: Verify disabled sparse checkout 110 | shell: bash 111 | run: set -x && ls -l sparse-checkout/src/git-command-manager.ts 112 | 113 | # Sparse checkout (non-cone mode) 114 | - name: Sparse checkout (non-cone mode) 115 | uses: ./ 116 | with: 117 | sparse-checkout: | 118 | /__test__/ 119 | /.github/ 120 | /dist/ 121 | sparse-checkout-cone-mode: false 122 | path: sparse-checkout-non-cone-mode 123 | 124 | - name: Verify sparse checkout (non-cone mode) 125 | run: __test__/verify-sparse-checkout-non-cone-mode.sh 126 | 127 | # LFS 128 | - name: Checkout LFS 129 | uses: ./ 130 | with: 131 | repository: actions/checkout # hardcoded, otherwise doesn't work from a fork 132 | ref: test-data/v2/lfs 133 | path: lfs 134 | lfs: true 135 | - name: Verify LFS 136 | shell: bash 137 | run: __test__/verify-lfs.sh 138 | 139 | # Submodules false 140 | - name: Checkout submodules false 141 | uses: ./ 142 | with: 143 | ref: test-data/v2/submodule-ssh-url 144 | path: submodules-false 145 | - name: Verify submodules false 146 | run: __test__/verify-submodules-false.sh 147 | 148 | # Submodules one level 149 | - name: Checkout submodules true 150 | uses: ./ 151 | with: 152 | ref: test-data/v2/submodule-ssh-url 153 | path: submodules-true 154 | submodules: true 155 | - name: Verify submodules true 156 | run: __test__/verify-submodules-true.sh 157 | 158 | # Submodules recursive 159 | - name: Checkout submodules recursive 160 | uses: ./ 161 | with: 162 | ref: test-data/v2/submodule-ssh-url 163 | path: submodules-recursive 164 | submodules: recursive 165 | - name: Verify submodules recursive 166 | run: __test__/verify-submodules-recursive.sh 167 | 168 | # Basic checkout using REST API 169 | - name: Remove basic 170 | if: runner.os != 'windows' 171 | run: rm -rf basic 172 | - name: Remove basic (Windows) 173 | if: runner.os == 'windows' 174 | shell: cmd 175 | run: rmdir /s /q basic 176 | - name: Override git version 177 | if: runner.os != 'windows' 178 | run: __test__/override-git-version.sh 179 | - name: Override git version (Windows) 180 | if: runner.os == 'windows' 181 | run: __test__\\override-git-version.cmd 182 | - name: Checkout basic using REST API 183 | uses: ./ 184 | with: 185 | ref: test-data/v2/basic 186 | path: basic 187 | - name: Verify basic 188 | run: __test__/verify-basic.sh --archive 189 | 190 | test-proxy: 191 | runs-on: ubuntu-latest 192 | container: 193 | image: ghcr.io/actions/test-ubuntu-git:main.20240221.114913.703z 194 | options: --dns 127.0.0.1 195 | services: 196 | squid-proxy: 197 | image: ubuntu/squid:latest 198 | ports: 199 | - 3128:3128 200 | env: 201 | https_proxy: http://squid-proxy:3128 202 | steps: 203 | # Clone this repo 204 | - name: Checkout 205 | uses: actions/checkout@v4.1.6 206 | 207 | # Basic checkout using git 208 | - name: Checkout basic 209 | uses: ./ 210 | with: 211 | ref: test-data/v2/basic 212 | path: basic 213 | - name: Verify basic 214 | run: __test__/verify-basic.sh 215 | 216 | # Basic checkout using REST API 217 | - name: Remove basic 218 | run: rm -rf basic 219 | - name: Override git version 220 | run: __test__/override-git-version.sh 221 | - name: Basic checkout using REST API 222 | uses: ./ 223 | with: 224 | ref: test-data/v2/basic 225 | path: basic 226 | - name: Verify basic 227 | run: __test__/verify-basic.sh --archive 228 | 229 | test-bypass-proxy: 230 | runs-on: ubuntu-latest 231 | env: 232 | https_proxy: http://no-such-proxy:3128 233 | no_proxy: api.github.com,github.com 234 | steps: 235 | # Clone this repo 236 | - name: Checkout 237 | uses: actions/checkout@v4.1.6 238 | 239 | # Basic checkout using git 240 | - name: Checkout basic 241 | uses: ./ 242 | with: 243 | ref: test-data/v2/basic 244 | path: basic 245 | - name: Verify basic 246 | run: __test__/verify-basic.sh 247 | - name: Remove basic 248 | run: rm -rf basic 249 | 250 | # Basic checkout using REST API 251 | - name: Override git version 252 | run: __test__/override-git-version.sh 253 | - name: Checkout basic using REST API 254 | uses: ./ 255 | with: 256 | ref: test-data/v2/basic 257 | path: basic 258 | - name: Verify basic 259 | run: __test__/verify-basic.sh --archive 260 | 261 | test-git-container: 262 | runs-on: ubuntu-latest 263 | container: bitnami/git:latest 264 | steps: 265 | # Clone this repo 266 | - name: Checkout 267 | uses: actions/checkout@v4.1.6 268 | with: 269 | path: localClone 270 | 271 | # Basic checkout using git 272 | - name: Checkout basic 273 | uses: ./localClone 274 | with: 275 | ref: test-data/v2/basic 276 | - name: Verify basic 277 | run: | 278 | if [ ! -f "./basic-file.txt" ]; then 279 | echo "Expected basic file does not exist" 280 | exit 1 281 | fi 282 | 283 | # Verify .git folder 284 | if [ ! -d "./.git" ]; then 285 | echo "Expected ./.git folder to exist" 286 | exit 1 287 | fi 288 | 289 | # Verify auth token 290 | git config --global --add safe.directory "*" 291 | git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main 292 | 293 | # needed to make checkout post cleanup succeed 294 | - name: Fix Checkout v4 295 | uses: actions/checkout@v4.1.6 296 | with: 297 | path: localClone 298 | 299 | test-output: 300 | runs-on: ubuntu-latest 301 | steps: 302 | # Clone this repo 303 | - name: Checkout 304 | uses: actions/checkout@v4.1.6 305 | 306 | # Basic checkout using git 307 | - name: Checkout basic 308 | id: checkout 309 | uses: ./ 310 | with: 311 | ref: test-data/v2/basic 312 | 313 | # Verify output 314 | - name: Verify output 315 | run: | 316 | echo "Commit: ${{ steps.checkout.outputs.commit }}" 317 | echo "Ref: ${{ steps.checkout.outputs.ref }}" 318 | 319 | if [ "${{ steps.checkout.outputs.ref }}" != "test-data/v2/basic" ]; then 320 | echo "Expected ref to be test-data/v2/basic" 321 | exit 1 322 | fi 323 | 324 | if [ "${{ steps.checkout.outputs.commit }}" != "82f71901cf8c021332310dcc8cdba84c4193ff5d" ]; then 325 | echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d" 326 | exit 1 327 | fi 328 | 329 | # needed to make checkout post cleanup succeed 330 | - name: Fix Checkout 331 | uses: actions/checkout@v4.1.6 332 | -------------------------------------------------------------------------------- /.github/workflows/update-main-version.yml: -------------------------------------------------------------------------------- 1 | name: Update Main Version 2 | run-name: Move ${{ github.event.inputs.major_version }} to ${{ github.event.inputs.target }} 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | target: 8 | description: The tag or reference to use 9 | required: true 10 | major_version: 11 | type: choice 12 | description: The major version to update 13 | options: 14 | - v4 15 | - v3 16 | - v2 17 | 18 | jobs: 19 | tag: 20 | runs-on: ubuntu-latest 21 | steps: 22 | # Note this update workflow can also be used as a rollback tool. 23 | # For that reason, it's best to pin `actions/checkout` to a known, stable version 24 | # (typically, about two releases back). 25 | - uses: actions/checkout@v4.1.6 26 | with: 27 | fetch-depth: 0 28 | - name: Git config 29 | run: | 30 | git config user.name "github-actions[bot]" 31 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 32 | - name: Tag new target 33 | run: git tag -f ${{ github.event.inputs.major_version }} ${{ github.event.inputs.target }} 34 | - name: Push new tag 35 | run: git push origin ${{ github.event.inputs.major_version }} --force 36 | -------------------------------------------------------------------------------- /.github/workflows/update-test-ubuntu-git.yml: -------------------------------------------------------------------------------- 1 | name: Publish test-ubuntu-git Container 2 | 3 | on: 4 | # Use an on demand workflow trigger. 5 | # (Forked copies of actions/checkout won't have permission to update GHCR.io/actions, 6 | # so avoid trigger events that run automatically.) 7 | workflow_dispatch: 8 | inputs: 9 | publish: 10 | description: 'Publish to ghcr.io? (main branch only)' 11 | type: boolean 12 | required: true 13 | default: false 14 | 15 | env: 16 | REGISTRY: ghcr.io 17 | IMAGE_NAME: actions/test-ubuntu-git 18 | 19 | jobs: 20 | build-and-push-image: 21 | runs-on: ubuntu-latest 22 | # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. 23 | permissions: 24 | contents: read 25 | packages: write 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | 31 | # Use `docker/login-action` to log in to GHCR.io. 32 | # Once published, the packages are scoped to the account defined here. 33 | - name: Log in to the ghcr.io container registry 34 | uses: docker/login-action@v3.3.0 35 | with: 36 | registry: ${{ env.REGISTRY }} 37 | username: ${{ github.actor }} 38 | password: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | - name: Format Timestamp 41 | id: timestamp 42 | # Use `date` with a custom format to achieve the key=value format GITHUB_OUTPUT expects. 43 | run: date -u "+now=%Y%m%d.%H%M%S.%3NZ" >> "$GITHUB_OUTPUT" 44 | 45 | - name: Issue Image Publish Warning 46 | if: ${{ inputs.publish && github.ref_name != 'main' }} 47 | run: echo "::warning::test-ubuntu-git images can only be published from the actions/checkout 'main' branch. Workflow will continue with push/publish disabled." 48 | 49 | # Use `docker/build-push-action` to build (and optionally publish) the image. 50 | - name: Build Docker Image (with optional Push) 51 | uses: docker/build-push-action@v6.5.0 52 | with: 53 | context: . 54 | file: images/test-ubuntu-git.Dockerfile 55 | # For now, attempts to push to ghcr.io must target the `main` branch. 56 | # In the future, consider also allowing attempts from `releases/*` branches. 57 | push: ${{ inputs.publish && github.ref_name == 'main' }} 58 | tags: | 59 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}.${{ steps.timestamp.outputs.now }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __test__/_temp 2 | _temp/ 3 | lib/ 4 | node_modules/ 5 | .vscode/ -------------------------------------------------------------------------------- /.licensed.yml: -------------------------------------------------------------------------------- 1 | sources: 2 | npm: true 3 | 4 | allowed: 5 | - apache-2.0 6 | - bsd-2-clause 7 | - bsd-3-clause 8 | - isc 9 | - mit 10 | - cc0-1.0 11 | - unlicense 12 | 13 | reviewed: 14 | npm: -------------------------------------------------------------------------------- /.licenses/npm/@actions/core.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@actions/core" 3 | version: 1.10.1 4 | type: npm 5 | summary: Actions core lib 6 | homepage: https://github.com/actions/toolkit/tree/main/packages/core 7 | license: mit 8 | licenses: 9 | - sources: LICENSE.md 10 | text: |- 11 | The MIT License (MIT) 12 | 13 | Copyright 2019 GitHub 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/@actions/exec.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@actions/exec" 3 | version: 1.1.1 4 | type: npm 5 | summary: Actions exec lib 6 | homepage: https://github.com/actions/toolkit/tree/main/packages/exec 7 | license: mit 8 | licenses: 9 | - sources: LICENSE.md 10 | text: |- 11 | The MIT License (MIT) 12 | 13 | Copyright 2019 GitHub 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/@actions/github.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@actions/github" 3 | version: 6.0.0 4 | type: npm 5 | summary: Actions github lib 6 | homepage: https://github.com/actions/toolkit/tree/main/packages/github 7 | license: mit 8 | licenses: 9 | - sources: LICENSE.md 10 | text: |- 11 | The MIT License (MIT) 12 | 13 | Copyright 2019 GitHub 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/@actions/http-client.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@actions/http-client" 3 | version: 2.2.1 4 | type: npm 5 | summary: Actions Http Client 6 | homepage: https://github.com/actions/toolkit/tree/main/packages/http-client 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | Actions Http Client for Node.js 12 | 13 | Copyright (c) GitHub, Inc. 14 | 15 | All rights reserved. 16 | 17 | MIT License 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 20 | associated documentation files (the "Software"), to deal in the Software without restriction, 21 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 22 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 23 | subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 28 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 29 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 30 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 31 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | notices: [] 33 | -------------------------------------------------------------------------------- /.licenses/npm/@actions/io.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@actions/io" 3 | version: 1.1.3 4 | type: npm 5 | summary: Actions io lib 6 | homepage: https://github.com/actions/toolkit/tree/main/packages/io 7 | license: mit 8 | licenses: 9 | - sources: LICENSE.md 10 | text: |- 11 | The MIT License (MIT) 12 | 13 | Copyright 2019 GitHub 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/@actions/tool-cache.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@actions/tool-cache" 3 | version: 2.0.1 4 | type: npm 5 | summary: Actions tool-cache lib 6 | homepage: https://github.com/actions/toolkit/tree/main/packages/tool-cache 7 | license: mit 8 | licenses: 9 | - sources: LICENSE.md 10 | text: |- 11 | The MIT License (MIT) 12 | 13 | Copyright 2019 GitHub 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/@fastify/busboy.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@fastify/busboy" 3 | version: 2.1.1 4 | type: npm 5 | summary: A streaming parser for HTML form data for node.js 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: |- 11 | Copyright Brian White. All rights reserved. 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to 15 | deal in the Software without restriction, including without limitation the 16 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 17 | sell copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in 21 | all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 28 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 29 | IN THE SOFTWARE. 30 | notices: [] 31 | -------------------------------------------------------------------------------- /.licenses/npm/@octokit/auth-token.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@octokit/auth-token" 3 | version: 4.0.0 4 | type: npm 5 | summary: GitHub API token authentication for browsers and Node.js 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | The MIT License 12 | 13 | Copyright (c) 2019 Octokit contributors 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | - sources: README.md 33 | text: "[MIT](LICENSE)" 34 | notices: [] 35 | -------------------------------------------------------------------------------- /.licenses/npm/@octokit/core.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@octokit/core" 3 | version: 5.2.0 4 | type: npm 5 | summary: Extendable client for GitHub's REST & GraphQL APIs 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | The MIT License 12 | 13 | Copyright (c) 2019 Octokit contributors 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | - sources: README.md 33 | text: "[MIT](LICENSE)" 34 | notices: [] 35 | -------------------------------------------------------------------------------- /.licenses/npm/@octokit/endpoint.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@octokit/endpoint" 3 | version: 9.0.5 4 | type: npm 5 | summary: Turns REST API endpoints into generic request options 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | The MIT License 12 | 13 | Copyright (c) 2018 Octokit contributors 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | - sources: README.md 33 | text: "[MIT](LICENSE)" 34 | notices: [] 35 | -------------------------------------------------------------------------------- /.licenses/npm/@octokit/graphql.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@octokit/graphql" 3 | version: 7.1.0 4 | type: npm 5 | summary: GitHub GraphQL API client for browsers and Node 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | The MIT License 12 | 13 | Copyright (c) 2018 Octokit contributors 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | - sources: README.md 33 | text: "[MIT](LICENSE)" 34 | notices: [] 35 | -------------------------------------------------------------------------------- /.licenses/npm/@octokit/openapi-types-20.0.0.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@octokit/openapi-types" 3 | version: 20.0.0 4 | type: npm 5 | summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: |- 11 | Copyright 2020 Gregor Martynus 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | - sources: README.md 19 | text: "[MIT](LICENSE)" 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/@octokit/openapi-types-22.1.0.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@octokit/openapi-types" 3 | version: 22.1.0 4 | type: npm 5 | summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: |- 11 | Copyright 2020 Gregor Martynus 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | - sources: README.md 19 | text: "[MIT](LICENSE)" 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/@octokit/plugin-paginate-rest.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@octokit/plugin-paginate-rest" 3 | version: 9.2.1 4 | type: npm 5 | summary: Octokit plugin to paginate REST API endpoint responses 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | MIT License Copyright (c) 2019 Octokit contributors 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | - sources: README.md 19 | text: "[MIT](LICENSE)" 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/@octokit/plugin-rest-endpoint-methods.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@octokit/plugin-rest-endpoint-methods" 3 | version: 10.4.1 4 | type: npm 5 | summary: Octokit plugin adding one method for all of api.github.com REST API endpoints 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | MIT License Copyright (c) 2019 Octokit contributors 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | - sources: README.md 19 | text: "[MIT](LICENSE)" 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/@octokit/request-error.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@octokit/request-error" 3 | version: 5.1.0 4 | type: npm 5 | summary: Error class for Octokit request errors 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | The MIT License 12 | 13 | Copyright (c) 2019 Octokit contributors 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | - sources: README.md 33 | text: "[MIT](LICENSE)" 34 | notices: [] 35 | -------------------------------------------------------------------------------- /.licenses/npm/@octokit/request.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@octokit/request" 3 | version: 8.4.0 4 | type: npm 5 | summary: Send parameterized requests to GitHub's APIs with sensible defaults in browsers 6 | and Node 7 | homepage: 8 | license: mit 9 | licenses: 10 | - sources: LICENSE 11 | text: | 12 | The MIT License 13 | 14 | Copyright (c) 2018 Octokit contributors 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in 24 | all copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 32 | THE SOFTWARE. 33 | - sources: README.md 34 | text: "[MIT](LICENSE)" 35 | notices: [] 36 | -------------------------------------------------------------------------------- /.licenses/npm/@octokit/types-12.6.0.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@octokit/types" 3 | version: 12.6.0 4 | type: npm 5 | summary: Shared TypeScript definitions for Octokit projects 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | MIT License Copyright (c) 2019 Octokit contributors 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | - sources: README.md 19 | text: "[MIT](LICENSE)" 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/@octokit/types-13.4.1.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@octokit/types" 3 | version: 13.4.1 4 | type: npm 5 | summary: Shared TypeScript definitions for Octokit projects 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | MIT License Copyright (c) 2019 Octokit contributors 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | - sources: README.md 19 | text: "[MIT](LICENSE)" 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/before-after-hook.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: before-after-hook 3 | version: 2.2.3 4 | type: npm 5 | summary: asynchronous before/error/after hooks for internal functionality 6 | homepage: 7 | license: apache-2.0 8 | licenses: 9 | - sources: LICENSE 10 | text: |2 11 | Apache License 12 | Version 2.0, January 2004 13 | http://www.apache.org/licenses/ 14 | 15 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 16 | 17 | 1. Definitions. 18 | 19 | "License" shall mean the terms and conditions for use, reproduction, 20 | and distribution as defined by Sections 1 through 9 of this document. 21 | 22 | "Licensor" shall mean the copyright owner or entity authorized by 23 | the copyright owner that is granting the License. 24 | 25 | "Legal Entity" shall mean the union of the acting entity and all 26 | other entities that control, are controlled by, or are under common 27 | control with that entity. For the purposes of this definition, 28 | "control" means (i) the power, direct or indirect, to cause the 29 | direction or management of such entity, whether by contract or 30 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 31 | outstanding shares, or (iii) beneficial ownership of such entity. 32 | 33 | "You" (or "Your") shall mean an individual or Legal Entity 34 | exercising permissions granted by this License. 35 | 36 | "Source" form shall mean the preferred form for making modifications, 37 | including but not limited to software source code, documentation 38 | source, and configuration files. 39 | 40 | "Object" form shall mean any form resulting from mechanical 41 | transformation or translation of a Source form, including but 42 | not limited to compiled object code, generated documentation, 43 | and conversions to other media types. 44 | 45 | "Work" shall mean the work of authorship, whether in Source or 46 | Object form, made available under the License, as indicated by a 47 | copyright notice that is included in or attached to the work 48 | (an example is provided in the Appendix below). 49 | 50 | "Derivative Works" shall mean any work, whether in Source or Object 51 | form, that is based on (or derived from) the Work and for which the 52 | editorial revisions, annotations, elaborations, or other modifications 53 | represent, as a whole, an original work of authorship. For the purposes 54 | of this License, Derivative Works shall not include works that remain 55 | separable from, or merely link (or bind by name) to the interfaces of, 56 | the Work and Derivative Works thereof. 57 | 58 | "Contribution" shall mean any work of authorship, including 59 | the original version of the Work and any modifications or additions 60 | to that Work or Derivative Works thereof, that is intentionally 61 | submitted to Licensor for inclusion in the Work by the copyright owner 62 | or by an individual or Legal Entity authorized to submit on behalf of 63 | the copyright owner. For the purposes of this definition, "submitted" 64 | means any form of electronic, verbal, or written communication sent 65 | to the Licensor or its representatives, including but not limited to 66 | communication on electronic mailing lists, source code control systems, 67 | and issue tracking systems that are managed by, or on behalf of, the 68 | Licensor for the purpose of discussing and improving the Work, but 69 | excluding communication that is conspicuously marked or otherwise 70 | designated in writing by the copyright owner as "Not a Contribution." 71 | 72 | "Contributor" shall mean Licensor and any individual or Legal Entity 73 | on behalf of whom a Contribution has been received by Licensor and 74 | subsequently incorporated within the Work. 75 | 76 | 2. Grant of Copyright License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | copyright license to reproduce, prepare Derivative Works of, 80 | publicly display, publicly perform, sublicense, and distribute the 81 | Work and such Derivative Works in Source or Object form. 82 | 83 | 3. Grant of Patent License. Subject to the terms and conditions of 84 | this License, each Contributor hereby grants to You a perpetual, 85 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 86 | (except as stated in this section) patent license to make, have made, 87 | use, offer to sell, sell, import, and otherwise transfer the Work, 88 | where such license applies only to those patent claims licensable 89 | by such Contributor that are necessarily infringed by their 90 | Contribution(s) alone or by combination of their Contribution(s) 91 | with the Work to which such Contribution(s) was submitted. If You 92 | institute patent litigation against any entity (including a 93 | cross-claim or counterclaim in a lawsuit) alleging that the Work 94 | or a Contribution incorporated within the Work constitutes direct 95 | or contributory patent infringement, then any patent licenses 96 | granted to You under this License for that Work shall terminate 97 | as of the date such litigation is filed. 98 | 99 | 4. Redistribution. You may reproduce and distribute copies of the 100 | Work or Derivative Works thereof in any medium, with or without 101 | modifications, and in Source or Object form, provided that You 102 | meet the following conditions: 103 | 104 | (a) You must give any other recipients of the Work or 105 | Derivative Works a copy of this License; and 106 | 107 | (b) You must cause any modified files to carry prominent notices 108 | stating that You changed the files; and 109 | 110 | (c) You must retain, in the Source form of any Derivative Works 111 | that You distribute, all copyright, patent, trademark, and 112 | attribution notices from the Source form of the Work, 113 | excluding those notices that do not pertain to any part of 114 | the Derivative Works; and 115 | 116 | (d) If the Work includes a "NOTICE" text file as part of its 117 | distribution, then any Derivative Works that You distribute must 118 | include a readable copy of the attribution notices contained 119 | within such NOTICE file, excluding those notices that do not 120 | pertain to any part of the Derivative Works, in at least one 121 | of the following places: within a NOTICE text file distributed 122 | as part of the Derivative Works; within the Source form or 123 | documentation, if provided along with the Derivative Works; or, 124 | within a display generated by the Derivative Works, if and 125 | wherever such third-party notices normally appear. The contents 126 | of the NOTICE file are for informational purposes only and 127 | do not modify the License. You may add Your own attribution 128 | notices within Derivative Works that You distribute, alongside 129 | or as an addendum to the NOTICE text from the Work, provided 130 | that such additional attribution notices cannot be construed 131 | as modifying the License. 132 | 133 | You may add Your own copyright statement to Your modifications and 134 | may provide additional or different license terms and conditions 135 | for use, reproduction, or distribution of Your modifications, or 136 | for any such Derivative Works as a whole, provided Your use, 137 | reproduction, and distribution of the Work otherwise complies with 138 | the conditions stated in this License. 139 | 140 | 5. Submission of Contributions. Unless You explicitly state otherwise, 141 | any Contribution intentionally submitted for inclusion in the Work 142 | by You to the Licensor shall be under the terms and conditions of 143 | this License, without any additional terms or conditions. 144 | Notwithstanding the above, nothing herein shall supersede or modify 145 | the terms of any separate license agreement you may have executed 146 | with Licensor regarding such Contributions. 147 | 148 | 6. Trademarks. This License does not grant permission to use the trade 149 | names, trademarks, service marks, or product names of the Licensor, 150 | except as required for reasonable and customary use in describing the 151 | origin of the Work and reproducing the content of the NOTICE file. 152 | 153 | 7. Disclaimer of Warranty. Unless required by applicable law or 154 | agreed to in writing, Licensor provides the Work (and each 155 | Contributor provides its Contributions) on an "AS IS" BASIS, 156 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 157 | implied, including, without limitation, any warranties or conditions 158 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 159 | PARTICULAR PURPOSE. You are solely responsible for determining the 160 | appropriateness of using or redistributing the Work and assume any 161 | risks associated with Your exercise of permissions under this License. 162 | 163 | 8. Limitation of Liability. In no event and under no legal theory, 164 | whether in tort (including negligence), contract, or otherwise, 165 | unless required by applicable law (such as deliberate and grossly 166 | negligent acts) or agreed to in writing, shall any Contributor be 167 | liable to You for damages, including any direct, indirect, special, 168 | incidental, or consequential damages of any character arising as a 169 | result of this License or out of the use or inability to use the 170 | Work (including but not limited to damages for loss of goodwill, 171 | work stoppage, computer failure or malfunction, or any and all 172 | other commercial damages or losses), even if such Contributor 173 | has been advised of the possibility of such damages. 174 | 175 | 9. Accepting Warranty or Additional Liability. While redistributing 176 | the Work or Derivative Works thereof, You may choose to offer, 177 | and charge a fee for, acceptance of support, warranty, indemnity, 178 | or other liability obligations and/or rights consistent with this 179 | License. However, in accepting such obligations, You may act only 180 | on Your own behalf and on Your sole responsibility, not on behalf 181 | of any other Contributor, and only if You agree to indemnify, 182 | defend, and hold each Contributor harmless for any liability 183 | incurred by, or claims asserted against, such Contributor by reason 184 | of your accepting any such warranty or additional liability. 185 | 186 | END OF TERMS AND CONDITIONS 187 | 188 | APPENDIX: How to apply the Apache License to your work. 189 | 190 | To apply the Apache License to your work, attach the following 191 | boilerplate notice, with the fields enclosed by brackets "{}" 192 | replaced with your own identifying information. (Don't include 193 | the brackets!) The text should be enclosed in the appropriate 194 | comment syntax for the file format. We also recommend that a 195 | file or class name and description of purpose be included on the 196 | same "printed page" as the copyright notice for easier 197 | identification within third-party archives. 198 | 199 | Copyright 2018 Gregor Martynus and other contributors. 200 | 201 | Licensed under the Apache License, Version 2.0 (the "License"); 202 | you may not use this file except in compliance with the License. 203 | You may obtain a copy of the License at 204 | 205 | http://www.apache.org/licenses/LICENSE-2.0 206 | 207 | Unless required by applicable law or agreed to in writing, software 208 | distributed under the License is distributed on an "AS IS" BASIS, 209 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 210 | See the License for the specific language governing permissions and 211 | limitations under the License. 212 | - sources: README.md 213 | text: "[Apache 2.0](LICENSE)" 214 | notices: [] 215 | -------------------------------------------------------------------------------- /.licenses/npm/deprecation.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: deprecation 3 | version: 2.3.1 4 | type: npm 5 | summary: Log a deprecation message with stack 6 | homepage: https://github.com/gr2m/deprecation#readme 7 | license: isc 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | The ISC License 12 | 13 | Copyright (c) Gregor Martynus and contributors 14 | 15 | Permission to use, copy, modify, and/or distribute this software for any 16 | purpose with or without fee is hereby granted, provided that the above 17 | copyright notice and this permission notice appear in all copies. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 20 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 21 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 22 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 23 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 24 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 25 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 26 | - sources: README.md 27 | text: "[ISC](LICENSE)" 28 | notices: [] 29 | -------------------------------------------------------------------------------- /.licenses/npm/once.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: once 3 | version: 1.4.0 4 | type: npm 5 | summary: Run a function exactly one time 6 | homepage: https://github.com/isaacs/once#readme 7 | license: isc 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | The ISC License 12 | 13 | Copyright (c) Isaac Z. Schlueter and Contributors 14 | 15 | Permission to use, copy, modify, and/or distribute this software for any 16 | purpose with or without fee is hereby granted, provided that the above 17 | copyright notice and this permission notice appear in all copies. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 20 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 21 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 22 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 23 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 24 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 25 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 26 | notices: [] 27 | -------------------------------------------------------------------------------- /.licenses/npm/semver.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: semver 3 | version: 6.3.1 4 | type: npm 5 | summary: The semantic version parser used by npm. 6 | homepage: 7 | license: isc 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | The ISC License 12 | 13 | Copyright (c) Isaac Z. Schlueter and Contributors 14 | 15 | Permission to use, copy, modify, and/or distribute this software for any 16 | purpose with or without fee is hereby granted, provided that the above 17 | copyright notice and this permission notice appear in all copies. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 20 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 21 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 22 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 23 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 24 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 25 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 26 | notices: [] 27 | -------------------------------------------------------------------------------- /.licenses/npm/tunnel.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: tunnel 3 | version: 0.0.6 4 | type: npm 5 | summary: Node HTTP/HTTPS Agents for tunneling proxies 6 | homepage: https://github.com/koichik/node-tunnel/ 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | The MIT License (MIT) 12 | 13 | Copyright (c) 2012 Koichi Kobayashi 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | - sources: README.md 33 | text: Licensed under the [MIT](https://github.com/koichik/node-tunnel/blob/master/LICENSE) 34 | license. 35 | notices: [] 36 | -------------------------------------------------------------------------------- /.licenses/npm/undici.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: undici 3 | version: 5.28.4 4 | type: npm 5 | summary: An HTTP/1.1 client, written from scratch for Node.js 6 | homepage: https://undici.nodejs.org 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | MIT License 12 | 13 | Copyright (c) Matteo Collina and Undici contributors 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | - sources: README.md 33 | text: MIT 34 | notices: [] 35 | -------------------------------------------------------------------------------- /.licenses/npm/universal-user-agent.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: universal-user-agent 3 | version: 6.0.1 4 | type: npm 5 | summary: Get a user agent string in both browser and node 6 | homepage: 7 | license: isc 8 | licenses: 9 | - sources: LICENSE.md 10 | text: | 11 | # [ISC License](https://spdx.org/licenses/ISC) 12 | 13 | Copyright (c) 2018, Gregor Martynus (https://github.com/gr2m) 14 | 15 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | - sources: README.md 19 | text: "[ISC](LICENSE.md)" 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/uuid-3.4.0.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: uuid 3 | version: 3.4.0 4 | type: npm 5 | summary: RFC4122 (v1, v4, and v5) UUIDs 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE.md 10 | text: | 11 | The MIT License (MIT) 12 | 13 | Copyright (c) 2010-2016 Robert Kieffer and other contributors 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | notices: 33 | - sources: AUTHORS 34 | text: |- 35 | Robert Kieffer 36 | Christoph Tavan 37 | AJ ONeal 38 | Vincent Voyer 39 | Roman Shtylman 40 | -------------------------------------------------------------------------------- /.licenses/npm/uuid-8.3.2.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: uuid 3 | version: 8.3.2 4 | type: npm 5 | summary: RFC4122 (v1, v4, and v5) UUIDs 6 | homepage: https://github.com/uuidjs/uuid#readme 7 | license: mit 8 | licenses: 9 | - sources: LICENSE.md 10 | text: | 11 | The MIT License (MIT) 12 | 13 | Copyright (c) 2010-2020 Robert Kieffer and other contributors 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/uuid-9.0.1.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: uuid 3 | version: 9.0.1 4 | type: npm 5 | summary: RFC4122 (v1, v4, and v5) UUIDs 6 | homepage: 7 | license: mit 8 | licenses: 9 | - sources: LICENSE.md 10 | text: | 11 | The MIT License (MIT) 12 | 13 | Copyright (c) 2010-2020 Robert Kieffer and other contributors 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | notices: [] 21 | -------------------------------------------------------------------------------- /.licenses/npm/wrappy.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: wrappy 3 | version: 1.0.2 4 | type: npm 5 | summary: Callback wrapping utility 6 | homepage: https://github.com/npm/wrappy 7 | license: isc 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | The ISC License 12 | 13 | Copyright (c) Isaac Z. Schlueter and Contributors 14 | 15 | Permission to use, copy, modify, and/or distribute this software for any 16 | purpose with or without fee is hereby granted, provided that the above 17 | copyright notice and this permission notice appear in all copies. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 20 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 21 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 22 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 23 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 24 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 25 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 26 | notices: [] 27 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "parser": "typescript" 11 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v4.2.2 4 | * `url-helper.ts` now leverages well-known environment variables by @jww3 in https://github.com/actions/checkout/pull/1941 5 | * Expand unit test coverage for `isGhes` by @jww3 in https://github.com/actions/checkout/pull/1946 6 | 7 | ## v4.2.1 8 | * Check out other refs/* by commit if provided, fall back to ref by @orhantoy in https://github.com/actions/checkout/pull/1924 9 | 10 | ## v4.2.0 11 | 12 | * Add Ref and Commit outputs by @lucacome in https://github.com/actions/checkout/pull/1180 13 | * Dependency updates by @dependabot- https://github.com/actions/checkout/pull/1777, https://github.com/actions/checkout/pull/1872 14 | 15 | ## v4.1.7 16 | * Bump the minor-npm-dependencies group across 1 directory with 4 updates by @dependabot in https://github.com/actions/checkout/pull/1739 17 | * Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/actions/checkout/pull/1697 18 | * Check out other refs/* by commit by @orhantoy in https://github.com/actions/checkout/pull/1774 19 | * Pin actions/checkout's own workflows to a known, good, stable version. by @jww3 in https://github.com/actions/checkout/pull/1776 20 | 21 | ## v4.1.6 22 | * Check platform to set archive extension appropriately by @cory-miller in https://github.com/actions/checkout/pull/1732 23 | 24 | ## v4.1.5 25 | * Update NPM dependencies by @cory-miller in https://github.com/actions/checkout/pull/1703 26 | * Bump github/codeql-action from 2 to 3 by @dependabot in https://github.com/actions/checkout/pull/1694 27 | * Bump actions/setup-node from 1 to 4 by @dependabot in https://github.com/actions/checkout/pull/1696 28 | * Bump actions/upload-artifact from 2 to 4 by @dependabot in https://github.com/actions/checkout/pull/1695 29 | * README: Suggest `user.email` to be `41898282+github-actions[bot]@users.noreply.github.com` by @cory-miller in https://github.com/actions/checkout/pull/1707 30 | 31 | ## v4.1.4 32 | - Disable `extensions.worktreeConfig` when disabling `sparse-checkout` by @jww3 in https://github.com/actions/checkout/pull/1692 33 | - Add dependabot config by @cory-miller in https://github.com/actions/checkout/pull/1688 34 | - Bump the minor-actions-dependencies group with 2 updates by @dependabot in https://github.com/actions/checkout/pull/1693 35 | - Bump word-wrap from 1.2.3 to 1.2.5 by @dependabot in https://github.com/actions/checkout/pull/1643 36 | 37 | ## v4.1.3 38 | - Check git version before attempting to disable `sparse-checkout` by @jww3 in https://github.com/actions/checkout/pull/1656 39 | - Add SSH user parameter by @cory-miller in https://github.com/actions/checkout/pull/1685 40 | - Update `actions/checkout` version in `update-main-version.yml` by @jww3 in https://github.com/actions/checkout/pull/1650 41 | 42 | ## v4.1.2 43 | - Fix: Disable sparse checkout whenever `sparse-checkout` option is not present @dscho in https://github.com/actions/checkout/pull/1598 44 | 45 | ## v4.1.1 46 | - Correct link to GitHub Docs by @peterbe in https://github.com/actions/checkout/pull/1511 47 | - Link to release page from what's new section by @cory-miller in https://github.com/actions/checkout/pull/1514 48 | 49 | ## v4.1.0 50 | - [Add support for partial checkout filters](https://github.com/actions/checkout/pull/1396) 51 | 52 | ## v4.0.0 53 | - [Support fetching without the --progress option](https://github.com/actions/checkout/pull/1067) 54 | - [Update to node20](https://github.com/actions/checkout/pull/1436) 55 | 56 | ## v3.6.0 57 | - [Fix: Mark test scripts with Bash'isms to be run via Bash](https://github.com/actions/checkout/pull/1377) 58 | - [Add option to fetch tags even if fetch-depth > 0](https://github.com/actions/checkout/pull/579) 59 | 60 | ## v3.5.3 61 | - [Fix: Checkout fail in self-hosted runners when faulty submodule are checked-in](https://github.com/actions/checkout/pull/1196) 62 | - [Fix typos found by codespell](https://github.com/actions/checkout/pull/1287) 63 | - [Add support for sparse checkouts](https://github.com/actions/checkout/pull/1369) 64 | 65 | ## v3.5.2 66 | - [Fix api endpoint for GHES](https://github.com/actions/checkout/pull/1289) 67 | 68 | ## v3.5.1 69 | - [Fix slow checkout on Windows](https://github.com/actions/checkout/pull/1246) 70 | 71 | ## v3.5.0 72 | * [Add new public key for known_hosts](https://github.com/actions/checkout/pull/1237) 73 | 74 | ## v3.4.0 75 | - [Upgrade codeql actions to v2](https://github.com/actions/checkout/pull/1209) 76 | - [Upgrade dependencies](https://github.com/actions/checkout/pull/1210) 77 | - [Upgrade @actions/io](https://github.com/actions/checkout/pull/1225) 78 | 79 | ## v3.3.0 80 | - [Implement branch list using callbacks from exec function](https://github.com/actions/checkout/pull/1045) 81 | - [Add in explicit reference to private checkout options](https://github.com/actions/checkout/pull/1050) 82 | - [Fix comment typos (that got added in #770)](https://github.com/actions/checkout/pull/1057) 83 | 84 | ## v3.2.0 85 | - [Add GitHub Action to perform release](https://github.com/actions/checkout/pull/942) 86 | - [Fix status badge](https://github.com/actions/checkout/pull/967) 87 | - [Replace datadog/squid with ubuntu/squid Docker image](https://github.com/actions/checkout/pull/1002) 88 | - [Wrap pipeline commands for submoduleForeach in quotes](https://github.com/actions/checkout/pull/964) 89 | - [Update @actions/io to 1.1.2](https://github.com/actions/checkout/pull/1029) 90 | - [Upgrading version to 3.2.0](https://github.com/actions/checkout/pull/1039) 91 | 92 | ## v3.1.0 93 | - [Use @actions/core `saveState` and `getState`](https://github.com/actions/checkout/pull/939) 94 | - [Add `github-server-url` input](https://github.com/actions/checkout/pull/922) 95 | 96 | ## v3.0.2 97 | - [Add input `set-safe-directory`](https://github.com/actions/checkout/pull/770) 98 | 99 | ## v3.0.1 100 | - [Fixed an issue where checkout failed to run in container jobs due to the new git setting `safe.directory`](https://github.com/actions/checkout/pull/762) 101 | - [Bumped various npm package versions](https://github.com/actions/checkout/pull/744) 102 | 103 | ## v3.0.0 104 | 105 | - [Update to node 16](https://github.com/actions/checkout/pull/689) 106 | 107 | ## v2.3.1 108 | 109 | - [Fix default branch resolution for .wiki and when using SSH](https://github.com/actions/checkout/pull/284) 110 | 111 | ## v2.3.0 112 | 113 | - [Fallback to the default branch](https://github.com/actions/checkout/pull/278) 114 | 115 | ## v2.2.0 116 | 117 | - [Fetch all history for all tags and branches when fetch-depth=0](https://github.com/actions/checkout/pull/258) 118 | 119 | ## v2.1.1 120 | 121 | - Changes to support GHES ([here](https://github.com/actions/checkout/pull/236) and [here](https://github.com/actions/checkout/pull/248)) 122 | 123 | ## v2.1.0 124 | 125 | - [Group output](https://github.com/actions/checkout/pull/191) 126 | - [Changes to support GHES alpha release](https://github.com/actions/checkout/pull/199) 127 | - [Persist core.sshCommand for submodules](https://github.com/actions/checkout/pull/184) 128 | - [Add support ssh](https://github.com/actions/checkout/pull/163) 129 | - [Convert submodule SSH URL to HTTPS, when not using SSH](https://github.com/actions/checkout/pull/179) 130 | - [Add submodule support](https://github.com/actions/checkout/pull/157) 131 | - [Follow proxy settings](https://github.com/actions/checkout/pull/144) 132 | - [Fix ref for pr closed event when a pr is merged](https://github.com/actions/checkout/pull/141) 133 | - [Fix issue checking detached when git less than 2.22](https://github.com/actions/checkout/pull/128) 134 | 135 | ## v2.0.0 136 | 137 | - [Do not pass cred on command line](https://github.com/actions/checkout/pull/108) 138 | - [Add input persist-credentials](https://github.com/actions/checkout/pull/107) 139 | - [Fallback to REST API to download repo](https://github.com/actions/checkout/pull/104) 140 | 141 | ## v2 (beta) 142 | 143 | - Improved fetch performance 144 | - The default behavior now fetches only the SHA being checked-out 145 | - Script authenticated git commands 146 | - Persists `with.token` in the local git config 147 | - Enables your scripts to run authenticated git commands 148 | - Post-job cleanup removes the token 149 | - Coming soon: Opt out by setting `with.persist-credentials` to `false` 150 | - Creates a local branch 151 | - No longer detached HEAD when checking out a branch 152 | - A local branch is created with the corresponding upstream branch set 153 | - Improved layout 154 | - `with.path` is always relative to `github.workspace` 155 | - Aligns better with container actions, where `github.workspace` gets mapped in 156 | - Removed input `submodules` 157 | 158 | 159 | ## v1 160 | 161 | Refer [here](https://github.com/actions/checkout/blob/v1/CHANGELOG.md) for the V1 changelog 162 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @actions/actions-launch 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Submitting a pull request 4 | 5 | 1. Fork and clone the repository 6 | 1. Configure and install the dependencies: `npm install` 7 | 1. Create a new branch: `git checkout -b my-branch-name` 8 | 1. Make your change, add tests, and make sure the tests still pass: `npm run test` 9 | 1. Make sure your code is correctly formatted: `npm run format` 10 | 1. Update `dist/index.js` using `npm run build`. This creates a single javascript file that is used as an entrypoint for the action 11 | 1. Push to your fork and submit a pull request 12 | 1. Pat yourself on the back and wait for your pull request to be reviewed and merged 13 | 14 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 15 | 16 | - Write tests. 17 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 18 | 19 | ## Resources 20 | 21 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 22 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 23 | - [GitHub Help](https://help.github.com) 24 | - [Writing good commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 25 | 26 | Thanks! :heart: :heart: :heart: 27 | 28 | GitHub Actions Team :octocat: 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 GitHub, Inc. and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build and Test](https://github.com/actions/checkout/actions/workflows/test.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/test.yml) 2 | 3 | # Checkout V4 4 | 5 | This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it. 6 | 7 | Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. Set `fetch-depth: 0` to fetch all history for all branches and tags. Refer [here](https://docs.github.com/actions/using-workflows/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events. 8 | 9 | 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. 10 | 11 | When Git 2.18 or higher is not in your PATH, falls back to the REST API to download the files. 12 | 13 | # What's new 14 | 15 | Please refer to the [release page](https://github.com/actions/checkout/releases/latest) for the latest release notes. 16 | 17 | # Usage 18 | 19 | 20 | ```yaml 21 | - uses: actions/checkout@v4 22 | with: 23 | # Repository name with owner. For example, actions/checkout 24 | # Default: ${{ github.repository }} 25 | repository: '' 26 | 27 | # The branch, tag or SHA to checkout. When checking out the repository that 28 | # triggered a workflow, this defaults to the reference or SHA for that event. 29 | # Otherwise, uses the default branch. 30 | ref: '' 31 | 32 | # Personal access token (PAT) used to fetch the repository. The PAT is configured 33 | # with the local git config, which enables your scripts to run authenticated git 34 | # commands. The post-job step removes the PAT. 35 | # 36 | # We recommend using a service account with the least permissions necessary. Also 37 | # when generating a new PAT, select the least scopes necessary. 38 | # 39 | # [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) 40 | # 41 | # Default: ${{ github.token }} 42 | token: '' 43 | 44 | # SSH key used to fetch the repository. The SSH key is configured with the local 45 | # git config, which enables your scripts to run authenticated git commands. The 46 | # post-job step removes the SSH key. 47 | # 48 | # We recommend using a service account with the least permissions necessary. 49 | # 50 | # [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) 51 | ssh-key: '' 52 | 53 | # Known hosts in addition to the user and global host key database. The public SSH 54 | # keys for a host may be obtained using the utility `ssh-keyscan`. For example, 55 | # `ssh-keyscan github.com`. The public key for github.com is always implicitly 56 | # added. 57 | ssh-known-hosts: '' 58 | 59 | # Whether to perform strict host key checking. When true, adds the options 60 | # `StrictHostKeyChecking=yes` and `CheckHostIP=no` to the SSH command line. Use 61 | # the input `ssh-known-hosts` to configure additional hosts. 62 | # Default: true 63 | ssh-strict: '' 64 | 65 | # The user to use when connecting to the remote SSH host. By default 'git' is 66 | # used. 67 | # Default: git 68 | ssh-user: '' 69 | 70 | # Whether to configure the token or SSH key with the local git config 71 | # Default: true 72 | persist-credentials: '' 73 | 74 | # Relative path under $GITHUB_WORKSPACE to place the repository 75 | path: '' 76 | 77 | # Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching 78 | # Default: true 79 | clean: '' 80 | 81 | # Partially clone against a given filter. Overrides sparse-checkout if set. 82 | # Default: null 83 | filter: '' 84 | 85 | # Do a sparse checkout on given patterns. Each pattern should be separated with 86 | # new lines. 87 | # Default: null 88 | sparse-checkout: '' 89 | 90 | # Specifies whether to use cone-mode when doing a sparse checkout. 91 | # Default: true 92 | sparse-checkout-cone-mode: '' 93 | 94 | # Number of commits to fetch. 0 indicates all history for all branches and tags. 95 | # Default: 1 96 | fetch-depth: '' 97 | 98 | # Whether to fetch tags, even if fetch-depth > 0. 99 | # Default: false 100 | fetch-tags: '' 101 | 102 | # Whether to show progress status output when fetching. 103 | # Default: true 104 | show-progress: '' 105 | 106 | # Whether to download Git-LFS files 107 | # Default: false 108 | lfs: '' 109 | 110 | # Whether to checkout submodules: `true` to checkout submodules or `recursive` to 111 | # recursively checkout submodules. 112 | # 113 | # When the `ssh-key` input is not provided, SSH URLs beginning with 114 | # `git@github.com:` are converted to HTTPS. 115 | # 116 | # Default: false 117 | submodules: '' 118 | 119 | # Add repository path as safe.directory for Git global config by running `git 120 | # config --global --add safe.directory ` 121 | # Default: true 122 | set-safe-directory: '' 123 | 124 | # The base URL for the GitHub instance that you are trying to clone from, will use 125 | # environment defaults to fetch from the same instance that the workflow is 126 | # running from unless specified. Example URLs are https://github.com or 127 | # https://my-ghes-server.example.com 128 | github-server-url: '' 129 | ``` 130 | 131 | 132 | # Scenarios 133 | 134 | - [Fetch only the root files](#Fetch-only-the-root-files) 135 | - [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder) 136 | - [Fetch only a single file](#Fetch-only-a-single-file) 137 | - [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches) 138 | - [Checkout a different branch](#Checkout-a-different-branch) 139 | - [Checkout HEAD^](#Checkout-HEAD) 140 | - [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side) 141 | - [Checkout multiple repos (nested)](#Checkout-multiple-repos-nested) 142 | - [Checkout multiple repos (private)](#Checkout-multiple-repos-private) 143 | - [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit) 144 | - [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event) 145 | - [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token) 146 | - [Push a commit to a PR using the built-in token](#Push-a-commit-to-a-PR-using-the-built-in-token) 147 | 148 | ## Fetch only the root files 149 | 150 | ```yaml 151 | - uses: actions/checkout@v4 152 | with: 153 | sparse-checkout: . 154 | ``` 155 | 156 | ## Fetch only the root files and `.github` and `src` folder 157 | 158 | ```yaml 159 | - uses: actions/checkout@v4 160 | with: 161 | sparse-checkout: | 162 | .github 163 | src 164 | ``` 165 | 166 | ## Fetch only a single file 167 | 168 | ```yaml 169 | - uses: actions/checkout@v4 170 | with: 171 | sparse-checkout: | 172 | README.md 173 | sparse-checkout-cone-mode: false 174 | ``` 175 | 176 | ## Fetch all history for all tags and branches 177 | 178 | ```yaml 179 | - uses: actions/checkout@v4 180 | with: 181 | fetch-depth: 0 182 | ``` 183 | 184 | ## Checkout a different branch 185 | 186 | ```yaml 187 | - uses: actions/checkout@v4 188 | with: 189 | ref: my-branch 190 | ``` 191 | 192 | ## Checkout HEAD^ 193 | 194 | ```yaml 195 | - uses: actions/checkout@v4 196 | with: 197 | fetch-depth: 2 198 | - run: git checkout HEAD^ 199 | ``` 200 | 201 | ## Checkout multiple repos (side by side) 202 | 203 | ```yaml 204 | - name: Checkout 205 | uses: actions/checkout@v4 206 | with: 207 | path: main 208 | 209 | - name: Checkout tools repo 210 | uses: actions/checkout@v4 211 | with: 212 | repository: my-org/my-tools 213 | path: my-tools 214 | ``` 215 | > - If your secondary repository is private or internal you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private) 216 | 217 | ## Checkout multiple repos (nested) 218 | 219 | ```yaml 220 | - name: Checkout 221 | uses: actions/checkout@v4 222 | 223 | - name: Checkout tools repo 224 | uses: actions/checkout@v4 225 | with: 226 | repository: my-org/my-tools 227 | path: my-tools 228 | ``` 229 | > - If your secondary repository is private or internal you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private) 230 | 231 | ## Checkout multiple repos (private) 232 | 233 | ```yaml 234 | - name: Checkout 235 | uses: actions/checkout@v4 236 | with: 237 | path: main 238 | 239 | - name: Checkout private tools 240 | uses: actions/checkout@v4 241 | with: 242 | repository: my-org/my-private-tools 243 | token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT 244 | path: my-tools 245 | ``` 246 | 247 | > - `${{ 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). 248 | 249 | 250 | ## Checkout pull request HEAD commit instead of merge commit 251 | 252 | ```yaml 253 | - uses: actions/checkout@v4 254 | with: 255 | ref: ${{ github.event.pull_request.head.sha }} 256 | ``` 257 | 258 | ## Checkout pull request on closed event 259 | 260 | ```yaml 261 | on: 262 | pull_request: 263 | branches: [main] 264 | types: [opened, synchronize, closed] 265 | jobs: 266 | build: 267 | runs-on: ubuntu-latest 268 | steps: 269 | - uses: actions/checkout@v4 270 | ``` 271 | 272 | ## Push a commit using the built-in token 273 | 274 | ```yaml 275 | on: push 276 | jobs: 277 | build: 278 | runs-on: ubuntu-latest 279 | steps: 280 | - uses: actions/checkout@v4 281 | - run: | 282 | date > generated.txt 283 | # Note: the following account information will not work on GHES 284 | git config user.name "github-actions[bot]" 285 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 286 | git add . 287 | git commit -m "generated" 288 | git push 289 | ``` 290 | *NOTE:* The user email is `{user.id}+{user.login}@users.noreply.github.com`. See users API: https://api.github.com/users/github-actions%5Bbot%5D 291 | 292 | ## Push a commit to a PR using the built-in token 293 | 294 | In a pull request trigger, `ref` is required as GitHub Actions checks out in detached HEAD mode, meaning it doesn’t check out your branch by default. 295 | 296 | ```yaml 297 | on: pull_request 298 | jobs: 299 | build: 300 | runs-on: ubuntu-latest 301 | steps: 302 | - uses: actions/checkout@v4 303 | with: 304 | ref: ${{ github.head_ref }} 305 | - run: | 306 | date > generated.txt 307 | # Note: the following account information will not work on GHES 308 | git config user.name "github-actions[bot]" 309 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 310 | git add . 311 | git commit -m "generated" 312 | git push 313 | ``` 314 | 315 | *NOTE:* The user email is `{user.id}+{user.login}@users.noreply.github.com`. See users API: https://api.github.com/users/github-actions%5Bbot%5D 316 | 317 | # Recommended permissions 318 | 319 | When using the `checkout` action in your GitHub Actions workflow, it is recommended to set the following `GITHUB_TOKEN` permissions to ensure proper functionality, unless alternative auth is provided via the `token` or `ssh-key` inputs: 320 | 321 | ```yaml 322 | permissions: 323 | contents: read 324 | ``` 325 | 326 | # License 327 | 328 | The scripts and documentation in this project are released under the [MIT License](LICENSE) 329 | -------------------------------------------------------------------------------- /__test__/git-command-manager.test.ts: -------------------------------------------------------------------------------- 1 | import * as exec from '@actions/exec' 2 | import * as fshelper from '../lib/fs-helper' 3 | import * as commandManager from '../lib/git-command-manager' 4 | 5 | let git: commandManager.IGitCommandManager 6 | let mockExec = jest.fn() 7 | 8 | describe('git-auth-helper tests', () => { 9 | beforeAll(async () => {}) 10 | 11 | beforeEach(async () => { 12 | jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn()) 13 | jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn()) 14 | }) 15 | 16 | afterEach(() => { 17 | jest.restoreAllMocks() 18 | }) 19 | 20 | afterAll(() => {}) 21 | 22 | it('branch list matches', async () => { 23 | mockExec.mockImplementation((path, args, options) => { 24 | console.log(args, options.listeners.stdout) 25 | 26 | if (args.includes('version')) { 27 | options.listeners.stdout(Buffer.from('2.18')) 28 | return 0 29 | } 30 | 31 | if (args.includes('rev-parse')) { 32 | options.listeners.stdline(Buffer.from('refs/heads/foo')) 33 | options.listeners.stdline(Buffer.from('refs/heads/bar')) 34 | return 0 35 | } 36 | 37 | return 1 38 | }) 39 | jest.spyOn(exec, 'exec').mockImplementation(mockExec) 40 | const workingDirectory = 'test' 41 | const lfs = false 42 | const doSparseCheckout = false 43 | git = await commandManager.createCommandManager( 44 | workingDirectory, 45 | lfs, 46 | doSparseCheckout 47 | ) 48 | 49 | let branches = await git.branchList(false) 50 | 51 | expect(branches).toHaveLength(2) 52 | expect(branches.sort()).toEqual(['foo', 'bar'].sort()) 53 | }) 54 | 55 | it('ambiguous ref name output is captured', async () => { 56 | mockExec.mockImplementation((path, args, options) => { 57 | console.log(args, options.listeners.stdout) 58 | 59 | if (args.includes('version')) { 60 | options.listeners.stdout(Buffer.from('2.18')) 61 | return 0 62 | } 63 | 64 | if (args.includes('rev-parse')) { 65 | options.listeners.stdline(Buffer.from('refs/heads/foo')) 66 | // If refs/tags/v1 and refs/heads/tags/v1 existed on this repository 67 | options.listeners.errline( 68 | Buffer.from("error: refname 'tags/v1' is ambiguous") 69 | ) 70 | return 0 71 | } 72 | 73 | return 1 74 | }) 75 | jest.spyOn(exec, 'exec').mockImplementation(mockExec) 76 | const workingDirectory = 'test' 77 | const lfs = false 78 | const doSparseCheckout = false 79 | git = await commandManager.createCommandManager( 80 | workingDirectory, 81 | lfs, 82 | doSparseCheckout 83 | ) 84 | 85 | let branches = await git.branchList(false) 86 | 87 | expect(branches).toHaveLength(1) 88 | expect(branches.sort()).toEqual(['foo'].sort()) 89 | }) 90 | }) 91 | 92 | describe('Test fetchDepth and fetchTags options', () => { 93 | beforeEach(async () => { 94 | jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn()) 95 | jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn()) 96 | mockExec.mockImplementation((path, args, options) => { 97 | console.log(args, options.listeners.stdout) 98 | 99 | if (args.includes('version')) { 100 | options.listeners.stdout(Buffer.from('2.18')) 101 | } 102 | 103 | return 0 104 | }) 105 | }) 106 | 107 | afterEach(() => { 108 | jest.restoreAllMocks() 109 | }) 110 | 111 | it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is true', async () => { 112 | jest.spyOn(exec, 'exec').mockImplementation(mockExec) 113 | const workingDirectory = 'test' 114 | const lfs = false 115 | const doSparseCheckout = false 116 | git = await commandManager.createCommandManager( 117 | workingDirectory, 118 | lfs, 119 | doSparseCheckout 120 | ) 121 | 122 | const refSpec = ['refspec1', 'refspec2'] 123 | const options = { 124 | filter: 'filterValue', 125 | fetchDepth: 0, 126 | fetchTags: true 127 | } 128 | 129 | await git.fetch(refSpec, options) 130 | 131 | expect(mockExec).toHaveBeenCalledWith( 132 | expect.any(String), 133 | [ 134 | '-c', 135 | 'protocol.version=2', 136 | 'fetch', 137 | '--prune', 138 | '--no-recurse-submodules', 139 | '--filter=filterValue', 140 | 'origin', 141 | 'refspec1', 142 | 'refspec2' 143 | ], 144 | expect.any(Object) 145 | ) 146 | }) 147 | 148 | it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is false', async () => { 149 | jest.spyOn(exec, 'exec').mockImplementation(mockExec) 150 | 151 | const workingDirectory = 'test' 152 | const lfs = false 153 | const doSparseCheckout = false 154 | git = await commandManager.createCommandManager( 155 | workingDirectory, 156 | lfs, 157 | doSparseCheckout 158 | ) 159 | const refSpec = ['refspec1', 'refspec2'] 160 | const options = { 161 | filter: 'filterValue', 162 | fetchDepth: 0, 163 | fetchTags: false 164 | } 165 | 166 | await git.fetch(refSpec, options) 167 | 168 | expect(mockExec).toHaveBeenCalledWith( 169 | expect.any(String), 170 | [ 171 | '-c', 172 | 'protocol.version=2', 173 | 'fetch', 174 | '--no-tags', 175 | '--prune', 176 | '--no-recurse-submodules', 177 | '--filter=filterValue', 178 | 'origin', 179 | 'refspec1', 180 | 'refspec2' 181 | ], 182 | expect.any(Object) 183 | ) 184 | }) 185 | 186 | it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is false', async () => { 187 | jest.spyOn(exec, 'exec').mockImplementation(mockExec) 188 | 189 | const workingDirectory = 'test' 190 | const lfs = false 191 | const doSparseCheckout = false 192 | git = await commandManager.createCommandManager( 193 | workingDirectory, 194 | lfs, 195 | doSparseCheckout 196 | ) 197 | const refSpec = ['refspec1', 'refspec2'] 198 | const options = { 199 | filter: 'filterValue', 200 | fetchDepth: 1, 201 | fetchTags: false 202 | } 203 | 204 | await git.fetch(refSpec, options) 205 | 206 | expect(mockExec).toHaveBeenCalledWith( 207 | expect.any(String), 208 | [ 209 | '-c', 210 | 'protocol.version=2', 211 | 'fetch', 212 | '--no-tags', 213 | '--prune', 214 | '--no-recurse-submodules', 215 | '--filter=filterValue', 216 | '--depth=1', 217 | 'origin', 218 | 'refspec1', 219 | 'refspec2' 220 | ], 221 | expect.any(Object) 222 | ) 223 | }) 224 | 225 | it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is true', async () => { 226 | jest.spyOn(exec, 'exec').mockImplementation(mockExec) 227 | 228 | const workingDirectory = 'test' 229 | const lfs = false 230 | const doSparseCheckout = false 231 | git = await commandManager.createCommandManager( 232 | workingDirectory, 233 | lfs, 234 | doSparseCheckout 235 | ) 236 | const refSpec = ['refspec1', 'refspec2'] 237 | const options = { 238 | filter: 'filterValue', 239 | fetchDepth: 1, 240 | fetchTags: true 241 | } 242 | 243 | await git.fetch(refSpec, options) 244 | 245 | expect(mockExec).toHaveBeenCalledWith( 246 | expect.any(String), 247 | [ 248 | '-c', 249 | 'protocol.version=2', 250 | 'fetch', 251 | '--prune', 252 | '--no-recurse-submodules', 253 | '--filter=filterValue', 254 | '--depth=1', 255 | 'origin', 256 | 'refspec1', 257 | 'refspec2' 258 | ], 259 | expect.any(Object) 260 | ) 261 | }) 262 | 263 | it('should call execGit with the correct arguments when showProgress is true', async () => { 264 | jest.spyOn(exec, 'exec').mockImplementation(mockExec) 265 | 266 | const workingDirectory = 'test' 267 | const lfs = false 268 | const doSparseCheckout = false 269 | git = await commandManager.createCommandManager( 270 | workingDirectory, 271 | lfs, 272 | doSparseCheckout 273 | ) 274 | const refSpec = ['refspec1', 'refspec2'] 275 | const options = { 276 | filter: 'filterValue', 277 | showProgress: true 278 | } 279 | 280 | await git.fetch(refSpec, options) 281 | 282 | expect(mockExec).toHaveBeenCalledWith( 283 | expect.any(String), 284 | [ 285 | '-c', 286 | 'protocol.version=2', 287 | 'fetch', 288 | '--no-tags', 289 | '--prune', 290 | '--no-recurse-submodules', 291 | '--progress', 292 | '--filter=filterValue', 293 | 'origin', 294 | 'refspec1', 295 | 'refspec2' 296 | ], 297 | expect.any(Object) 298 | ) 299 | }) 300 | 301 | it('should call execGit with the correct arguments when fetchDepth is 42 and showProgress is true', async () => { 302 | jest.spyOn(exec, 'exec').mockImplementation(mockExec) 303 | 304 | const workingDirectory = 'test' 305 | const lfs = false 306 | const doSparseCheckout = false 307 | git = await commandManager.createCommandManager( 308 | workingDirectory, 309 | lfs, 310 | doSparseCheckout 311 | ) 312 | const refSpec = ['refspec1', 'refspec2'] 313 | const options = { 314 | filter: 'filterValue', 315 | fetchDepth: 42, 316 | showProgress: true 317 | } 318 | 319 | await git.fetch(refSpec, options) 320 | 321 | expect(mockExec).toHaveBeenCalledWith( 322 | expect.any(String), 323 | [ 324 | '-c', 325 | 'protocol.version=2', 326 | 'fetch', 327 | '--no-tags', 328 | '--prune', 329 | '--no-recurse-submodules', 330 | '--progress', 331 | '--filter=filterValue', 332 | '--depth=42', 333 | 'origin', 334 | 'refspec1', 335 | 'refspec2' 336 | ], 337 | expect.any(Object) 338 | ) 339 | }) 340 | 341 | it('should call execGit with the correct arguments when fetchTags is true and showProgress is true', async () => { 342 | jest.spyOn(exec, 'exec').mockImplementation(mockExec) 343 | 344 | const workingDirectory = 'test' 345 | const lfs = false 346 | const doSparseCheckout = false 347 | git = await commandManager.createCommandManager( 348 | workingDirectory, 349 | lfs, 350 | doSparseCheckout 351 | ) 352 | const refSpec = ['refspec1', 'refspec2'] 353 | const options = { 354 | filter: 'filterValue', 355 | fetchTags: true, 356 | showProgress: true 357 | } 358 | 359 | await git.fetch(refSpec, options) 360 | 361 | expect(mockExec).toHaveBeenCalledWith( 362 | expect.any(String), 363 | [ 364 | '-c', 365 | 'protocol.version=2', 366 | 'fetch', 367 | '--prune', 368 | '--no-recurse-submodules', 369 | '--progress', 370 | '--filter=filterValue', 371 | 'origin', 372 | 'refspec1', 373 | 'refspec2' 374 | ], 375 | expect.any(Object) 376 | ) 377 | }) 378 | }) 379 | -------------------------------------------------------------------------------- /__test__/git-version.test.ts: -------------------------------------------------------------------------------- 1 | import {GitVersion} from '../src/git-version' 2 | import {MinimumGitSparseCheckoutVersion} from '../src/git-command-manager' 3 | 4 | describe('git-version tests', () => { 5 | it('basics', async () => { 6 | let version = new GitVersion('') 7 | expect(version.isValid()).toBeFalsy() 8 | 9 | version = new GitVersion('asdf') 10 | expect(version.isValid()).toBeFalsy() 11 | 12 | version = new GitVersion('1.2') 13 | expect(version.isValid()).toBeTruthy() 14 | expect(version.toString()).toBe('1.2') 15 | 16 | version = new GitVersion('1.2.3') 17 | expect(version.isValid()).toBeTruthy() 18 | expect(version.toString()).toBe('1.2.3') 19 | }) 20 | 21 | it('check minimum', async () => { 22 | let version = new GitVersion('4.5') 23 | expect(version.checkMinimum(new GitVersion('3.6'))).toBeTruthy() 24 | expect(version.checkMinimum(new GitVersion('3.6.7'))).toBeTruthy() 25 | expect(version.checkMinimum(new GitVersion('4.4'))).toBeTruthy() 26 | expect(version.checkMinimum(new GitVersion('4.5'))).toBeTruthy() 27 | expect(version.checkMinimum(new GitVersion('4.5.0'))).toBeTruthy() 28 | expect(version.checkMinimum(new GitVersion('4.6'))).toBeFalsy() 29 | expect(version.checkMinimum(new GitVersion('4.6.0'))).toBeFalsy() 30 | expect(version.checkMinimum(new GitVersion('5.1'))).toBeFalsy() 31 | expect(version.checkMinimum(new GitVersion('5.1.2'))).toBeFalsy() 32 | 33 | version = new GitVersion('4.5.6') 34 | expect(version.checkMinimum(new GitVersion('3.6'))).toBeTruthy() 35 | expect(version.checkMinimum(new GitVersion('3.6.7'))).toBeTruthy() 36 | expect(version.checkMinimum(new GitVersion('4.4'))).toBeTruthy() 37 | expect(version.checkMinimum(new GitVersion('4.5'))).toBeTruthy() 38 | expect(version.checkMinimum(new GitVersion('4.5.5'))).toBeTruthy() 39 | expect(version.checkMinimum(new GitVersion('4.5.6'))).toBeTruthy() 40 | expect(version.checkMinimum(new GitVersion('4.5.7'))).toBeFalsy() 41 | expect(version.checkMinimum(new GitVersion('4.6'))).toBeFalsy() 42 | expect(version.checkMinimum(new GitVersion('4.6.0'))).toBeFalsy() 43 | expect(version.checkMinimum(new GitVersion('5.1'))).toBeFalsy() 44 | expect(version.checkMinimum(new GitVersion('5.1.2'))).toBeFalsy() 45 | }) 46 | 47 | it('sparse checkout', async () => { 48 | const minSparseVer = MinimumGitSparseCheckoutVersion 49 | expect(new GitVersion('1.0').checkMinimum(minSparseVer)).toBeFalsy() 50 | expect(new GitVersion('1.99').checkMinimum(minSparseVer)).toBeFalsy() 51 | expect(new GitVersion('2.0').checkMinimum(minSparseVer)).toBeFalsy() 52 | expect(new GitVersion('2.24').checkMinimum(minSparseVer)).toBeFalsy() 53 | expect(new GitVersion('2.24.0').checkMinimum(minSparseVer)).toBeFalsy() 54 | expect(new GitVersion('2.24.9').checkMinimum(minSparseVer)).toBeFalsy() 55 | expect(new GitVersion('2.25').checkMinimum(minSparseVer)).toBeFalsy() 56 | expect(new GitVersion('2.25.0').checkMinimum(minSparseVer)).toBeFalsy() 57 | expect(new GitVersion('2.25.1').checkMinimum(minSparseVer)).toBeFalsy() 58 | expect(new GitVersion('2.25.9').checkMinimum(minSparseVer)).toBeFalsy() 59 | expect(new GitVersion('2.26').checkMinimum(minSparseVer)).toBeFalsy() 60 | expect(new GitVersion('2.26.0').checkMinimum(minSparseVer)).toBeFalsy() 61 | expect(new GitVersion('2.26.1').checkMinimum(minSparseVer)).toBeFalsy() 62 | expect(new GitVersion('2.26.9').checkMinimum(minSparseVer)).toBeFalsy() 63 | expect(new GitVersion('2.27').checkMinimum(minSparseVer)).toBeFalsy() 64 | expect(new GitVersion('2.27.0').checkMinimum(minSparseVer)).toBeFalsy() 65 | expect(new GitVersion('2.27.1').checkMinimum(minSparseVer)).toBeFalsy() 66 | expect(new GitVersion('2.27.9').checkMinimum(minSparseVer)).toBeFalsy() 67 | // /--------------------------------------- 68 | // ^^^ before / after vvv 69 | // --------------------------/ 70 | expect(new GitVersion('2.28').checkMinimum(minSparseVer)).toBeTruthy() 71 | expect(new GitVersion('2.28.0').checkMinimum(minSparseVer)).toBeTruthy() 72 | expect(new GitVersion('2.28.1').checkMinimum(minSparseVer)).toBeTruthy() 73 | expect(new GitVersion('2.28.9').checkMinimum(minSparseVer)).toBeTruthy() 74 | expect(new GitVersion('2.29').checkMinimum(minSparseVer)).toBeTruthy() 75 | expect(new GitVersion('2.29.0').checkMinimum(minSparseVer)).toBeTruthy() 76 | expect(new GitVersion('2.29.1').checkMinimum(minSparseVer)).toBeTruthy() 77 | expect(new GitVersion('2.29.9').checkMinimum(minSparseVer)).toBeTruthy() 78 | expect(new GitVersion('2.99').checkMinimum(minSparseVer)).toBeTruthy() 79 | expect(new GitVersion('3.0').checkMinimum(minSparseVer)).toBeTruthy() 80 | expect(new GitVersion('3.99').checkMinimum(minSparseVer)).toBeTruthy() 81 | expect(new GitVersion('4.0').checkMinimum(minSparseVer)).toBeTruthy() 82 | expect(new GitVersion('4.99').checkMinimum(minSparseVer)).toBeTruthy() 83 | expect(new GitVersion('5.0').checkMinimum(minSparseVer)).toBeTruthy() 84 | expect(new GitVersion('5.99').checkMinimum(minSparseVer)).toBeTruthy() 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /__test__/input-helper.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as fsHelper from '../lib/fs-helper' 3 | import * as github from '@actions/github' 4 | import * as inputHelper from '../lib/input-helper' 5 | import * as path from 'path' 6 | import * as workflowContextHelper from '../lib/workflow-context-helper' 7 | import {IGitSourceSettings} from '../lib/git-source-settings' 8 | 9 | const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE'] 10 | const gitHubWorkspace = path.resolve('/checkout-tests/workspace') 11 | 12 | // Inputs for mock @actions/core 13 | let inputs = {} as any 14 | 15 | // Shallow clone original @actions/github context 16 | let originalContext = {...github.context} 17 | 18 | describe('input-helper tests', () => { 19 | beforeAll(() => { 20 | // Mock getInput 21 | jest.spyOn(core, 'getInput').mockImplementation((name: string) => { 22 | return inputs[name] 23 | }) 24 | 25 | // Mock error/warning/info/debug 26 | jest.spyOn(core, 'error').mockImplementation(jest.fn()) 27 | jest.spyOn(core, 'warning').mockImplementation(jest.fn()) 28 | jest.spyOn(core, 'info').mockImplementation(jest.fn()) 29 | jest.spyOn(core, 'debug').mockImplementation(jest.fn()) 30 | 31 | // Mock github context 32 | jest.spyOn(github.context, 'repo', 'get').mockImplementation(() => { 33 | return { 34 | owner: 'some-owner', 35 | repo: 'some-repo' 36 | } 37 | }) 38 | github.context.ref = 'refs/heads/some-ref' 39 | github.context.sha = '1234567890123456789012345678901234567890' 40 | 41 | // Mock ./fs-helper directoryExistsSync() 42 | jest 43 | .spyOn(fsHelper, 'directoryExistsSync') 44 | .mockImplementation((path: string) => path == gitHubWorkspace) 45 | 46 | // Mock ./workflowContextHelper getOrganizationId() 47 | jest 48 | .spyOn(workflowContextHelper, 'getOrganizationId') 49 | .mockImplementation(() => Promise.resolve(123456)) 50 | 51 | // GitHub workspace 52 | process.env['GITHUB_WORKSPACE'] = gitHubWorkspace 53 | }) 54 | 55 | beforeEach(() => { 56 | // Reset inputs 57 | inputs = {} 58 | }) 59 | 60 | afterAll(() => { 61 | // Restore GitHub workspace 62 | delete process.env['GITHUB_WORKSPACE'] 63 | if (originalGitHubWorkspace) { 64 | process.env['GITHUB_WORKSPACE'] = originalGitHubWorkspace 65 | } 66 | 67 | // Restore @actions/github context 68 | github.context.ref = originalContext.ref 69 | github.context.sha = originalContext.sha 70 | 71 | // Restore 72 | jest.restoreAllMocks() 73 | }) 74 | 75 | it('sets defaults', async () => { 76 | const settings: IGitSourceSettings = await inputHelper.getInputs() 77 | expect(settings).toBeTruthy() 78 | expect(settings.authToken).toBeFalsy() 79 | expect(settings.clean).toBe(true) 80 | expect(settings.commit).toBeTruthy() 81 | expect(settings.commit).toBe('1234567890123456789012345678901234567890') 82 | expect(settings.filter).toBe(undefined) 83 | expect(settings.sparseCheckout).toBe(undefined) 84 | expect(settings.sparseCheckoutConeMode).toBe(true) 85 | expect(settings.fetchDepth).toBe(1) 86 | expect(settings.fetchTags).toBe(false) 87 | expect(settings.showProgress).toBe(true) 88 | expect(settings.lfs).toBe(false) 89 | expect(settings.ref).toBe('refs/heads/some-ref') 90 | expect(settings.repositoryName).toBe('some-repo') 91 | expect(settings.repositoryOwner).toBe('some-owner') 92 | expect(settings.repositoryPath).toBe(gitHubWorkspace) 93 | expect(settings.setSafeDirectory).toBe(true) 94 | }) 95 | 96 | it('qualifies ref', async () => { 97 | let originalRef = github.context.ref 98 | try { 99 | github.context.ref = 'some-unqualified-ref' 100 | const settings: IGitSourceSettings = await inputHelper.getInputs() 101 | expect(settings).toBeTruthy() 102 | expect(settings.commit).toBe('1234567890123456789012345678901234567890') 103 | expect(settings.ref).toBe('refs/heads/some-unqualified-ref') 104 | } finally { 105 | github.context.ref = originalRef 106 | } 107 | }) 108 | 109 | it('requires qualified repo', async () => { 110 | inputs.repository = 'some-unqualified-repo' 111 | try { 112 | await inputHelper.getInputs() 113 | throw 'should not reach here' 114 | } catch (err) { 115 | expect(`(${(err as any).message}`).toMatch( 116 | "Invalid repository 'some-unqualified-repo'" 117 | ) 118 | } 119 | }) 120 | 121 | it('roots path', async () => { 122 | inputs.path = 'some-directory/some-subdirectory' 123 | const settings: IGitSourceSettings = await inputHelper.getInputs() 124 | expect(settings.repositoryPath).toBe( 125 | path.join(gitHubWorkspace, 'some-directory', 'some-subdirectory') 126 | ) 127 | }) 128 | 129 | it('sets ref to empty when explicit sha', async () => { 130 | inputs.ref = '1111111111222222222233333333334444444444' 131 | const settings: IGitSourceSettings = await inputHelper.getInputs() 132 | expect(settings.ref).toBeFalsy() 133 | expect(settings.commit).toBe('1111111111222222222233333333334444444444') 134 | }) 135 | 136 | it('sets sha to empty when explicit ref', async () => { 137 | inputs.ref = 'refs/heads/some-other-ref' 138 | const settings: IGitSourceSettings = await inputHelper.getInputs() 139 | expect(settings.ref).toBe('refs/heads/some-other-ref') 140 | expect(settings.commit).toBeFalsy() 141 | }) 142 | 143 | it('sets workflow organization ID', async () => { 144 | const settings: IGitSourceSettings = await inputHelper.getInputs() 145 | expect(settings.workflowOrganizationId).toBe(123456) 146 | }) 147 | }) 148 | -------------------------------------------------------------------------------- /__test__/modify-work-tree.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "./basic/basic-file.txt" ]; then 4 | echo "Expected basic file does not exist" 5 | exit 1 6 | fi 7 | 8 | echo hello >> ./basic/basic-file.txt 9 | echo hello >> ./basic/new-file.txt 10 | git -C ./basic status -------------------------------------------------------------------------------- /__test__/override-git-version.cmd: -------------------------------------------------------------------------------- 1 | 2 | mkdir override-git-version 3 | cd override-git-version 4 | echo @echo override git version 1.2.3 > git.cmd 5 | echo "%CD%" >> $GITHUB_PATH 6 | cd .. 7 | -------------------------------------------------------------------------------- /__test__/override-git-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir override-git-version 4 | cd override-git-version 5 | echo "#!/bin/sh" > git 6 | echo "echo override git version 1.2.3" >> git 7 | chmod +x git 8 | echo "$(pwd)" >> $GITHUB_PATH 9 | cd .. 10 | -------------------------------------------------------------------------------- /__test__/ref-helper.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as refHelper from '../lib/ref-helper' 3 | import {IGitCommandManager} from '../lib/git-command-manager' 4 | 5 | const commit = '1234567890123456789012345678901234567890' 6 | let git: IGitCommandManager 7 | 8 | describe('ref-helper tests', () => { 9 | beforeEach(() => { 10 | git = {} as unknown as IGitCommandManager 11 | }) 12 | 13 | it('getCheckoutInfo requires git', async () => { 14 | const git = null as unknown as IGitCommandManager 15 | try { 16 | await refHelper.getCheckoutInfo(git, 'refs/heads/my/branch', commit) 17 | throw new Error('Should not reach here') 18 | } catch (err) { 19 | expect((err as any)?.message).toBe('Arg git cannot be empty') 20 | } 21 | }) 22 | 23 | it('getCheckoutInfo requires ref or commit', async () => { 24 | try { 25 | await refHelper.getCheckoutInfo(git, '', '') 26 | throw new Error('Should not reach here') 27 | } catch (err) { 28 | expect((err as any)?.message).toBe( 29 | 'Args ref and commit cannot both be empty' 30 | ) 31 | } 32 | }) 33 | 34 | it('getCheckoutInfo sha only', async () => { 35 | const checkoutInfo = await refHelper.getCheckoutInfo(git, '', commit) 36 | expect(checkoutInfo.ref).toBe(commit) 37 | expect(checkoutInfo.startPoint).toBeFalsy() 38 | }) 39 | 40 | it('getCheckoutInfo refs/heads/', async () => { 41 | const checkoutInfo = await refHelper.getCheckoutInfo( 42 | git, 43 | 'refs/heads/my/branch', 44 | commit 45 | ) 46 | expect(checkoutInfo.ref).toBe('my/branch') 47 | expect(checkoutInfo.startPoint).toBe('refs/remotes/origin/my/branch') 48 | }) 49 | 50 | it('getCheckoutInfo refs/pull/', async () => { 51 | const checkoutInfo = await refHelper.getCheckoutInfo( 52 | git, 53 | 'refs/pull/123/merge', 54 | commit 55 | ) 56 | expect(checkoutInfo.ref).toBe('refs/remotes/pull/123/merge') 57 | expect(checkoutInfo.startPoint).toBeFalsy() 58 | }) 59 | 60 | it('getCheckoutInfo refs/tags/', async () => { 61 | const checkoutInfo = await refHelper.getCheckoutInfo( 62 | git, 63 | 'refs/tags/my-tag', 64 | commit 65 | ) 66 | expect(checkoutInfo.ref).toBe('refs/tags/my-tag') 67 | expect(checkoutInfo.startPoint).toBeFalsy() 68 | }) 69 | 70 | it('getCheckoutInfo refs/', async () => { 71 | const checkoutInfo = await refHelper.getCheckoutInfo( 72 | git, 73 | 'refs/gh/queue/main/pr-123', 74 | commit 75 | ) 76 | expect(checkoutInfo.ref).toBe(commit) 77 | expect(checkoutInfo.startPoint).toBeFalsy() 78 | }) 79 | 80 | it('getCheckoutInfo refs/ without commit', async () => { 81 | const checkoutInfo = await refHelper.getCheckoutInfo( 82 | git, 83 | 'refs/non-standard-ref', 84 | '' 85 | ) 86 | expect(checkoutInfo.ref).toBe('refs/non-standard-ref') 87 | expect(checkoutInfo.startPoint).toBeFalsy() 88 | }) 89 | 90 | it('getCheckoutInfo unqualified branch only', async () => { 91 | git.branchExists = jest.fn(async (remote: boolean, pattern: string) => { 92 | return true 93 | }) 94 | 95 | const checkoutInfo = await refHelper.getCheckoutInfo(git, 'my/branch', '') 96 | 97 | expect(checkoutInfo.ref).toBe('my/branch') 98 | expect(checkoutInfo.startPoint).toBe('refs/remotes/origin/my/branch') 99 | }) 100 | 101 | it('getCheckoutInfo unqualified tag only', async () => { 102 | git.branchExists = jest.fn(async (remote: boolean, pattern: string) => { 103 | return false 104 | }) 105 | git.tagExists = jest.fn(async (pattern: string) => { 106 | return true 107 | }) 108 | 109 | const checkoutInfo = await refHelper.getCheckoutInfo(git, 'my-tag', '') 110 | 111 | expect(checkoutInfo.ref).toBe('refs/tags/my-tag') 112 | expect(checkoutInfo.startPoint).toBeFalsy() 113 | }) 114 | 115 | it('getCheckoutInfo unqualified ref only, not a branch or tag', async () => { 116 | git.branchExists = jest.fn(async (remote: boolean, pattern: string) => { 117 | return false 118 | }) 119 | git.tagExists = jest.fn(async (pattern: string) => { 120 | return false 121 | }) 122 | 123 | try { 124 | await refHelper.getCheckoutInfo(git, 'my-ref', '') 125 | throw new Error('Should not reach here') 126 | } catch (err) { 127 | expect((err as any)?.message).toBe( 128 | "A branch or tag with the name 'my-ref' could not be found" 129 | ) 130 | } 131 | }) 132 | 133 | it('getRefSpec requires ref or commit', async () => { 134 | assert.throws( 135 | () => refHelper.getRefSpec('', ''), 136 | /Args ref and commit cannot both be empty/ 137 | ) 138 | }) 139 | 140 | it('getRefSpec sha + refs/heads/', async () => { 141 | const refSpec = refHelper.getRefSpec('refs/heads/my/branch', commit) 142 | expect(refSpec.length).toBe(1) 143 | expect(refSpec[0]).toBe(`+${commit}:refs/remotes/origin/my/branch`) 144 | }) 145 | 146 | it('getRefSpec sha + refs/pull/', async () => { 147 | const refSpec = refHelper.getRefSpec('refs/pull/123/merge', commit) 148 | expect(refSpec.length).toBe(1) 149 | expect(refSpec[0]).toBe(`+${commit}:refs/remotes/pull/123/merge`) 150 | }) 151 | 152 | it('getRefSpec sha + refs/tags/', async () => { 153 | const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit) 154 | expect(refSpec.length).toBe(1) 155 | expect(refSpec[0]).toBe(`+${commit}:refs/tags/my-tag`) 156 | }) 157 | 158 | it('getRefSpec sha only', async () => { 159 | const refSpec = refHelper.getRefSpec('', commit) 160 | expect(refSpec.length).toBe(1) 161 | expect(refSpec[0]).toBe(commit) 162 | }) 163 | 164 | it('getRefSpec unqualified ref only', async () => { 165 | const refSpec = refHelper.getRefSpec('my-ref', '') 166 | expect(refSpec.length).toBe(2) 167 | expect(refSpec[0]).toBe('+refs/heads/my-ref*:refs/remotes/origin/my-ref*') 168 | expect(refSpec[1]).toBe('+refs/tags/my-ref*:refs/tags/my-ref*') 169 | }) 170 | 171 | it('getRefSpec refs/heads/ only', async () => { 172 | const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '') 173 | expect(refSpec.length).toBe(1) 174 | expect(refSpec[0]).toBe( 175 | '+refs/heads/my/branch:refs/remotes/origin/my/branch' 176 | ) 177 | }) 178 | 179 | it('getRefSpec refs/pull/ only', async () => { 180 | const refSpec = refHelper.getRefSpec('refs/pull/123/merge', '') 181 | expect(refSpec.length).toBe(1) 182 | expect(refSpec[0]).toBe('+refs/pull/123/merge:refs/remotes/pull/123/merge') 183 | }) 184 | 185 | it('getRefSpec refs/tags/ only', async () => { 186 | const refSpec = refHelper.getRefSpec('refs/tags/my-tag', '') 187 | expect(refSpec.length).toBe(1) 188 | expect(refSpec[0]).toBe('+refs/tags/my-tag:refs/tags/my-tag') 189 | }) 190 | }) 191 | -------------------------------------------------------------------------------- /__test__/retry-helper.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import {RetryHelper} from '../lib/retry-helper' 3 | 4 | let info: string[] 5 | let retryHelper: any 6 | 7 | describe('retry-helper tests', () => { 8 | beforeAll(() => { 9 | // Mock @actions/core info() 10 | jest.spyOn(core, 'info').mockImplementation((message: string) => { 11 | info.push(message) 12 | }) 13 | 14 | retryHelper = new RetryHelper(3, 0, 0) 15 | }) 16 | 17 | beforeEach(() => { 18 | // Reset info 19 | info = [] 20 | }) 21 | 22 | afterAll(() => { 23 | // Restore 24 | jest.restoreAllMocks() 25 | }) 26 | 27 | it('first attempt succeeds', async () => { 28 | const actual = await retryHelper.execute(async () => { 29 | return 'some result' 30 | }) 31 | expect(actual).toBe('some result') 32 | expect(info).toHaveLength(0) 33 | }) 34 | 35 | it('second attempt succeeds', async () => { 36 | let attempts = 0 37 | const actual = await retryHelper.execute(() => { 38 | if (++attempts == 1) { 39 | throw new Error('some error') 40 | } 41 | 42 | return Promise.resolve('some result') 43 | }) 44 | expect(attempts).toBe(2) 45 | expect(actual).toBe('some result') 46 | expect(info).toHaveLength(2) 47 | expect(info[0]).toBe('some error') 48 | expect(info[1]).toMatch(/Waiting .+ seconds before trying again/) 49 | }) 50 | 51 | it('third attempt succeeds', async () => { 52 | let attempts = 0 53 | const actual = await retryHelper.execute(() => { 54 | if (++attempts < 3) { 55 | throw new Error(`some error ${attempts}`) 56 | } 57 | 58 | return Promise.resolve('some result') 59 | }) 60 | expect(attempts).toBe(3) 61 | expect(actual).toBe('some result') 62 | expect(info).toHaveLength(4) 63 | expect(info[0]).toBe('some error 1') 64 | expect(info[1]).toMatch(/Waiting .+ seconds before trying again/) 65 | expect(info[2]).toBe('some error 2') 66 | expect(info[3]).toMatch(/Waiting .+ seconds before trying again/) 67 | }) 68 | 69 | it('all attempts fail succeeds', async () => { 70 | let attempts = 0 71 | let error: Error = null as unknown as Error 72 | try { 73 | await retryHelper.execute(() => { 74 | throw new Error(`some error ${++attempts}`) 75 | }) 76 | } catch (err) { 77 | error = err as Error 78 | } 79 | expect(error.message).toBe('some error 3') 80 | expect(attempts).toBe(3) 81 | expect(info).toHaveLength(4) 82 | expect(info[0]).toBe('some error 1') 83 | expect(info[1]).toMatch(/Waiting .+ seconds before trying again/) 84 | expect(info[2]).toBe('some error 2') 85 | expect(info[3]).toMatch(/Waiting .+ seconds before trying again/) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /__test__/url-helper.test.ts: -------------------------------------------------------------------------------- 1 | import * as urlHelper from '../src/url-helper' 2 | 3 | describe('getServerUrl tests', () => { 4 | it('basics', async () => { 5 | // Note that URL::toString will append a trailing / when passed just a domain name ... 6 | expect(urlHelper.getServerUrl().toString()).toBe('https://github.com/') 7 | expect(urlHelper.getServerUrl(' ').toString()).toBe('https://github.com/') 8 | expect(urlHelper.getServerUrl(' ').toString()).toBe('https://github.com/') 9 | expect(urlHelper.getServerUrl('http://contoso.com').toString()).toBe( 10 | 'http://contoso.com/' 11 | ) 12 | expect(urlHelper.getServerUrl('https://contoso.com').toString()).toBe( 13 | 'https://contoso.com/' 14 | ) 15 | expect(urlHelper.getServerUrl('https://contoso.com/').toString()).toBe( 16 | 'https://contoso.com/' 17 | ) 18 | 19 | // ... but can't make that same assumption when passed an URL that includes some deeper path. 20 | expect(urlHelper.getServerUrl('https://contoso.com/a/b').toString()).toBe( 21 | 'https://contoso.com/a/b' 22 | ) 23 | }) 24 | }) 25 | 26 | describe('isGhes tests', () => { 27 | const pristineEnv = process.env 28 | 29 | beforeEach(() => { 30 | jest.resetModules() 31 | process.env = {...pristineEnv} 32 | }) 33 | 34 | afterAll(() => { 35 | process.env = pristineEnv 36 | }) 37 | 38 | it('basics', async () => { 39 | delete process.env['GITHUB_SERVER_URL'] 40 | expect(urlHelper.isGhes()).toBeFalsy() 41 | expect(urlHelper.isGhes('https://github.com')).toBeFalsy() 42 | expect(urlHelper.isGhes('https://contoso.ghe.com')).toBeFalsy() 43 | expect(urlHelper.isGhes('https://test.github.localhost')).toBeFalsy() 44 | expect(urlHelper.isGhes('https://src.onpremise.fabrikam.com')).toBeTruthy() 45 | }) 46 | 47 | it('returns false when the GITHUB_SERVER_URL environment variable is not defined', async () => { 48 | delete process.env['GITHUB_SERVER_URL'] 49 | expect(urlHelper.isGhes()).toBeFalsy() 50 | }) 51 | 52 | it('returns false when the GITHUB_SERVER_URL environment variable is set to github.com', async () => { 53 | process.env['GITHUB_SERVER_URL'] = 'https://github.com' 54 | expect(urlHelper.isGhes()).toBeFalsy() 55 | }) 56 | 57 | it('returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL', async () => { 58 | process.env['GITHUB_SERVER_URL'] = 'https://contoso.ghe.com' 59 | expect(urlHelper.isGhes()).toBeFalsy() 60 | }) 61 | 62 | it('returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix', async () => { 63 | process.env['GITHUB_SERVER_URL'] = 'https://mock-github.localhost' 64 | expect(urlHelper.isGhes()).toBeFalsy() 65 | }) 66 | 67 | it('returns true when the GITHUB_SERVER_URL environment variable is set to some other URL', async () => { 68 | process.env['GITHUB_SERVER_URL'] = 'https://src.onpremise.fabrikam.com' 69 | expect(urlHelper.isGhes()).toBeTruthy() 70 | }) 71 | }) 72 | 73 | describe('getServerApiUrl tests', () => { 74 | it('basics', async () => { 75 | expect(urlHelper.getServerApiUrl()).toBe('https://api.github.com') 76 | expect(urlHelper.getServerApiUrl('https://github.com')).toBe( 77 | 'https://api.github.com' 78 | ) 79 | expect(urlHelper.getServerApiUrl('https://GitHub.com')).toBe( 80 | 'https://api.github.com' 81 | ) 82 | expect(urlHelper.getServerApiUrl('https://contoso.ghe.com')).toBe( 83 | 'https://api.contoso.ghe.com' 84 | ) 85 | expect(urlHelper.getServerApiUrl('https://fabrikam.GHE.COM')).toBe( 86 | 'https://api.fabrikam.ghe.com' 87 | ) 88 | expect( 89 | urlHelper.getServerApiUrl('https://src.onpremise.fabrikam.com') 90 | ).toBe('https://src.onpremise.fabrikam.com/api/v3') 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /__test__/verify-basic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -f "./basic/basic-file.txt" ]; then 4 | echo "Expected basic file does not exist" 5 | exit 1 6 | fi 7 | 8 | if [ "$1" = "--archive" ]; then 9 | # Verify no .git folder 10 | if [ -d "./basic/.git" ]; then 11 | echo "Did not expect ./basic/.git folder to exist" 12 | exit 1 13 | fi 14 | else 15 | # Verify .git folder 16 | if [ ! -d "./basic/.git" ]; then 17 | echo "Expected ./basic/.git folder to exist" 18 | exit 1 19 | fi 20 | 21 | # Verify that sparse-checkout is disabled. 22 | SPARSE_CHECKOUT_ENABLED=$(git -C ./basic config --local --get-all core.sparseCheckout) 23 | if [ "$SPARSE_CHECKOUT_ENABLED" != "" ]; then 24 | echo "Expected sparse-checkout to be disabled (discovered: $SPARSE_CHECKOUT_ENABLED)" 25 | exit 1 26 | fi 27 | 28 | # Verify git configuration shows worktreeConfig is effectively disabled 29 | WORKTREE_CONFIG_ENABLED=$(git -C ./basic config --local --get-all extensions.worktreeConfig) 30 | if [[ "$WORKTREE_CONFIG_ENABLED" != "" ]]; then 31 | echo "Expected extensions.worktreeConfig (boolean) to be disabled in git config. This could be an artifact of sparse checkout functionality." 32 | exit 1 33 | fi 34 | 35 | # Verify auth token 36 | cd basic 37 | git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main 38 | fi 39 | -------------------------------------------------------------------------------- /__test__/verify-clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$(git -C ./basic status --porcelain)" != "" ]]; then 4 | echo ---------------------------------------- 5 | echo git status 6 | echo ---------------------------------------- 7 | git status 8 | echo ---------------------------------------- 9 | echo git diff 10 | echo ---------------------------------------- 11 | git diff 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /__test__/verify-fetch-filter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Verify .git folder 4 | if [ ! -d "./fetch-filter/.git" ]; then 5 | echo "Expected ./fetch-filter/.git folder to exist" 6 | exit 1 7 | fi 8 | 9 | # Verify .git/config contains partialclonefilter 10 | 11 | CLONE_FILTER=$(git -C fetch-filter config --local --get remote.origin.partialclonefilter) 12 | 13 | if [ "$CLONE_FILTER" != "blob:none" ]; then 14 | echo "Expected ./fetch-filter/.git/config to have 'remote.origin.partialclonefilter' set to 'blob:none'" 15 | exit 1 16 | fi 17 | -------------------------------------------------------------------------------- /__test__/verify-lfs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "./lfs/regular-file.txt" ]; then 4 | echo "Expected regular file does not exist" 5 | exit 1 6 | fi 7 | 8 | if [ ! -f "./lfs/lfs-file.bin" ]; then 9 | echo "Expected lfs file does not exist" 10 | exit 1 11 | fi 12 | -------------------------------------------------------------------------------- /__test__/verify-no-unstaged-changes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$(git status --porcelain)" != "" ]]; then 4 | echo ---------------------------------------- 5 | echo git status 6 | echo ---------------------------------------- 7 | git status 8 | echo ---------------------------------------- 9 | echo git diff 10 | echo ---------------------------------------- 11 | git diff 12 | echo ---------------------------------------- 13 | echo Troubleshooting 14 | echo ---------------------------------------- 15 | echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run format && npm run build" 16 | exit 1 17 | fi 18 | -------------------------------------------------------------------------------- /__test__/verify-side-by-side.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "./side-by-side-1/side-by-side-test-file-1.txt" ]; then 4 | echo "Expected file 1 does not exist" 5 | exit 1 6 | fi 7 | 8 | if [ ! -f "./side-by-side-2/side-by-side-test-file-2.txt" ]; then 9 | echo "Expected file 2 does not exist" 10 | exit 1 11 | fi 12 | -------------------------------------------------------------------------------- /__test__/verify-sparse-checkout-non-cone-mode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Verify .git folder 4 | if [ ! -d "./sparse-checkout-non-cone-mode/.git" ]; then 5 | echo "Expected ./sparse-checkout-non-cone-mode/.git folder to exist" 6 | exit 1 7 | fi 8 | 9 | # Verify sparse-checkout (non-cone-mode) 10 | cd sparse-checkout-non-cone-mode 11 | 12 | ENABLED=$(git config --local --get-all core.sparseCheckout) 13 | 14 | if [ "$?" != "0" ]; then 15 | echo "Failed to verify that sparse-checkout is enabled" 16 | exit 1 17 | fi 18 | 19 | # Check that sparse-checkout is enabled 20 | if [ "$ENABLED" != "true" ]; then 21 | echo "Expected sparse-checkout to be enabled (is: $ENABLED)" 22 | exit 1 23 | fi 24 | 25 | SPARSE_CHECKOUT_FILE=$(git rev-parse --git-path info/sparse-checkout) 26 | 27 | if [ "$?" != "0" ]; then 28 | echo "Failed to validate sparse-checkout" 29 | exit 1 30 | fi 31 | 32 | # Check that sparse-checkout list is not empty 33 | if [ ! -f "$SPARSE_CHECKOUT_FILE" ]; then 34 | echo "Expected sparse-checkout file to exist" 35 | exit 1 36 | fi 37 | 38 | # Check that all folders from sparse-checkout exists 39 | for pattern in $(cat "$SPARSE_CHECKOUT_FILE") 40 | do 41 | if [ ! -d "${pattern#/}" ]; then 42 | echo "Expected directory '${pattern#/}' to exist" 43 | exit 1 44 | fi 45 | done 46 | 47 | # Verify that the root directory is not checked out 48 | if [ -f README.md ]; then 49 | echo "Expected top-level files not to exist" 50 | exit 1 51 | fi -------------------------------------------------------------------------------- /__test__/verify-sparse-checkout.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Verify .git folder 4 | if [ ! -d "./sparse-checkout/.git" ]; then 5 | echo "Expected ./sparse-checkout/.git folder to exist" 6 | exit 1 7 | fi 8 | 9 | # Verify sparse-checkout 10 | cd sparse-checkout 11 | 12 | SPARSE=$(git sparse-checkout list) 13 | 14 | if [ "$?" != "0" ]; then 15 | echo "Failed to validate sparse-checkout" 16 | exit 1 17 | fi 18 | 19 | # Check that sparse-checkout list is not empty 20 | if [ -z "$SPARSE" ]; then 21 | echo "Expected sparse-checkout list to not be empty" 22 | exit 1 23 | fi 24 | 25 | # Check that all folders of the sparse checkout exist 26 | for pattern in $SPARSE 27 | do 28 | if [ ! -d "$pattern" ]; then 29 | echo "Expected directory '$pattern' to exist" 30 | exit 1 31 | fi 32 | done 33 | 34 | checkSparse () { 35 | if [ ! -d "./$1" ]; then 36 | echo "Expected directory '$1' to exist" 37 | exit 1 38 | fi 39 | 40 | for file in $(git ls-tree -r --name-only HEAD $1) 41 | do 42 | if [ ! -f "$file" ]; then 43 | echo "Expected file '$file' to exist" 44 | exit 1 45 | fi 46 | done 47 | } 48 | 49 | # Check that all folders and their children have been checked out 50 | checkSparse __test__ 51 | checkSparse .github 52 | checkSparse dist 53 | 54 | # Check that only sparse-checkout folders have been checked out 55 | for pattern in $(git ls-tree --name-only HEAD) 56 | do 57 | if [ -d "$pattern" ]; then 58 | if [[ "$pattern" != "__test__" && "$pattern" != ".github" && "$pattern" != "dist" ]]; then 59 | echo "Expected directory '$pattern' to not exist" 60 | exit 1 61 | fi 62 | fi 63 | done -------------------------------------------------------------------------------- /__test__/verify-submodules-false.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "./submodules-false/regular-file.txt" ]; then 4 | echo "Expected regular file does not exist" 5 | exit 1 6 | fi 7 | 8 | if [ -f "./submodules-false/submodule-level-1/submodule-file.txt" ]; then 9 | echo "Unexpected submodule file exists" 10 | exit 1 11 | fi -------------------------------------------------------------------------------- /__test__/verify-submodules-recursive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "./submodules-recursive/regular-file.txt" ]; then 4 | echo "Expected regular file does not exist" 5 | exit 1 6 | fi 7 | 8 | if [ ! -f "./submodules-recursive/submodule-level-1/submodule-file.txt" ]; then 9 | echo "Expected submodule file does not exist" 10 | exit 1 11 | fi 12 | 13 | if [ ! -f "./submodules-recursive/submodule-level-1/submodule-level-2/nested-submodule-file.txt" ]; then 14 | echo "Expected nested submodule file does not exists" 15 | exit 1 16 | fi 17 | 18 | echo "Testing persisted credential" 19 | pushd ./submodules-recursive/submodule-level-1/submodule-level-2 20 | git config --local --name-only --get-regexp http.+extraheader && git fetch 21 | if [ "$?" != "0" ]; then 22 | echo "Failed to validate persisted credential" 23 | popd 24 | exit 1 25 | fi 26 | popd 27 | -------------------------------------------------------------------------------- /__test__/verify-submodules-true.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "./submodules-true/regular-file.txt" ]; then 4 | echo "Expected regular file does not exist" 5 | exit 1 6 | fi 7 | 8 | if [ ! -f "./submodules-true/submodule-level-1/submodule-file.txt" ]; then 9 | echo "Expected submodule file does not exist" 10 | exit 1 11 | fi 12 | 13 | if [ -f "./submodules-true/submodule-level-1/submodule-level-2/nested-submodule-file.txt" ]; then 14 | echo "Unexpected nested submodule file exists" 15 | exit 1 16 | fi 17 | 18 | echo "Testing persisted credential" 19 | pushd ./submodules-true/submodule-level-1 20 | git config --local --name-only --get-regexp http.+extraheader && git fetch 21 | if [ "$?" != "0" ]; then 22 | echo "Failed to validate persisted credential" 23 | popd 24 | exit 1 25 | fi 26 | popd 27 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Checkout' 2 | description: 'Checkout a Git repository at a particular version' 3 | inputs: 4 | repository: 5 | description: 'Repository name with owner. For example, actions/checkout' 6 | default: ${{ github.repository }} 7 | ref: 8 | description: > 9 | The branch, tag or SHA to checkout. When checking out the repository that 10 | triggered a workflow, this defaults to the reference or SHA for that 11 | event. Otherwise, uses the default branch. 12 | token: 13 | description: > 14 | Personal access token (PAT) used to fetch the repository. The PAT is configured 15 | with the local git config, which enables your scripts to run authenticated git 16 | commands. The post-job step removes the PAT. 17 | 18 | 19 | We recommend using a service account with the least permissions necessary. 20 | Also when generating a new PAT, select the least scopes necessary. 21 | 22 | 23 | [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) 24 | default: ${{ github.token }} 25 | ssh-key: 26 | description: > 27 | SSH key used to fetch the repository. The SSH key is configured with the local 28 | git config, which enables your scripts to run authenticated git commands. 29 | The post-job step removes the SSH key. 30 | 31 | 32 | We recommend using a service account with the least permissions necessary. 33 | 34 | 35 | [Learn more about creating and using 36 | encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) 37 | ssh-known-hosts: 38 | description: > 39 | Known hosts in addition to the user and global host key database. The public 40 | SSH keys for a host may be obtained using the utility `ssh-keyscan`. For example, 41 | `ssh-keyscan github.com`. The public key for github.com is always implicitly added. 42 | ssh-strict: 43 | description: > 44 | Whether to perform strict host key checking. When true, adds the options `StrictHostKeyChecking=yes` 45 | and `CheckHostIP=no` to the SSH command line. Use the input `ssh-known-hosts` to 46 | configure additional hosts. 47 | default: true 48 | ssh-user: 49 | description: > 50 | The user to use when connecting to the remote SSH host. By default 'git' is used. 51 | default: git 52 | persist-credentials: 53 | description: 'Whether to configure the token or SSH key with the local git config' 54 | default: true 55 | path: 56 | description: 'Relative path under $GITHUB_WORKSPACE to place the repository' 57 | clean: 58 | description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching' 59 | default: true 60 | filter: 61 | description: > 62 | Partially clone against a given filter. 63 | Overrides sparse-checkout if set. 64 | default: null 65 | sparse-checkout: 66 | description: > 67 | Do a sparse checkout on given patterns. 68 | Each pattern should be separated with new lines. 69 | default: null 70 | sparse-checkout-cone-mode: 71 | description: > 72 | Specifies whether to use cone-mode when doing a sparse checkout. 73 | default: true 74 | fetch-depth: 75 | description: 'Number of commits to fetch. 0 indicates all history for all branches and tags.' 76 | default: 1 77 | fetch-tags: 78 | description: 'Whether to fetch tags, even if fetch-depth > 0.' 79 | default: false 80 | show-progress: 81 | description: 'Whether to show progress status output when fetching.' 82 | default: true 83 | lfs: 84 | description: 'Whether to download Git-LFS files' 85 | default: false 86 | submodules: 87 | description: > 88 | Whether to checkout submodules: `true` to checkout submodules or `recursive` to 89 | recursively checkout submodules. 90 | 91 | 92 | When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are 93 | converted to HTTPS. 94 | default: false 95 | set-safe-directory: 96 | description: Add repository path as safe.directory for Git global config by running `git config --global --add safe.directory ` 97 | default: true 98 | github-server-url: 99 | description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com 100 | required: false 101 | outputs: 102 | ref: 103 | description: 'The branch, tag or SHA that was checked out' 104 | commit: 105 | description: 'The commit SHA that was checked out' 106 | runs: 107 | using: node20 108 | main: dist/index.js 109 | post: dist/index.js 110 | -------------------------------------------------------------------------------- /dist/problem-matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "checkout-git", 5 | "pattern": [ 6 | { 7 | "regexp": "^(fatal|error): (.*)$", 8 | "message": 2 9 | } 10 | ] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /images/test-ubuntu-git.Dockerfile: -------------------------------------------------------------------------------- 1 | # Defines the test-ubuntu-git Container Image. 2 | # Consumed by actions/checkout CI/CD validation workflows. 3 | 4 | FROM ubuntu:latest 5 | 6 | RUN apt update 7 | RUN apt install -y git 8 | 9 | LABEL org.opencontainers.image.title="Ubuntu + git (validation image)" 10 | LABEL org.opencontainers.image.description="Ubuntu image with git pre-installed. Intended primarily for testing `actions/checkout` during CI/CD workflows." 11 | LABEL org.opencontainers.image.documentation="https://github.com/actions/checkout/tree/main/images/test-ubuntu-git.md" 12 | LABEL org.opencontainers.image.licenses=MIT 13 | -------------------------------------------------------------------------------- /images/test-ubuntu-git.md: -------------------------------------------------------------------------------- 1 | # `test-ubuntu-git` Container Image 2 | 3 | [![Publish test-ubuntu-git Container](https://github.com/actions/checkout/actions/workflows/update-test-ubuntu-git.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/update-test-ubuntu-git.yml) 4 | 5 | ## Purpose 6 | 7 | `test-ubuntu-git` is a container image hosted on the GitHub Container Registry, `ghcr.io`. 8 | 9 | It is intended primarily for testing the [`actions/checkout` repository](https://github.com/actions/checkout) as part of `actions/checkout`'s CI/CD workflows. 10 | 11 | The composition of `test-ubuntu-git` is intentionally minimal. It is comprised of [git](https://git-scm.com/) installed on top of a [base-level ubuntu image](https://hub.docker.com/_/ubuntu/tags). 12 | 13 | # License 14 | 15 | `test-ubuntu-git` is released under the [MIT License](/LICENSE). 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | fakeTimers: {}, 4 | moduleFileExtensions: ['js', 'ts'], 5 | testEnvironment: 'node', 6 | testMatch: ['**/*.test.ts'], 7 | testRunner: 'jest-circus/runner', 8 | transform: { 9 | '^.+\\.ts$': 'ts-jest' 10 | }, 11 | verbose: true 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "checkout", 3 | "version": "4.2.2", 4 | "description": "checkout action", 5 | "main": "lib/main.js", 6 | "scripts": { 7 | "build": "tsc && ncc build && node lib/misc/generate-docs.js", 8 | "format": "prettier --write '**/*.ts'", 9 | "format-check": "prettier --check '**/*.ts'", 10 | "lint": "eslint src/**/*.ts", 11 | "test": "jest", 12 | "licensed-check": "src/misc/licensed-check.sh", 13 | "licensed-generate": "src/misc/licensed-generate.sh" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/actions/checkout.git" 18 | }, 19 | "keywords": [ 20 | "github", 21 | "actions", 22 | "checkout" 23 | ], 24 | "author": "GitHub", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/actions/checkout/issues" 28 | }, 29 | "homepage": "https://github.com/actions/checkout#readme", 30 | "dependencies": { 31 | "@actions/core": "^1.10.1", 32 | "@actions/exec": "^1.1.1", 33 | "@actions/github": "^6.0.0", 34 | "@actions/io": "^1.1.3", 35 | "@actions/tool-cache": "^2.0.1", 36 | "uuid": "^9.0.1" 37 | }, 38 | "devDependencies": { 39 | "@types/jest": "^29.5.12", 40 | "@types/node": "^20.12.12", 41 | "@types/uuid": "^9.0.8", 42 | "@typescript-eslint/eslint-plugin": "^7.9.0", 43 | "@typescript-eslint/parser": "^7.9.0", 44 | "@vercel/ncc": "^0.38.1", 45 | "eslint": "^8.57.0", 46 | "eslint-plugin-github": "^4.10.2", 47 | "eslint-plugin-jest": "^28.8.2", 48 | "jest": "^29.7.0", 49 | "jest-circus": "^29.7.0", 50 | "js-yaml": "^4.1.0", 51 | "prettier": "^3.3.3", 52 | "ts-jest": "^29.2.5", 53 | "typescript": "^5.5.4" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/fs-helper.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | 3 | export function directoryExistsSync(path: string, required?: boolean): boolean { 4 | if (!path) { 5 | throw new Error("Arg 'path' must not be empty") 6 | } 7 | 8 | let stats: fs.Stats 9 | try { 10 | stats = fs.statSync(path) 11 | } catch (error) { 12 | if ((error as any)?.code === 'ENOENT') { 13 | if (!required) { 14 | return false 15 | } 16 | 17 | throw new Error(`Directory '${path}' does not exist`) 18 | } 19 | 20 | throw new Error( 21 | `Encountered an error when checking whether path '${path}' exists: ${ 22 | (error as any)?.message ?? error 23 | }` 24 | ) 25 | } 26 | 27 | if (stats.isDirectory()) { 28 | return true 29 | } else if (!required) { 30 | return false 31 | } 32 | 33 | throw new Error(`Directory '${path}' does not exist`) 34 | } 35 | 36 | export function existsSync(path: string): boolean { 37 | if (!path) { 38 | throw new Error("Arg 'path' must not be empty") 39 | } 40 | 41 | try { 42 | fs.statSync(path) 43 | } catch (error) { 44 | if ((error as any)?.code === 'ENOENT') { 45 | return false 46 | } 47 | 48 | throw new Error( 49 | `Encountered an error when checking whether path '${path}' exists: ${ 50 | (error as any)?.message ?? error 51 | }` 52 | ) 53 | } 54 | 55 | return true 56 | } 57 | 58 | export function fileExistsSync(path: string): boolean { 59 | if (!path) { 60 | throw new Error("Arg 'path' must not be empty") 61 | } 62 | 63 | let stats: fs.Stats 64 | try { 65 | stats = fs.statSync(path) 66 | } catch (error) { 67 | if ((error as any)?.code === 'ENOENT') { 68 | return false 69 | } 70 | 71 | throw new Error( 72 | `Encountered an error when checking whether path '${path}' exists: ${ 73 | (error as any)?.message ?? error 74 | }` 75 | ) 76 | } 77 | 78 | if (!stats.isDirectory()) { 79 | return true 80 | } 81 | 82 | return false 83 | } 84 | -------------------------------------------------------------------------------- /src/git-directory-helper.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as core from '@actions/core' 3 | import * as fs from 'fs' 4 | import * as fsHelper from './fs-helper' 5 | import * as io from '@actions/io' 6 | import * as path from 'path' 7 | import {IGitCommandManager} from './git-command-manager' 8 | 9 | export async function prepareExistingDirectory( 10 | git: IGitCommandManager | undefined, 11 | repositoryPath: string, 12 | repositoryUrl: string, 13 | clean: boolean, 14 | ref: string 15 | ): Promise { 16 | assert.ok(repositoryPath, 'Expected repositoryPath to be defined') 17 | assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined') 18 | 19 | // Indicates whether to delete the directory contents 20 | let remove = false 21 | 22 | // Check whether using git or REST API 23 | if (!git) { 24 | remove = true 25 | } 26 | // Fetch URL does not match 27 | else if ( 28 | !fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) || 29 | repositoryUrl !== (await git.tryGetFetchUrl()) 30 | ) { 31 | remove = true 32 | } else { 33 | // Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process 34 | const lockPaths = [ 35 | path.join(repositoryPath, '.git', 'index.lock'), 36 | path.join(repositoryPath, '.git', 'shallow.lock') 37 | ] 38 | for (const lockPath of lockPaths) { 39 | try { 40 | await io.rmRF(lockPath) 41 | } catch (error) { 42 | core.debug( 43 | `Unable to delete '${lockPath}'. ${(error as any)?.message ?? error}` 44 | ) 45 | } 46 | } 47 | 48 | try { 49 | core.startGroup('Removing previously created refs, to avoid conflicts') 50 | // Checkout detached HEAD 51 | if (!(await git.isDetached())) { 52 | await git.checkoutDetach() 53 | } 54 | 55 | // Remove all refs/heads/* 56 | let branches = await git.branchList(false) 57 | for (const branch of branches) { 58 | await git.branchDelete(false, branch) 59 | } 60 | 61 | // Remove any conflicting refs/remotes/origin/* 62 | // Example 1: Consider ref is refs/heads/foo and previously fetched refs/remotes/origin/foo/bar 63 | // Example 2: Consider ref is refs/heads/foo/bar and previously fetched refs/remotes/origin/foo 64 | if (ref) { 65 | ref = ref.startsWith('refs/') ? ref : `refs/heads/${ref}` 66 | if (ref.startsWith('refs/heads/')) { 67 | const upperName1 = ref.toUpperCase().substr('REFS/HEADS/'.length) 68 | const upperName1Slash = `${upperName1}/` 69 | branches = await git.branchList(true) 70 | for (const branch of branches) { 71 | const upperName2 = branch.substr('origin/'.length).toUpperCase() 72 | const upperName2Slash = `${upperName2}/` 73 | if ( 74 | upperName1.startsWith(upperName2Slash) || 75 | upperName2.startsWith(upperName1Slash) 76 | ) { 77 | await git.branchDelete(true, branch) 78 | } 79 | } 80 | } 81 | } 82 | core.endGroup() 83 | 84 | // Check for submodules and delete any existing files if submodules are present 85 | if (!(await git.submoduleStatus())) { 86 | remove = true 87 | core.info('Bad Submodules found, removing existing files') 88 | } 89 | 90 | // Clean 91 | if (clean) { 92 | core.startGroup('Cleaning the repository') 93 | if (!(await git.tryClean())) { 94 | core.debug( 95 | `The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For further investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.` 96 | ) 97 | remove = true 98 | } else if (!(await git.tryReset())) { 99 | remove = true 100 | } 101 | core.endGroup() 102 | 103 | if (remove) { 104 | core.warning( 105 | `Unable to clean or reset the repository. The repository will be recreated instead.` 106 | ) 107 | } 108 | } 109 | } catch (error) { 110 | core.warning( 111 | `Unable to prepare the existing repository. The repository will be recreated instead.` 112 | ) 113 | remove = true 114 | } 115 | } 116 | 117 | if (remove) { 118 | // Delete the contents of the directory. Don't delete the directory itself 119 | // since it might be the current working directory. 120 | core.info(`Deleting the contents of '${repositoryPath}'`) 121 | for (const file of await fs.promises.readdir(repositoryPath)) { 122 | await io.rmRF(path.join(repositoryPath, file)) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/git-source-provider.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as fsHelper from './fs-helper' 3 | import * as gitAuthHelper from './git-auth-helper' 4 | import * as gitCommandManager from './git-command-manager' 5 | import * as gitDirectoryHelper from './git-directory-helper' 6 | import * as githubApiHelper from './github-api-helper' 7 | import * as io from '@actions/io' 8 | import * as path from 'path' 9 | import * as refHelper from './ref-helper' 10 | import * as stateHelper from './state-helper' 11 | import * as urlHelper from './url-helper' 12 | import { 13 | MinimumGitSparseCheckoutVersion, 14 | IGitCommandManager 15 | } from './git-command-manager' 16 | import {IGitSourceSettings} from './git-source-settings' 17 | 18 | export async function getSource(settings: IGitSourceSettings): Promise { 19 | // Repository URL 20 | core.info( 21 | `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}` 22 | ) 23 | const repositoryUrl = urlHelper.getFetchUrl(settings) 24 | 25 | // Remove conflicting file path 26 | if (fsHelper.fileExistsSync(settings.repositoryPath)) { 27 | await io.rmRF(settings.repositoryPath) 28 | } 29 | 30 | // Create directory 31 | let isExisting = true 32 | if (!fsHelper.directoryExistsSync(settings.repositoryPath)) { 33 | isExisting = false 34 | await io.mkdirP(settings.repositoryPath) 35 | } 36 | 37 | // Git command manager 38 | core.startGroup('Getting Git version info') 39 | const git = await getGitCommandManager(settings) 40 | core.endGroup() 41 | 42 | let authHelper: gitAuthHelper.IGitAuthHelper | null = null 43 | try { 44 | if (git) { 45 | authHelper = gitAuthHelper.createAuthHelper(git, settings) 46 | if (settings.setSafeDirectory) { 47 | // Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail 48 | // Otherwise all git commands we run in a container fail 49 | await authHelper.configureTempGlobalConfig() 50 | core.info( 51 | `Adding repository directory to the temporary git global config as a safe directory` 52 | ) 53 | 54 | await git 55 | .config('safe.directory', settings.repositoryPath, true, true) 56 | .catch(error => { 57 | core.info( 58 | `Failed to initialize safe directory with error: ${error}` 59 | ) 60 | }) 61 | 62 | stateHelper.setSafeDirectory() 63 | } 64 | } 65 | 66 | // Prepare existing directory, otherwise recreate 67 | if (isExisting) { 68 | await gitDirectoryHelper.prepareExistingDirectory( 69 | git, 70 | settings.repositoryPath, 71 | repositoryUrl, 72 | settings.clean, 73 | settings.ref 74 | ) 75 | } 76 | 77 | if (!git) { 78 | // Downloading using REST API 79 | core.info(`The repository will be downloaded using the GitHub REST API`) 80 | core.info( 81 | `To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH` 82 | ) 83 | if (settings.submodules) { 84 | throw new Error( 85 | `Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.` 86 | ) 87 | } else if (settings.sshKey) { 88 | throw new Error( 89 | `Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.` 90 | ) 91 | } 92 | 93 | await githubApiHelper.downloadRepository( 94 | settings.authToken, 95 | settings.repositoryOwner, 96 | settings.repositoryName, 97 | settings.ref, 98 | settings.commit, 99 | settings.repositoryPath, 100 | settings.githubServerUrl 101 | ) 102 | return 103 | } 104 | 105 | // Save state for POST action 106 | stateHelper.setRepositoryPath(settings.repositoryPath) 107 | 108 | // Initialize the repository 109 | if ( 110 | !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git')) 111 | ) { 112 | core.startGroup('Initializing the repository') 113 | await git.init() 114 | await git.remoteAdd('origin', repositoryUrl) 115 | core.endGroup() 116 | } 117 | 118 | // Disable automatic garbage collection 119 | core.startGroup('Disabling automatic garbage collection') 120 | if (!(await git.tryDisableAutomaticGarbageCollection())) { 121 | core.warning( 122 | `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.` 123 | ) 124 | } 125 | core.endGroup() 126 | 127 | // If we didn't initialize it above, do it now 128 | if (!authHelper) { 129 | authHelper = gitAuthHelper.createAuthHelper(git, settings) 130 | } 131 | // Configure auth 132 | core.startGroup('Setting up auth') 133 | await authHelper.configureAuth() 134 | core.endGroup() 135 | 136 | // Determine the default branch 137 | if (!settings.ref && !settings.commit) { 138 | core.startGroup('Determining the default branch') 139 | if (settings.sshKey) { 140 | settings.ref = await git.getDefaultBranch(repositoryUrl) 141 | } else { 142 | settings.ref = await githubApiHelper.getDefaultBranch( 143 | settings.authToken, 144 | settings.repositoryOwner, 145 | settings.repositoryName, 146 | settings.githubServerUrl 147 | ) 148 | } 149 | core.endGroup() 150 | } 151 | 152 | // LFS install 153 | if (settings.lfs) { 154 | await git.lfsInstall() 155 | } 156 | 157 | // Fetch 158 | core.startGroup('Fetching the repository') 159 | const fetchOptions: { 160 | filter?: string 161 | fetchDepth?: number 162 | fetchTags?: boolean 163 | showProgress?: boolean 164 | } = {} 165 | 166 | if (settings.filter) { 167 | fetchOptions.filter = settings.filter 168 | } else if (settings.sparseCheckout) { 169 | fetchOptions.filter = 'blob:none' 170 | } 171 | 172 | if (settings.fetchDepth <= 0) { 173 | // Fetch all branches and tags 174 | let refSpec = refHelper.getRefSpecForAllHistory( 175 | settings.ref, 176 | settings.commit 177 | ) 178 | await git.fetch(refSpec, fetchOptions) 179 | 180 | // When all history is fetched, the ref we're interested in may have moved to a different 181 | // commit (push or force push). If so, fetch again with a targeted refspec. 182 | if (!(await refHelper.testRef(git, settings.ref, settings.commit))) { 183 | refSpec = refHelper.getRefSpec(settings.ref, settings.commit) 184 | await git.fetch(refSpec, fetchOptions) 185 | } 186 | } else { 187 | fetchOptions.fetchDepth = settings.fetchDepth 188 | fetchOptions.fetchTags = settings.fetchTags 189 | const refSpec = refHelper.getRefSpec(settings.ref, settings.commit) 190 | await git.fetch(refSpec, fetchOptions) 191 | } 192 | core.endGroup() 193 | 194 | // Checkout info 195 | core.startGroup('Determining the checkout info') 196 | const checkoutInfo = await refHelper.getCheckoutInfo( 197 | git, 198 | settings.ref, 199 | settings.commit 200 | ) 201 | core.endGroup() 202 | 203 | // LFS fetch 204 | // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time). 205 | // Explicit lfs fetch will fetch lfs objects in parallel. 206 | // For sparse checkouts, let `checkout` fetch the needed objects lazily. 207 | if (settings.lfs && !settings.sparseCheckout) { 208 | core.startGroup('Fetching LFS objects') 209 | await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref) 210 | core.endGroup() 211 | } 212 | 213 | // Sparse checkout 214 | if (!settings.sparseCheckout) { 215 | let gitVersion = await git.version() 216 | // no need to disable sparse-checkout if the installed git runtime doesn't even support it. 217 | if (gitVersion.checkMinimum(MinimumGitSparseCheckoutVersion)) { 218 | await git.disableSparseCheckout() 219 | } 220 | } else { 221 | core.startGroup('Setting up sparse checkout') 222 | if (settings.sparseCheckoutConeMode) { 223 | await git.sparseCheckout(settings.sparseCheckout) 224 | } else { 225 | await git.sparseCheckoutNonConeMode(settings.sparseCheckout) 226 | } 227 | core.endGroup() 228 | } 229 | 230 | // Checkout 231 | core.startGroup('Checking out the ref') 232 | await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint) 233 | core.endGroup() 234 | 235 | // Submodules 236 | if (settings.submodules) { 237 | // Temporarily override global config 238 | core.startGroup('Setting up auth for fetching submodules') 239 | await authHelper.configureGlobalAuth() 240 | core.endGroup() 241 | 242 | // Checkout submodules 243 | core.startGroup('Fetching submodules') 244 | await git.submoduleSync(settings.nestedSubmodules) 245 | await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules) 246 | await git.submoduleForeach( 247 | 'git config --local gc.auto 0', 248 | settings.nestedSubmodules 249 | ) 250 | core.endGroup() 251 | 252 | // Persist credentials 253 | if (settings.persistCredentials) { 254 | core.startGroup('Persisting credentials for submodules') 255 | await authHelper.configureSubmoduleAuth() 256 | core.endGroup() 257 | } 258 | } 259 | 260 | // Get commit information 261 | const commitInfo = await git.log1() 262 | 263 | // Log commit sha 264 | const commitSHA = await git.log1('--format=%H') 265 | core.setOutput('commit', commitSHA.trim()) 266 | 267 | // Check for incorrect pull request merge commit 268 | await refHelper.checkCommitInfo( 269 | settings.authToken, 270 | commitInfo, 271 | settings.repositoryOwner, 272 | settings.repositoryName, 273 | settings.ref, 274 | settings.commit, 275 | settings.githubServerUrl 276 | ) 277 | } finally { 278 | // Remove auth 279 | if (authHelper) { 280 | if (!settings.persistCredentials) { 281 | core.startGroup('Removing auth') 282 | await authHelper.removeAuth() 283 | core.endGroup() 284 | } 285 | authHelper.removeGlobalConfig() 286 | } 287 | } 288 | } 289 | 290 | export async function cleanup(repositoryPath: string): Promise { 291 | // Repo exists? 292 | if ( 293 | !repositoryPath || 294 | !fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config')) 295 | ) { 296 | return 297 | } 298 | 299 | let git: IGitCommandManager 300 | try { 301 | git = await gitCommandManager.createCommandManager( 302 | repositoryPath, 303 | false, 304 | false 305 | ) 306 | } catch { 307 | return 308 | } 309 | 310 | // Remove auth 311 | const authHelper = gitAuthHelper.createAuthHelper(git) 312 | try { 313 | if (stateHelper.PostSetSafeDirectory) { 314 | // Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail 315 | // Otherwise all git commands we run in a container fail 316 | await authHelper.configureTempGlobalConfig() 317 | core.info( 318 | `Adding repository directory to the temporary git global config as a safe directory` 319 | ) 320 | 321 | await git 322 | .config('safe.directory', repositoryPath, true, true) 323 | .catch(error => { 324 | core.info(`Failed to initialize safe directory with error: ${error}`) 325 | }) 326 | } 327 | 328 | await authHelper.removeAuth() 329 | } finally { 330 | await authHelper.removeGlobalConfig() 331 | } 332 | } 333 | 334 | async function getGitCommandManager( 335 | settings: IGitSourceSettings 336 | ): Promise { 337 | core.info(`Working directory is '${settings.repositoryPath}'`) 338 | try { 339 | return await gitCommandManager.createCommandManager( 340 | settings.repositoryPath, 341 | settings.lfs, 342 | settings.sparseCheckout != null 343 | ) 344 | } catch (err) { 345 | // Git is required for LFS 346 | if (settings.lfs) { 347 | throw err 348 | } 349 | 350 | // Otherwise fallback to REST API 351 | return undefined 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/git-source-settings.ts: -------------------------------------------------------------------------------- 1 | export interface IGitSourceSettings { 2 | /** 3 | * The location on disk where the repository will be placed 4 | */ 5 | repositoryPath: string 6 | 7 | /** 8 | * The repository owner 9 | */ 10 | repositoryOwner: string 11 | 12 | /** 13 | * The repository name 14 | */ 15 | repositoryName: string 16 | 17 | /** 18 | * The ref to fetch 19 | */ 20 | ref: string 21 | 22 | /** 23 | * The commit to checkout 24 | */ 25 | commit: string 26 | 27 | /** 28 | * Indicates whether to clean the repository 29 | */ 30 | clean: boolean 31 | 32 | /** 33 | * The filter determining which objects to include 34 | */ 35 | filter: string | undefined 36 | 37 | /** 38 | * The array of folders to make the sparse checkout 39 | */ 40 | sparseCheckout: string[] 41 | 42 | /** 43 | * Indicates whether to use cone mode in the sparse checkout (if any) 44 | */ 45 | sparseCheckoutConeMode: boolean 46 | 47 | /** 48 | * The depth when fetching 49 | */ 50 | fetchDepth: number 51 | 52 | /** 53 | * Fetch tags, even if fetchDepth > 0 (default: false) 54 | */ 55 | fetchTags: boolean 56 | 57 | /** 58 | * Indicates whether to use the --progress option when fetching 59 | */ 60 | showProgress: boolean 61 | 62 | /** 63 | * Indicates whether to fetch LFS objects 64 | */ 65 | lfs: boolean 66 | 67 | /** 68 | * Indicates whether to checkout submodules 69 | */ 70 | submodules: boolean 71 | 72 | /** 73 | * Indicates whether to recursively checkout submodules 74 | */ 75 | nestedSubmodules: boolean 76 | 77 | /** 78 | * The auth token to use when fetching the repository 79 | */ 80 | authToken: string 81 | 82 | /** 83 | * The SSH key to configure 84 | */ 85 | sshKey: string 86 | 87 | /** 88 | * Additional SSH known hosts 89 | */ 90 | sshKnownHosts: string 91 | 92 | /** 93 | * Indicates whether the server must be a known host 94 | */ 95 | sshStrict: boolean 96 | 97 | /** 98 | * The SSH user to login as 99 | */ 100 | sshUser: string 101 | 102 | /** 103 | * Indicates whether to persist the credentials on disk to enable scripting authenticated git commands 104 | */ 105 | persistCredentials: boolean 106 | 107 | /** 108 | * Organization ID for the currently running workflow (used for auth settings) 109 | */ 110 | workflowOrganizationId: number | undefined 111 | 112 | /** 113 | * Indicates whether to add repositoryPath as safe.directory in git global config 114 | */ 115 | setSafeDirectory: boolean 116 | 117 | /** 118 | * User override on the GitHub Server/Host URL that hosts the repository to be cloned 119 | */ 120 | githubServerUrl: string | undefined 121 | } 122 | -------------------------------------------------------------------------------- /src/git-version.ts: -------------------------------------------------------------------------------- 1 | export class GitVersion { 2 | private readonly major: number = NaN 3 | private readonly minor: number = NaN 4 | private readonly patch: number = NaN 5 | 6 | /** 7 | * Used for comparing the version of git and git-lfs against the minimum required version 8 | * @param version the version string, e.g. 1.2 or 1.2.3 9 | */ 10 | constructor(version?: string) { 11 | if (version) { 12 | const match = version.match(/^(\d+)\.(\d+)(\.(\d+))?$/) 13 | if (match) { 14 | this.major = Number(match[1]) 15 | this.minor = Number(match[2]) 16 | if (match[4]) { 17 | this.patch = Number(match[4]) 18 | } 19 | } 20 | } 21 | } 22 | 23 | /** 24 | * Compares the instance against a minimum required version 25 | * @param minimum Minimum version 26 | */ 27 | checkMinimum(minimum: GitVersion): boolean { 28 | if (!minimum.isValid()) { 29 | throw new Error('Arg minimum is not a valid version') 30 | } 31 | 32 | // Major is insufficient 33 | if (this.major < minimum.major) { 34 | return false 35 | } 36 | 37 | // Major is equal 38 | if (this.major === minimum.major) { 39 | // Minor is insufficient 40 | if (this.minor < minimum.minor) { 41 | return false 42 | } 43 | 44 | // Minor is equal 45 | if (this.minor === minimum.minor) { 46 | // Patch is insufficient 47 | if (this.patch && this.patch < (minimum.patch || 0)) { 48 | return false 49 | } 50 | } 51 | } 52 | 53 | return true 54 | } 55 | 56 | /** 57 | * Indicates whether the instance was constructed from a valid version string 58 | */ 59 | isValid(): boolean { 60 | return !isNaN(this.major) 61 | } 62 | 63 | /** 64 | * Returns the version as a string, e.g. 1.2 or 1.2.3 65 | */ 66 | toString(): string { 67 | let result = '' 68 | if (this.isValid()) { 69 | result = `${this.major}.${this.minor}` 70 | if (!isNaN(this.patch)) { 71 | result += `.${this.patch}` 72 | } 73 | } 74 | 75 | return result 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/github-api-helper.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as core from '@actions/core' 3 | import * as fs from 'fs' 4 | import * as github from '@actions/github' 5 | import * as io from '@actions/io' 6 | import * as path from 'path' 7 | import * as retryHelper from './retry-helper' 8 | import * as toolCache from '@actions/tool-cache' 9 | import {v4 as uuid} from 'uuid' 10 | import {getServerApiUrl} from './url-helper' 11 | 12 | const IS_WINDOWS = process.platform === 'win32' 13 | 14 | export async function downloadRepository( 15 | authToken: string, 16 | owner: string, 17 | repo: string, 18 | ref: string, 19 | commit: string, 20 | repositoryPath: string, 21 | baseUrl?: string 22 | ): Promise { 23 | // Determine the default branch 24 | if (!ref && !commit) { 25 | core.info('Determining the default branch') 26 | ref = await getDefaultBranch(authToken, owner, repo, baseUrl) 27 | } 28 | 29 | // Download the archive 30 | let archiveData = await retryHelper.execute(async () => { 31 | core.info('Downloading the archive') 32 | return await downloadArchive(authToken, owner, repo, ref, commit, baseUrl) 33 | }) 34 | 35 | // Write archive to disk 36 | core.info('Writing archive to disk') 37 | const uniqueId = uuid() 38 | const archivePath = IS_WINDOWS 39 | ? path.join(repositoryPath, `${uniqueId}.zip`) 40 | : path.join(repositoryPath, `${uniqueId}.tar.gz`) 41 | await fs.promises.writeFile(archivePath, archiveData) 42 | archiveData = Buffer.from('') // Free memory 43 | 44 | // Extract archive 45 | core.info('Extracting the archive') 46 | const extractPath = path.join(repositoryPath, uniqueId) 47 | await io.mkdirP(extractPath) 48 | if (IS_WINDOWS) { 49 | await toolCache.extractZip(archivePath, extractPath) 50 | } else { 51 | await toolCache.extractTar(archivePath, extractPath) 52 | } 53 | await io.rmRF(archivePath) 54 | 55 | // Determine the path of the repository content. The archive contains 56 | // a top-level folder and the repository content is inside. 57 | const archiveFileNames = await fs.promises.readdir(extractPath) 58 | assert.ok( 59 | archiveFileNames.length == 1, 60 | 'Expected exactly one directory inside archive' 61 | ) 62 | const archiveVersion = archiveFileNames[0] // The top-level folder name includes the short SHA 63 | core.info(`Resolved version ${archiveVersion}`) 64 | const tempRepositoryPath = path.join(extractPath, archiveVersion) 65 | 66 | // Move the files 67 | for (const fileName of await fs.promises.readdir(tempRepositoryPath)) { 68 | const sourcePath = path.join(tempRepositoryPath, fileName) 69 | const targetPath = path.join(repositoryPath, fileName) 70 | if (IS_WINDOWS) { 71 | await io.cp(sourcePath, targetPath, {recursive: true}) // Copy on Windows (Windows Defender may have a lock) 72 | } else { 73 | await io.mv(sourcePath, targetPath) 74 | } 75 | } 76 | await io.rmRF(extractPath) 77 | } 78 | 79 | /** 80 | * Looks up the default branch name 81 | */ 82 | export async function getDefaultBranch( 83 | authToken: string, 84 | owner: string, 85 | repo: string, 86 | baseUrl?: string 87 | ): Promise { 88 | return await retryHelper.execute(async () => { 89 | core.info('Retrieving the default branch name') 90 | const octokit = github.getOctokit(authToken, { 91 | baseUrl: getServerApiUrl(baseUrl) 92 | }) 93 | let result: string 94 | try { 95 | // Get the default branch from the repo info 96 | const response = await octokit.rest.repos.get({owner, repo}) 97 | result = response.data.default_branch 98 | assert.ok(result, 'default_branch cannot be empty') 99 | } catch (err) { 100 | // Handle .wiki repo 101 | if ( 102 | (err as any)?.status === 404 && 103 | repo.toUpperCase().endsWith('.WIKI') 104 | ) { 105 | result = 'master' 106 | } 107 | // Otherwise error 108 | else { 109 | throw err 110 | } 111 | } 112 | 113 | // Print the default branch 114 | core.info(`Default branch '${result}'`) 115 | 116 | // Prefix with 'refs/heads' 117 | if (!result.startsWith('refs/')) { 118 | result = `refs/heads/${result}` 119 | } 120 | 121 | return result 122 | }) 123 | } 124 | 125 | async function downloadArchive( 126 | authToken: string, 127 | owner: string, 128 | repo: string, 129 | ref: string, 130 | commit: string, 131 | baseUrl?: string 132 | ): Promise { 133 | const octokit = github.getOctokit(authToken, { 134 | baseUrl: getServerApiUrl(baseUrl) 135 | }) 136 | const download = IS_WINDOWS 137 | ? octokit.rest.repos.downloadZipballArchive 138 | : octokit.rest.repos.downloadTarballArchive 139 | const response = await download({ 140 | owner: owner, 141 | repo: repo, 142 | ref: commit || ref 143 | }) 144 | return Buffer.from(response.data as ArrayBuffer) // response.data is ArrayBuffer 145 | } 146 | -------------------------------------------------------------------------------- /src/input-helper.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as fsHelper from './fs-helper' 3 | import * as github from '@actions/github' 4 | import * as path from 'path' 5 | import * as workflowContextHelper from './workflow-context-helper' 6 | import {IGitSourceSettings} from './git-source-settings' 7 | 8 | export async function getInputs(): Promise { 9 | const result = {} as unknown as IGitSourceSettings 10 | 11 | // GitHub workspace 12 | let githubWorkspacePath = process.env['GITHUB_WORKSPACE'] 13 | if (!githubWorkspacePath) { 14 | throw new Error('GITHUB_WORKSPACE not defined') 15 | } 16 | githubWorkspacePath = path.resolve(githubWorkspacePath) 17 | core.debug(`GITHUB_WORKSPACE = '${githubWorkspacePath}'`) 18 | fsHelper.directoryExistsSync(githubWorkspacePath, true) 19 | 20 | // Qualified repository 21 | const qualifiedRepository = 22 | core.getInput('repository') || 23 | `${github.context.repo.owner}/${github.context.repo.repo}` 24 | core.debug(`qualified repository = '${qualifiedRepository}'`) 25 | const splitRepository = qualifiedRepository.split('/') 26 | if ( 27 | splitRepository.length !== 2 || 28 | !splitRepository[0] || 29 | !splitRepository[1] 30 | ) { 31 | throw new Error( 32 | `Invalid repository '${qualifiedRepository}'. Expected format {owner}/{repo}.` 33 | ) 34 | } 35 | result.repositoryOwner = splitRepository[0] 36 | result.repositoryName = splitRepository[1] 37 | 38 | // Repository path 39 | result.repositoryPath = core.getInput('path') || '.' 40 | result.repositoryPath = path.resolve( 41 | githubWorkspacePath, 42 | result.repositoryPath 43 | ) 44 | if ( 45 | !(result.repositoryPath + path.sep).startsWith( 46 | githubWorkspacePath + path.sep 47 | ) 48 | ) { 49 | throw new Error( 50 | `Repository path '${result.repositoryPath}' is not under '${githubWorkspacePath}'` 51 | ) 52 | } 53 | 54 | // Workflow repository? 55 | const isWorkflowRepository = 56 | qualifiedRepository.toUpperCase() === 57 | `${github.context.repo.owner}/${github.context.repo.repo}`.toUpperCase() 58 | 59 | // Source branch, source version 60 | result.ref = core.getInput('ref') 61 | if (!result.ref) { 62 | if (isWorkflowRepository) { 63 | result.ref = github.context.ref 64 | result.commit = github.context.sha 65 | 66 | // Some events have an unqualifed ref. For example when a PR is merged (pull_request closed event), 67 | // the ref is unqualifed like "main" instead of "refs/heads/main". 68 | if (result.commit && result.ref && !result.ref.startsWith('refs/')) { 69 | result.ref = `refs/heads/${result.ref}` 70 | } 71 | } 72 | } 73 | // SHA? 74 | else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) { 75 | result.commit = result.ref 76 | result.ref = '' 77 | } 78 | core.debug(`ref = '${result.ref}'`) 79 | core.debug(`commit = '${result.commit}'`) 80 | 81 | // Clean 82 | result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE' 83 | core.debug(`clean = ${result.clean}`) 84 | 85 | // Filter 86 | const filter = core.getInput('filter') 87 | if (filter) { 88 | result.filter = filter 89 | } 90 | 91 | core.debug(`filter = ${result.filter}`) 92 | 93 | // Sparse checkout 94 | const sparseCheckout = core.getMultilineInput('sparse-checkout') 95 | if (sparseCheckout.length) { 96 | result.sparseCheckout = sparseCheckout 97 | core.debug(`sparse checkout = ${result.sparseCheckout}`) 98 | } 99 | 100 | result.sparseCheckoutConeMode = 101 | (core.getInput('sparse-checkout-cone-mode') || 'true').toUpperCase() === 102 | 'TRUE' 103 | 104 | // Fetch depth 105 | result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1')) 106 | if (isNaN(result.fetchDepth) || result.fetchDepth < 0) { 107 | result.fetchDepth = 0 108 | } 109 | core.debug(`fetch depth = ${result.fetchDepth}`) 110 | 111 | // Fetch tags 112 | result.fetchTags = 113 | (core.getInput('fetch-tags') || 'false').toUpperCase() === 'TRUE' 114 | core.debug(`fetch tags = ${result.fetchTags}`) 115 | 116 | // Show fetch progress 117 | result.showProgress = 118 | (core.getInput('show-progress') || 'true').toUpperCase() === 'TRUE' 119 | core.debug(`show progress = ${result.showProgress}`) 120 | 121 | // LFS 122 | result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE' 123 | core.debug(`lfs = ${result.lfs}`) 124 | 125 | // Submodules 126 | result.submodules = false 127 | result.nestedSubmodules = false 128 | const submodulesString = (core.getInput('submodules') || '').toUpperCase() 129 | if (submodulesString == 'RECURSIVE') { 130 | result.submodules = true 131 | result.nestedSubmodules = true 132 | } else if (submodulesString == 'TRUE') { 133 | result.submodules = true 134 | } 135 | core.debug(`submodules = ${result.submodules}`) 136 | core.debug(`recursive submodules = ${result.nestedSubmodules}`) 137 | 138 | // Auth token 139 | result.authToken = core.getInput('token', {required: true}) 140 | 141 | // SSH 142 | result.sshKey = core.getInput('ssh-key') 143 | result.sshKnownHosts = core.getInput('ssh-known-hosts') 144 | result.sshStrict = 145 | (core.getInput('ssh-strict') || 'true').toUpperCase() === 'TRUE' 146 | result.sshUser = core.getInput('ssh-user') 147 | 148 | // Persist credentials 149 | result.persistCredentials = 150 | (core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE' 151 | 152 | // Workflow organization ID 153 | result.workflowOrganizationId = 154 | await workflowContextHelper.getOrganizationId() 155 | 156 | // Set safe.directory in git global config. 157 | result.setSafeDirectory = 158 | (core.getInput('set-safe-directory') || 'true').toUpperCase() === 'TRUE' 159 | 160 | // Determine the GitHub URL that the repository is being hosted from 161 | result.githubServerUrl = core.getInput('github-server-url') 162 | core.debug(`GitHub Host URL = ${result.githubServerUrl}`) 163 | 164 | return result 165 | } 166 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as coreCommand from '@actions/core/lib/command' 3 | import * as gitSourceProvider from './git-source-provider' 4 | import * as inputHelper from './input-helper' 5 | import * as path from 'path' 6 | import * as stateHelper from './state-helper' 7 | 8 | async function run(): Promise { 9 | try { 10 | const sourceSettings = await inputHelper.getInputs() 11 | 12 | try { 13 | // Register problem matcher 14 | coreCommand.issueCommand( 15 | 'add-matcher', 16 | {}, 17 | path.join(__dirname, 'problem-matcher.json') 18 | ) 19 | 20 | // Get sources 21 | await gitSourceProvider.getSource(sourceSettings) 22 | core.setOutput('ref', sourceSettings.ref) 23 | } finally { 24 | // Unregister problem matcher 25 | coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '') 26 | } 27 | } catch (error) { 28 | core.setFailed(`${(error as any)?.message ?? error}`) 29 | } 30 | } 31 | 32 | async function cleanup(): Promise { 33 | try { 34 | await gitSourceProvider.cleanup(stateHelper.RepositoryPath) 35 | } catch (error) { 36 | core.warning(`${(error as any)?.message ?? error}`) 37 | } 38 | } 39 | 40 | // Main 41 | if (!stateHelper.IsPost) { 42 | run() 43 | } 44 | // Post 45 | else { 46 | cleanup() 47 | } 48 | -------------------------------------------------------------------------------- /src/misc/generate-docs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as os from 'os' 3 | import * as path from 'path' 4 | import * as yaml from 'js-yaml' 5 | 6 | // 7 | // SUMMARY 8 | // 9 | // This script rebuilds the usage section in the README.md to be consistent with the action.yml 10 | 11 | function updateUsage( 12 | actionReference: string, 13 | actionYamlPath = 'action.yml', 14 | readmePath = 'README.md', 15 | startToken = '', 16 | endToken = '' 17 | ): void { 18 | if (!actionReference) { 19 | throw new Error('Parameter actionReference must not be empty') 20 | } 21 | 22 | // Load the action.yml 23 | const actionYaml = yaml.load(fs.readFileSync(actionYamlPath).toString()) 24 | 25 | // Load the README 26 | const originalReadme = fs.readFileSync(readmePath).toString() 27 | 28 | // Find the start token 29 | const startTokenIndex = originalReadme.indexOf(startToken) 30 | if (startTokenIndex < 0) { 31 | throw new Error(`Start token '${startToken}' not found`) 32 | } 33 | 34 | // Find the end token 35 | const endTokenIndex = originalReadme.indexOf(endToken) 36 | if (endTokenIndex < 0) { 37 | throw new Error(`End token '${endToken}' not found`) 38 | } else if (endTokenIndex < startTokenIndex) { 39 | throw new Error('Start token must appear before end token') 40 | } 41 | 42 | // Build the new README 43 | const newReadme: string[] = [] 44 | 45 | // Append the beginning 46 | newReadme.push(originalReadme.substr(0, startTokenIndex + startToken.length)) 47 | 48 | // Build the new usage section 49 | newReadme.push('```yaml', `- uses: ${actionReference}`, ' with:') 50 | const inputs = actionYaml.inputs 51 | let firstInput = true 52 | for (const key of Object.keys(inputs)) { 53 | const input = inputs[key] 54 | 55 | // Line break between inputs 56 | if (!firstInput) { 57 | newReadme.push('') 58 | } 59 | 60 | // Constrain the width of the description 61 | const width = 80 62 | let description = (input.description as string) 63 | .trimRight() 64 | .replace(/\r\n/g, '\n') // Convert CR to LF 65 | .replace(/ +/g, ' ') // Squash consecutive spaces 66 | .replace(/ \n/g, '\n') // Squash space followed by newline 67 | while (description) { 68 | // Longer than width? Find a space to break apart 69 | let segment: string = description 70 | if (description.length > width) { 71 | segment = description.substr(0, width + 1) 72 | while (!segment.endsWith(' ') && !segment.endsWith('\n') && segment) { 73 | segment = segment.substr(0, segment.length - 1) 74 | } 75 | 76 | // Trimmed too much? 77 | if (segment.length < width * 0.67) { 78 | segment = description 79 | } 80 | } else { 81 | segment = description 82 | } 83 | 84 | // Check for newline 85 | const newlineIndex = segment.indexOf('\n') 86 | if (newlineIndex >= 0) { 87 | segment = segment.substr(0, newlineIndex + 1) 88 | } 89 | 90 | // Append segment 91 | newReadme.push(` # ${segment}`.trimRight()) 92 | 93 | // Remaining 94 | description = description.substr(segment.length) 95 | } 96 | 97 | if (input.default !== undefined) { 98 | // Append blank line if description had paragraphs 99 | if ((input.description as string).trimRight().match(/\n[ ]*\r?\n/)) { 100 | newReadme.push(` #`) 101 | } 102 | 103 | // Default 104 | newReadme.push(` # Default: ${input.default}`) 105 | } 106 | 107 | // Input name 108 | newReadme.push(` ${key}: ''`) 109 | 110 | firstInput = false 111 | } 112 | 113 | newReadme.push('```') 114 | 115 | // Append the end 116 | newReadme.push(originalReadme.substr(endTokenIndex)) 117 | 118 | // Write the new README 119 | fs.writeFileSync(readmePath, newReadme.join(os.EOL)) 120 | } 121 | 122 | updateUsage( 123 | 'actions/checkout@v4', 124 | path.join(__dirname, '..', '..', 'action.yml'), 125 | path.join(__dirname, '..', '..', 'README.md') 126 | ) 127 | -------------------------------------------------------------------------------- /src/misc/licensed-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | src/misc/licensed-download.sh 6 | 7 | echo 'Running: licensed cached' 8 | _temp/licensed-3.6.0/licensed status -------------------------------------------------------------------------------- /src/misc/licensed-download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ ! -f _temp/licensed-3.6.0.done ]; then 6 | echo 'Clearing temp' 7 | rm -rf _temp/licensed-3.6.0 || true 8 | 9 | echo 'Downloading licensed' 10 | mkdir -p _temp/licensed-3.6.0 11 | pushd _temp/licensed-3.6.0 12 | if [[ "$OSTYPE" == "darwin"* ]]; then 13 | curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.6.0/licensed-3.6.0-darwin-x64.tar.gz 14 | else 15 | curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.6.0/licensed-3.6.0-linux-x64.tar.gz 16 | fi 17 | 18 | echo 'Extracting licenesed' 19 | tar -xzf licensed.tar.gz 20 | popd 21 | touch _temp/licensed-3.6.0.done 22 | else 23 | echo 'Licensed already downloaded' 24 | fi 25 | -------------------------------------------------------------------------------- /src/misc/licensed-generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | src/misc/licensed-download.sh 6 | 7 | echo 'Running: licensed cached' 8 | _temp/licensed-3.6.0/licensed cache -------------------------------------------------------------------------------- /src/ref-helper.ts: -------------------------------------------------------------------------------- 1 | import {IGitCommandManager} from './git-command-manager' 2 | import * as core from '@actions/core' 3 | import * as github from '@actions/github' 4 | import {getServerApiUrl, isGhes} from './url-helper' 5 | 6 | export const tagsRefSpec = '+refs/tags/*:refs/tags/*' 7 | 8 | export interface ICheckoutInfo { 9 | ref: string 10 | startPoint: string 11 | } 12 | 13 | export async function getCheckoutInfo( 14 | git: IGitCommandManager, 15 | ref: string, 16 | commit: string 17 | ): Promise { 18 | if (!git) { 19 | throw new Error('Arg git cannot be empty') 20 | } 21 | 22 | if (!ref && !commit) { 23 | throw new Error('Args ref and commit cannot both be empty') 24 | } 25 | 26 | const result = {} as unknown as ICheckoutInfo 27 | const upperRef = (ref || '').toUpperCase() 28 | 29 | // SHA only 30 | if (!ref) { 31 | result.ref = commit 32 | } 33 | // refs/heads/ 34 | else if (upperRef.startsWith('REFS/HEADS/')) { 35 | const branch = ref.substring('refs/heads/'.length) 36 | result.ref = branch 37 | result.startPoint = `refs/remotes/origin/${branch}` 38 | } 39 | // refs/pull/ 40 | else if (upperRef.startsWith('REFS/PULL/')) { 41 | const branch = ref.substring('refs/pull/'.length) 42 | result.ref = `refs/remotes/pull/${branch}` 43 | } 44 | // refs/tags/ 45 | else if (upperRef.startsWith('REFS/TAGS/')) { 46 | result.ref = ref 47 | } 48 | // refs/ 49 | else if (upperRef.startsWith('REFS/')) { 50 | result.ref = commit ? commit : ref 51 | } 52 | // Unqualified ref, check for a matching branch or tag 53 | else { 54 | if (await git.branchExists(true, `origin/${ref}`)) { 55 | result.ref = ref 56 | result.startPoint = `refs/remotes/origin/${ref}` 57 | } else if (await git.tagExists(`${ref}`)) { 58 | result.ref = `refs/tags/${ref}` 59 | } else { 60 | throw new Error( 61 | `A branch or tag with the name '${ref}' could not be found` 62 | ) 63 | } 64 | } 65 | 66 | return result 67 | } 68 | 69 | export function getRefSpecForAllHistory(ref: string, commit: string): string[] { 70 | const result = ['+refs/heads/*:refs/remotes/origin/*', tagsRefSpec] 71 | if (ref && ref.toUpperCase().startsWith('REFS/PULL/')) { 72 | const branch = ref.substring('refs/pull/'.length) 73 | result.push(`+${commit || ref}:refs/remotes/pull/${branch}`) 74 | } 75 | 76 | return result 77 | } 78 | 79 | export function getRefSpec(ref: string, commit: string): string[] { 80 | if (!ref && !commit) { 81 | throw new Error('Args ref and commit cannot both be empty') 82 | } 83 | 84 | const upperRef = (ref || '').toUpperCase() 85 | 86 | // SHA 87 | if (commit) { 88 | // refs/heads 89 | if (upperRef.startsWith('REFS/HEADS/')) { 90 | const branch = ref.substring('refs/heads/'.length) 91 | return [`+${commit}:refs/remotes/origin/${branch}`] 92 | } 93 | // refs/pull/ 94 | else if (upperRef.startsWith('REFS/PULL/')) { 95 | const branch = ref.substring('refs/pull/'.length) 96 | return [`+${commit}:refs/remotes/pull/${branch}`] 97 | } 98 | // refs/tags/ 99 | else if (upperRef.startsWith('REFS/TAGS/')) { 100 | return [`+${commit}:${ref}`] 101 | } 102 | // Otherwise no destination ref 103 | else { 104 | return [commit] 105 | } 106 | } 107 | // Unqualified ref, check for a matching branch or tag 108 | else if (!upperRef.startsWith('REFS/')) { 109 | return [ 110 | `+refs/heads/${ref}*:refs/remotes/origin/${ref}*`, 111 | `+refs/tags/${ref}*:refs/tags/${ref}*` 112 | ] 113 | } 114 | // refs/heads/ 115 | else if (upperRef.startsWith('REFS/HEADS/')) { 116 | const branch = ref.substring('refs/heads/'.length) 117 | return [`+${ref}:refs/remotes/origin/${branch}`] 118 | } 119 | // refs/pull/ 120 | else if (upperRef.startsWith('REFS/PULL/')) { 121 | const branch = ref.substring('refs/pull/'.length) 122 | return [`+${ref}:refs/remotes/pull/${branch}`] 123 | } 124 | // refs/tags/ 125 | else { 126 | return [`+${ref}:${ref}`] 127 | } 128 | } 129 | 130 | /** 131 | * Tests whether the initial fetch created the ref at the expected commit 132 | */ 133 | export async function testRef( 134 | git: IGitCommandManager, 135 | ref: string, 136 | commit: string 137 | ): Promise { 138 | if (!git) { 139 | throw new Error('Arg git cannot be empty') 140 | } 141 | 142 | if (!ref && !commit) { 143 | throw new Error('Args ref and commit cannot both be empty') 144 | } 145 | 146 | // No SHA? Nothing to test 147 | if (!commit) { 148 | return true 149 | } 150 | // SHA only? 151 | else if (!ref) { 152 | return await git.shaExists(commit) 153 | } 154 | 155 | const upperRef = ref.toUpperCase() 156 | 157 | // refs/heads/ 158 | if (upperRef.startsWith('REFS/HEADS/')) { 159 | const branch = ref.substring('refs/heads/'.length) 160 | return ( 161 | (await git.branchExists(true, `origin/${branch}`)) && 162 | commit === (await git.revParse(`refs/remotes/origin/${branch}`)) 163 | ) 164 | } 165 | // refs/pull/ 166 | else if (upperRef.startsWith('REFS/PULL/')) { 167 | // Assume matches because fetched using the commit 168 | return true 169 | } 170 | // refs/tags/ 171 | else if (upperRef.startsWith('REFS/TAGS/')) { 172 | const tagName = ref.substring('refs/tags/'.length) 173 | return ( 174 | (await git.tagExists(tagName)) && commit === (await git.revParse(ref)) 175 | ) 176 | } 177 | // Unexpected 178 | else { 179 | core.debug(`Unexpected ref format '${ref}' when testing ref info`) 180 | return true 181 | } 182 | } 183 | 184 | export async function checkCommitInfo( 185 | token: string, 186 | commitInfo: string, 187 | repositoryOwner: string, 188 | repositoryName: string, 189 | ref: string, 190 | commit: string, 191 | baseUrl?: string 192 | ): Promise { 193 | try { 194 | // GHES? 195 | if (isGhes(baseUrl)) { 196 | return 197 | } 198 | 199 | // Auth token? 200 | if (!token) { 201 | return 202 | } 203 | 204 | // Public PR synchronize, for workflow repo? 205 | if ( 206 | fromPayload('repository.private') !== false || 207 | github.context.eventName !== 'pull_request' || 208 | fromPayload('action') !== 'synchronize' || 209 | repositoryOwner !== github.context.repo.owner || 210 | repositoryName !== github.context.repo.repo || 211 | ref !== github.context.ref || 212 | !ref.startsWith('refs/pull/') || 213 | commit !== github.context.sha 214 | ) { 215 | return 216 | } 217 | 218 | // Head SHA 219 | const expectedHeadSha = fromPayload('after') 220 | if (!expectedHeadSha) { 221 | core.debug('Unable to determine head sha') 222 | return 223 | } 224 | 225 | // Base SHA 226 | const expectedBaseSha = fromPayload('pull_request.base.sha') 227 | if (!expectedBaseSha) { 228 | core.debug('Unable to determine base sha') 229 | return 230 | } 231 | 232 | // Expected message? 233 | const expectedMessage = `Merge ${expectedHeadSha} into ${expectedBaseSha}` 234 | if (commitInfo.indexOf(expectedMessage) >= 0) { 235 | return 236 | } 237 | 238 | // Extract details from message 239 | const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/) 240 | if (!match) { 241 | core.debug('Unexpected message format') 242 | return 243 | } 244 | 245 | // Post telemetry 246 | const actualHeadSha = match[1] 247 | if (actualHeadSha !== expectedHeadSha) { 248 | core.debug( 249 | `Expected head sha ${expectedHeadSha}; actual head sha ${actualHeadSha}` 250 | ) 251 | const octokit = github.getOctokit(token, { 252 | baseUrl: getServerApiUrl(baseUrl), 253 | userAgent: `actions-checkout-tracepoint/1.0 (code=STALE_MERGE;owner=${repositoryOwner};repo=${repositoryName};pr=${fromPayload( 254 | 'number' 255 | )};run_id=${ 256 | process.env['GITHUB_RUN_ID'] 257 | };expected_head_sha=${expectedHeadSha};actual_head_sha=${actualHeadSha})` 258 | }) 259 | await octokit.rest.repos.get({ 260 | owner: repositoryOwner, 261 | repo: repositoryName 262 | }) 263 | } 264 | } catch (err) { 265 | core.debug( 266 | `Error when validating commit info: ${(err as any)?.stack ?? err}` 267 | ) 268 | } 269 | } 270 | 271 | function fromPayload(path: string): any { 272 | return select(github.context.payload, path) 273 | } 274 | 275 | function select(obj: any, path: string): any { 276 | if (!obj) { 277 | return undefined 278 | } 279 | 280 | const i = path.indexOf('.') 281 | if (i < 0) { 282 | return obj[path] 283 | } 284 | 285 | const key = path.substr(0, i) 286 | return select(obj[key], path.substr(i + 1)) 287 | } 288 | -------------------------------------------------------------------------------- /src/regexp-helper.ts: -------------------------------------------------------------------------------- 1 | export function escape(value: string): string { 2 | return value.replace(/[^a-zA-Z0-9_]/g, x => { 3 | return `\\${x}` 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /src/retry-helper.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | 3 | const defaultMaxAttempts = 3 4 | const defaultMinSeconds = 10 5 | const defaultMaxSeconds = 20 6 | 7 | export class RetryHelper { 8 | private maxAttempts: number 9 | private minSeconds: number 10 | private maxSeconds: number 11 | 12 | constructor( 13 | maxAttempts: number = defaultMaxAttempts, 14 | minSeconds: number = defaultMinSeconds, 15 | maxSeconds: number = defaultMaxSeconds 16 | ) { 17 | this.maxAttempts = maxAttempts 18 | this.minSeconds = Math.floor(minSeconds) 19 | this.maxSeconds = Math.floor(maxSeconds) 20 | if (this.minSeconds > this.maxSeconds) { 21 | throw new Error('min seconds should be less than or equal to max seconds') 22 | } 23 | } 24 | 25 | async execute(action: () => Promise): Promise { 26 | let attempt = 1 27 | while (attempt < this.maxAttempts) { 28 | // Try 29 | try { 30 | return await action() 31 | } catch (err) { 32 | core.info((err as any)?.message) 33 | } 34 | 35 | // Sleep 36 | const seconds = this.getSleepAmount() 37 | core.info(`Waiting ${seconds} seconds before trying again`) 38 | await this.sleep(seconds) 39 | attempt++ 40 | } 41 | 42 | // Last attempt 43 | return await action() 44 | } 45 | 46 | private getSleepAmount(): number { 47 | return ( 48 | Math.floor(Math.random() * (this.maxSeconds - this.minSeconds + 1)) + 49 | this.minSeconds 50 | ) 51 | } 52 | 53 | private async sleep(seconds: number): Promise { 54 | return new Promise(resolve => setTimeout(resolve, seconds * 1000)) 55 | } 56 | } 57 | 58 | export async function execute(action: () => Promise): Promise { 59 | const retryHelper = new RetryHelper() 60 | return await retryHelper.execute(action) 61 | } 62 | -------------------------------------------------------------------------------- /src/state-helper.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | 3 | /** 4 | * Indicates whether the POST action is running 5 | */ 6 | export const IsPost = !!core.getState('isPost') 7 | 8 | /** 9 | * The repository path for the POST action. The value is empty during the MAIN action. 10 | */ 11 | export const RepositoryPath = core.getState('repositoryPath') 12 | 13 | /** 14 | * The set-safe-directory for the POST action. The value is set if input: 'safe-directory' is set during the MAIN action. 15 | */ 16 | export const PostSetSafeDirectory = core.getState('setSafeDirectory') === 'true' 17 | 18 | /** 19 | * The SSH key path for the POST action. The value is empty during the MAIN action. 20 | */ 21 | export const SshKeyPath = core.getState('sshKeyPath') 22 | 23 | /** 24 | * The SSH known hosts path for the POST action. The value is empty during the MAIN action. 25 | */ 26 | export const SshKnownHostsPath = core.getState('sshKnownHostsPath') 27 | 28 | /** 29 | * Save the repository path so the POST action can retrieve the value. 30 | */ 31 | export function setRepositoryPath(repositoryPath: string) { 32 | core.saveState('repositoryPath', repositoryPath) 33 | } 34 | 35 | /** 36 | * Save the SSH key path so the POST action can retrieve the value. 37 | */ 38 | export function setSshKeyPath(sshKeyPath: string) { 39 | core.saveState('sshKeyPath', sshKeyPath) 40 | } 41 | 42 | /** 43 | * Save the SSH known hosts path so the POST action can retrieve the value. 44 | */ 45 | export function setSshKnownHostsPath(sshKnownHostsPath: string) { 46 | core.saveState('sshKnownHostsPath', sshKnownHostsPath) 47 | } 48 | 49 | /** 50 | * Save the set-safe-directory input so the POST action can retrieve the value. 51 | */ 52 | export function setSafeDirectory() { 53 | core.saveState('setSafeDirectory', 'true') 54 | } 55 | 56 | // Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic. 57 | // This is necessary since we don't have a separate entry point. 58 | if (!IsPost) { 59 | core.saveState('isPost', 'true') 60 | } 61 | -------------------------------------------------------------------------------- /src/url-helper.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import {URL} from 'url' 3 | import {IGitSourceSettings} from './git-source-settings' 4 | 5 | export function getFetchUrl(settings: IGitSourceSettings): string { 6 | assert.ok( 7 | settings.repositoryOwner, 8 | 'settings.repositoryOwner must be defined' 9 | ) 10 | assert.ok(settings.repositoryName, 'settings.repositoryName must be defined') 11 | const serviceUrl = getServerUrl(settings.githubServerUrl) 12 | const encodedOwner = encodeURIComponent(settings.repositoryOwner) 13 | const encodedName = encodeURIComponent(settings.repositoryName) 14 | if (settings.sshKey) { 15 | const user = settings.sshUser.length > 0 ? settings.sshUser : 'git' 16 | return `${user}@${serviceUrl.hostname}:${encodedOwner}/${encodedName}.git` 17 | } 18 | 19 | // "origin" is SCHEME://HOSTNAME[:PORT] 20 | return `${serviceUrl.origin}/${encodedOwner}/${encodedName}` 21 | } 22 | 23 | export function getServerUrl(url?: string): URL { 24 | let resolvedUrl = process.env['GITHUB_SERVER_URL'] || 'https://github.com' 25 | if (hasContent(url, WhitespaceMode.Trim)) { 26 | resolvedUrl = url! 27 | } 28 | 29 | return new URL(resolvedUrl) 30 | } 31 | 32 | export function getServerApiUrl(url?: string): string { 33 | if (hasContent(url, WhitespaceMode.Trim)) { 34 | let serverUrl = getServerUrl(url) 35 | if (isGhes(url)) { 36 | serverUrl.pathname = 'api/v3' 37 | } else { 38 | serverUrl.hostname = 'api.' + serverUrl.hostname 39 | } 40 | 41 | return pruneSuffix(serverUrl.toString(), '/') 42 | } 43 | 44 | return process.env['GITHUB_API_URL'] || 'https://api.github.com' 45 | } 46 | 47 | export function isGhes(url?: string): boolean { 48 | const ghUrl = new URL( 49 | url || process.env['GITHUB_SERVER_URL'] || 'https://github.com' 50 | ) 51 | 52 | const hostname = ghUrl.hostname.trimEnd().toUpperCase() 53 | const isGitHubHost = hostname === 'GITHUB.COM' 54 | const isGitHubEnterpriseCloudHost = hostname.endsWith('.GHE.COM') 55 | const isLocalHost = hostname.endsWith('.LOCALHOST') 56 | 57 | return !isGitHubHost && !isGitHubEnterpriseCloudHost && !isLocalHost 58 | } 59 | 60 | function pruneSuffix(text: string, suffix: string) { 61 | if (hasContent(suffix, WhitespaceMode.Preserve) && text?.endsWith(suffix)) { 62 | return text.substring(0, text.length - suffix.length) 63 | } 64 | return text 65 | } 66 | 67 | enum WhitespaceMode { 68 | Trim, 69 | Preserve 70 | } 71 | 72 | function hasContent( 73 | text: string | undefined, 74 | whitespaceMode: WhitespaceMode 75 | ): boolean { 76 | let refinedText = text ?? '' 77 | if (whitespaceMode == WhitespaceMode.Trim) { 78 | refinedText = refinedText.trim() 79 | } 80 | return refinedText.length > 0 81 | } 82 | -------------------------------------------------------------------------------- /src/workflow-context-helper.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as fs from 'fs' 3 | 4 | /** 5 | * Gets the organization ID of the running workflow or undefined if the value cannot be loaded from the GITHUB_EVENT_PATH 6 | */ 7 | export async function getOrganizationId(): Promise { 8 | try { 9 | const eventPath = process.env.GITHUB_EVENT_PATH 10 | if (!eventPath) { 11 | core.debug(`GITHUB_EVENT_PATH is not defined`) 12 | return 13 | } 14 | 15 | const content = await fs.promises.readFile(eventPath, {encoding: 'utf8'}) 16 | const event = JSON.parse(content) 17 | const id = event?.repository?.owner?.id 18 | if (typeof id !== 'number') { 19 | core.debug('Repository owner ID not found within GITHUB event info') 20 | return 21 | } 22 | 23 | return id as number 24 | } catch (err) { 25 | core.debug( 26 | `Unable to load organization ID from GITHUB_EVENT_PATH: ${ 27 | (err as any).message || err 28 | }` 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es6" 7 | ], 8 | "outDir": "./lib", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true 15 | }, 16 | "exclude": ["__test__", "lib", "node_modules"] 17 | } 18 | --------------------------------------------------------------------------------