Compare commits

...

125 Commits

Author SHA1 Message Date
Cory Miller b4ffde65f4
Link to release page from what's new section (#1514) 2023-10-17 11:52:30 -04:00
Peter Bengtsson 8530928916
Correct link to GitHub Docs (#1511)
No hardcoded language in the URL and uses the correct docs.github.com domain.
2023-10-13 11:07:47 -04:00
Josh Gross 7cdaf2fbc0
Update CODEOWNERS to Launch team (#1510) 2023-10-10 11:16:51 -04:00
Cory Miller 8ade135a41
Prepare 4.1.0 release (#1496) 2023-09-22 13:40:21 -04:00
Finley Garton c533a0a4cf
Add support for partial checkout filters (#1396)
* added filter option & tests

* added build file

* fix test oversight

* added exit 1

* updated docs to specify override

* undo unneeded readme change

* set to undefined rather than empty string

* run git config in correct di

---------

Co-authored-by: Cory Miller <13227161+cory-miller@users.noreply.github.com>
2023-09-22 13:30:36 -04:00
Varun Sivapalan 72f2cec99f
Update README.md for V4 (#1452)
* Update README.md for V4

* Update actionReference in generate-docs.ts for v4
2023-09-05 09:21:52 -04:00
Tatyana Kostromskaya 3df4ab11eb
Release 4.0.0 (#1447)
* Release 4.0.0

* Add new major version to workflow
2023-09-04 14:19:40 +02:00
Simon Baird 8b5e8b7687
Support fetching without the --progress option (#1067)
Setting the `show-progress` option to false in the `with` section of the
workflow step will cause git fetch to run without `--progress`.

The motivation is to be able to suppress the noisy progress status
output which adds many hundreds of "remote: Counting objects: 85%
(386/453)" and similar lines in the workflow log.

This should be sufficient to resolve #894 and its older friends,
though the solution is different to the one proposed there because
it doesn't use the --quiet flag. IIUC git doesn't show the progress
status by default since the output is not a terminal, so that's why
removing the --progress option is all that's needed.

Adding the --quiet flag doesn't make a lot of difference once the
--progress flag is removed, and actually I think using --quiet would
suppress some other more useful output that would be better left
visible.

Signed-off-by: Simon Baird <sbaird@redhat.com>
2023-09-01 14:19:18 -04:00
Tatyana Kostromskaya 97a652b800
Update default runtime to node20 (#1436)
* .

* update workflows
2023-08-29 17:01:52 +02:00
Luke Tomlinson f43a0e5ff2
Release 3.6.0 (#1437) 2023-08-24 09:54:47 -04:00
Robert Wieczoreck 7739b9ba2e
Add option to fetch tags even if fetch-depth > 0 (#579)
* Add option to fetch tags even if fetch-depth > 0

* Add jest tests for fetchDepth and fetchTags options
2023-08-16 16:34:54 -04:00
Johannes Schindelin 96f53100ba
Mark test scripts with Bash'isms to be run via Bash (#1377)
In #1369, I mistakenly replaced the hash-bang lines in the two new
scripts with `#!/bin/sh`, missing that both files contain the Bash'ism
`[[`. Symptom as per
https://github.com/actions/checkout/actions/runs/5200323109/jobs/9378889172?pr=1369#step:12:5

    __test__/verify-sparse-checkout.sh: 58: [[: not found

Let's change those hash-bang lines back to `#!/bin/bash`.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
2023-06-09 11:05:29 -04:00
Tingluo Huang c85c95e3d7
Release v3.5.3 (#1376) 2023-06-09 10:47:54 -04:00
Johannes Schindelin d106d4669b
Add support for sparse checkouts (#1369)
* Add support for sparse checkouts

* sparse-checkout: optionally turn off cone mode

While it _is_ true that cone mode is the default nowadays (mainly for
performance reasons: code mode is much faster than non-cone mode), there
_are_ legitimate use cases where non-cone mode is really useful.

Let's add a flag to optionally disable cone mode.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>

* Verify minimum Git version for sparse checkout

The `git sparse-checkout` command is available only since Git version
v2.25.0. The `actions/checkout` Action actually supports older Git
versions than that; As of time of writing, the minimum version is
v2.18.0.

Instead of raising this minimum version even for users who do not
require a sparse checkout, only check for this minimum version
specifically when a sparse checkout was asked for.

Suggested-by: Tingluo Huang <tingluohuang@github.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>

* Support sparse checkout/LFS better

Instead of fetching all the LFS objects present in the current revision
in a sparse checkout, whether they are needed inside the sparse cone or
not, let's instead only pull the ones that are actually needed.

To do that, let's avoid running that preemptive `git lfs fetch` call in
case of a sparse checkout.

An alternative that was considered during the development of this patch
(and ultimately rejected) was to use `git lfs pull --include <path>...`,
but it turned out to be too inflexible because it requires exact paths,
not the patterns that are available via the sparse checkout definition,
and that risks running into command-line length limitations.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>

---------

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Co-authored-by: Daniel <daniel.fernandez@feverup.com>
2023-06-09 09:08:21 -04:00
Dimitri Papadopoulos Orfanos f095bcc56b
Fix typos found by codespell (#1287) 2023-04-19 16:55:10 +02:00
SKi 47fbe2df0a
Fix: Checkout fail in self-hosted runners when faulty submodule are checked-in (#1196)
* Fix Self hosted runner issue wrt bad submodules - solution cleanup working space.

* Fix format with npm run format output

* Add mock implementation for new function submoduleStatus

* Add 2  test cases for submodule status.

* Codeql-Action Analyse revert v1 to v2

---------

Co-authored-by: Bassem Dghaidi <568794+Link-@users.noreply.github.com>
Co-authored-by: sminnie <minnie@sankhe.com>
2023-04-14 06:26:47 -04:00
Ferenc Hammerl 8e5e7e5ab8
Release v3.5.2 (#1291)
* Update version and changelog for 3.5.2

* Update changelog
2023-04-13 14:45:33 +02:00
Ferenc Hammerl eb35239ec2
Fix: convert baseUrl to serverApiUrl 'formatted' (#1289)
* fix: convert baseUrl to apiUrl

* Run prettier

---------

Co-authored-by: 1newsr <1newsr@users.noreply.github.com>
2023-04-13 14:25:50 +02:00
Ferenc Hammerl 83b7061638
Release v3.5.1 (#1284)
* Sync checkout version with tag version

* Downgrade to 3.5.1

* Format changelog
2023-04-12 17:02:21 +02:00
Binrui Dong 40a16ebeed
Improve checkout performance on Windows runners by upgrading @actions/github dependency (#1246)
* Improve checkout performance on Windows runners by upgrading @actions/github dependency

Re: https://github.com/actions/checkout/issues/1186

@dscho discovered that the checkout action could stall for a
considerable amount of time on Windows runners waiting for PowerShell
invocations made from 'windows-release' npm package to complete.

Then I studied the dependency chain to figure out where
'windows-release' was imported:

'@actions/checkout'@main
  <- '@actions/github'@2.2.0
    <- '@octokit/endpoint'@6.0.1
    <- '@octokit/graphql'@4.3.1
    <- '@octokit/request'@5.4.2
    <- '@octokit/rest'@16.43.1
      <- 'universal-user-agent'@4.0.1
        <- 'os-name'@3.1.0
          <- 'windows-release'@3.1.0

'universal-user-agent' package dropped its dependency on 'os-name' in
https://github.com/gr2m/universal-user-agent/releases/tag/v6.0.0 .

'@actions/github' v3 removed dependency on '@octokit/rest'@16.43.1 and
allows users to move away from the old 'universal-user-agent' v4.
(https://github.com/actions/toolkit/pull/453)

This pull request attempts to update the version of '@actions/github'
used in the checkout action to avoid importing 'windows-release'.

Based on testing in my own repositories, I can see an improvement in
reduced wait time between entering the checkout action and git actually
starts to do useful work.

* Update .licenses

* Rebuild index.js
2023-04-12 13:55:27 +02:00
Cameron Booth 8f4b7f8486
Add new public key for known_hosts (#1237)
* Add new public key for known_hosts

* Fix the build!
2023-03-24 01:34:48 -04:00
Tingluo Huang cd6a9fd493
Update update-main-version.yml 2023-03-23 22:44:49 -04:00
Cory Miller 24cb908017
Bump @actions/io to v1.1.3 (#1225)
* Backfill changelog and bump actions/io
* Fill in PR for action/io upgrade
* Licensed updates
* Compile JS
2023-03-15 15:45:08 -04:00
Bassem Dghaidi 27135e314d
Upgrade dependencies (#1210)
* Upgrade ncc & json5

* Add new dist build
2023-03-09 11:42:29 -05:00
Bassem Dghaidi 7b187184d1
Upgrade codeql actions to v2 (#1209) 2023-03-09 10:59:09 -05:00
Andrew Scheller ac59398561
Fix comment typos (that got added in #770) (#1057) 2022-12-28 17:19:08 -05:00
Ryan van Zeben 3ba5ee6fac
Add in explicit reference to private checkout options (#1050) 2022-12-16 16:06:54 -05:00
Cory Miller 8856415920
Implement branch list using callbacks from exec function (#1045)
When trying to list local branches to figure out what needs cleaned up during runs on non-ephemeral Actions Runners, we use git rev-parse --symbolic-full-name to get a list of branches. This can lead to ambiguous ref name errors when there are branches and tags with similar names.

Part of the reason we use rev-parse --symbolic-full-name vs git branch --list or git rev-parse --symbolic seems to related to a bug in Git 2.18. Until we can deprecate our usage of Git 2.18, I think we need to keep --symbolic-full-name. Since part of the problem is that these ambiguous ref name errors clog the Actions annotation limits, this is a mitigation to suppress those messages until we can get rid of the workaround.
2022-12-14 16:08:53 -05:00
Vallie Joseph 755da8c3cf
3.2.0 (#1039) 2022-12-12 14:11:05 -05:00
Cory Miller 26d48e8ea1
Update @actions/io to 1.1.2 (#1029) 2022-12-12 13:44:24 -05:00
Jok bf085276ce
wrap pipeline commands for submoduleForeach in quotes (#964)
* wrap pipeline commands for submoduleForeach in quotes

* Update src/git-auth-helper.ts

drop extraneous space.

Co-authored-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* Followed CONTRIBUTING.md instructions, updating dist/index.js

* fixed package-lock.json

* updating the pipeline so it runs from sh

Co-authored-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2022-11-15 21:51:19 -05:00
Cory Miller 5c3ccc22eb
Replace datadog/squid with ubuntu/squid Docker image (#1002)
* test with ubuntu/squid

* Start a contributing guide

* remove link
2022-11-10 15:54:12 -05:00
Scott Brenner 1f9a0c22da
README - fix status badge (#967) 2022-10-17 09:49:29 -04:00
Francesco Renzi 8230315d06
Add workflow to update a main version (#942)
* Add workflow to update a main version

* New line
2022-10-04 13:44:41 +01:00
Francesco Renzi 93ea575cb5
Prepare release v3.1.0 (#940)
* Prepare changelog for v3.1.0

* Bump package
2022-10-04 10:37:06 +01:00
Francesco Renzi 6a84743051
Bump @actions/core to 1.10.0 (#939)
* Bump @actions/core to 1.10.0

* Update licenses

* Use @actions/core helper functions
2022-10-03 18:04:49 +01:00
Peter Murray e6d535c99c
Inject GitHub host to be able to clone from another GitHub instance (#922)
* Adding the ability to specify the GitHub Server URL and allowing for it to differ from the Actions workflow host

* Adding tests for injecting the GitHub URL

* Addressing code review comments for PR #922
2022-09-26 12:34:52 -04:00
Tingluo Huang 2541b1294d
Prepare changelog for v3.0.2. (#777) 2022-04-21 10:29:04 -04:00
Tingluo Huang 0ffe6f9c55
Add set-safe-directory input to allow customers to take control. (#770)
* Add set-safe-directory input to allow customers to take control.
2022-04-20 21:37:43 -04:00
Thomas Boop dcd71f6466
Enforce safe directory (#762)
* set safe directory when running checkout

* Update CHANGELOG.md
2022-04-14 14:13:20 -04:00
Tingluo Huang add3486cc3
Patch to fix the dependbot alert. (#744)
* Patch to fix the dependbot alert.

* .

* .

* .
2022-04-05 13:01:33 -04:00
dependabot[bot] 5126516654
Bump minimist from 1.2.5 to 1.2.6 (#741)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-31 10:09:15 -04:00
Edward Thomson d50f8ea767
Add v3.0 release information to changelog (#740) 2022-03-25 09:52:31 -04:00
Thomas Boop 2d1c1198e7
update test workflows to checkout v3 (#709) 2022-03-01 13:02:13 -05:00
Thomas Boop a12a3943b4
update readme for v3 (#708)
* update readme for v3

* update readme with changes

* nit grammar
2022-03-01 12:46:45 -05:00
Thomas Boop 8f9e05e482
Update to node 16 (#689)
* Update to node 16

* update setup-node version

* Update check-dist.yml

update setup node version

* update dist/index.js
2022-02-28 16:17:29 -05:00
Ameya Lokare 230611dbd0
Change secret name for PAT to not start with GITHUB_ (#623)
Github doesn't allow secret names that start with `GITHUB_` (case insensitive). Update README to choose a different prefix (GH).
2021-11-02 16:20:59 -05:00
eric sciple ec3a7ce113
set insteadOf url for org-id (#621) 2021-11-01 11:43:18 -05:00
eric sciple fd47087372
codeql should analyze lib not dist (#620) 2021-10-20 15:11:24 -05:00
eric sciple 3d677ac575
script to generate license info (#614) 2021-10-19 14:30:04 -05:00
eric sciple 826ba42d6c
npm audit fix (#612) 2021-10-19 10:05:28 -05:00
eric sciple eb8a193c1d
update dev dependencies and react to new linting rules (#611) 2021-10-19 09:52:57 -05:00
Jeremy Epling c49af7ca1f
Create codeql-analysis.yml (#602) 2021-10-18 16:28:25 -05:00
Thomas Boop 1e204e9a92
update licensed check (#606) 2021-10-13 16:22:03 -05:00
eric sciple 0299a0d2b6
update dist (#605) 2021-10-13 16:07:05 -05:00
dependabot[bot] be0f448456
Bump ws from 5.2.2 to 5.2.3 (#604)
Bumps [ws](https://github.com/websockets/ws) from 5.2.2 to 5.2.3.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/5.2.2...5.2.3)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 09:14:20 -05:00
dependabot[bot] 56c00a7b1f
Bump tmpl from 1.0.4 to 1.0.5 (#588)
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

---
updated-dependencies:
- dependency-name: tmpl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 09:13:31 -05:00
dependabot[bot] 85e47d1a2b
Bump path-parse from 1.0.6 to 1.0.7 (#568)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 09:13:04 -05:00
dependabot[bot] 3fc17f8645
Bump hosted-git-info from 2.8.5 to 2.8.9 (#500)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.5 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.5...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 09:11:06 -05:00
dependabot[bot] e3bc06d986
Bump lodash from 4.17.15 to 4.17.21 (#499)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 09:08:31 -05:00
dependabot[bot] 442567ba57
Bump handlebars from 4.5.3 to 4.7.7 (#497)
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.5.3 to 4.7.7.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.5.3...v4.7.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 09:07:45 -05:00
dependabot[bot] 7f00b66d06
Bump y18n from 4.0.0 to 4.0.1 (#469)
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 09:07:05 -05:00
dependabot[bot] eccf386318
Bump @actions/core from 1.1.3 to 1.2.6 (#361)
Bumps [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) from 1.1.3 to 1.2.6.
- [Release notes](https://github.com/actions/toolkit/releases)
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 08:57:33 -05:00
dependabot[bot] 2bd2911be9
Bump acorn from 5.7.3 to 5.7.4 (#186)
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 08:55:25 -05:00
Brian Cristante afe4af09a7
Create check-dist.yml (#566)
* Add check-dist.yml

* Don't need to mv to git diff

* Upload the whole dist/ directory as an artifact

* Update .github/workflows/check-dist.yml
2021-08-17 16:08:22 -04:00
Ross Brodbeck 25a956c84d
Create CODEOWNERS 2021-02-04 12:25:41 -05:00
Johannes Schindelin 5a4ac9002d
Add missing `await`s (#379)
* auth-helper: properly await replacement of the token value in the config

After writing the `.extraheader` config, we manually replace the token
with the actual value. This is done in an `async` function, but we were
not `await`ing the result.

In our tests, this commit fixes a flakiness we observed where
`remote.origin.url` sometimes (very rarely, actually) is not set for
submodules. Our interpretation is that the configs are in the process of
being rewritten with the correct token value _while_ another `git
config` that wants to set the `insteadOf` value is reading the config,
which is currently empty.

A more idiomatic way to fix this in Typescript would use
`Promise.all()`, like this:

      await Promise.all(
        configPaths.map(async configPath => {
          core.debug(`Replacing token placeholder in '${configPath}'`)
          await this.replaceTokenPlaceholder(configPath)
        })
      )

However, during review of https://github.com/actions/checkout/pull/379
it was decided to keep the `for` loop in the interest of simplicity.

Reported by Ian Lynagh.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>

* downloadRepository(): await the result of recursive deletions

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>

* Ask ESLint to report floating Promises

This rule is quite helpful in avoiding hard-to-debug missing `await`s.

Note: there are two locations in `src/main.ts` that trigger warnings:
the `run()` and the `cleanup()` function are called without `await` and
without any `.catch()` clause.

In the initial version of https://github.com/actions/checkout/pull/379,
this was addressed by adding `.catch()` clauses. However, it was
determined that this is boilerplate code that will need to be fixed in a
broader way.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>

* Rebuild

This trick was brought to you by `npm ci && npm run build`. Needed to
get the PR build to pass.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
2020-11-03 09:44:09 -05:00
Thomas Boop c952173edf
Swap to Environment Files (#360) 2020-09-30 11:41:09 -04:00
Thomas Boop a81bbbf829
Remove unneeded commit information from build logs (#345)
* Remove unneeded commit information from stdout
2020-09-23 09:41:47 -04:00
Thomas Boop 21dc310f19
Add `Licensed` To Help Verify Prod Licenses (#326)
* Add Licensed file and workflow

* manual updates of dependencies

* Delete licenses.txt

* Ignore Generated Files in Git PR's
2020-09-10 09:24:29 -04:00
Thomas Boop be6c44d969 Revert "Delete licenses.txt" 2020-08-11 19:41:01 -04:00
Thomas Boop dac8cc78a1
Delete licenses.txt 2020-08-11 19:36:35 -04:00
Thomas Boop 2036a08e25
Add Third Party License Information to Dist Files (#320) 2020-08-07 09:22:39 -04:00
eric sciple 592cf69a22
Update README.md 2020-07-14 16:30:57 -04:00
eric sciple a4b69b4886
Update README.md 2020-07-14 13:08:52 -04:00
eric sciple 1433f62caa
update default branch (#305) 2020-07-14 09:23:30 -04:00
eric sciple 61b9e3751b
improve description for fetch-depth (#301) 2020-07-12 21:02:24 -04:00
eric sciple 28c7f3d2b5 changelog 2020-06-18 10:27:39 -04:00
eric sciple fb6f360df2
fix default branch for .wiki and when using ssh (#284) 2020-06-18 10:20:33 -04:00
eric sciple b4483adec3 changelog 2020-06-16 13:48:53 -04:00
eric sciple 00a3be8934
determine default branch (#278) 2020-06-16 13:41:01 -04:00
eric sciple 453ee27fca update troubleshooting instructions to include 'npm run format' 2020-05-31 17:48:51 -04:00
Daniel Hwang 65865e15a1
build because all is no more (#264) 2020-05-31 17:46:53 -04:00
eric sciple aabbfeb2ce changelog 2020-05-27 12:37:40 -04:00
eric sciple e52d022eb5
Fetch all history for all tags and branches when fetch-depth=0 (#258) 2020-05-27 09:54:28 -04:00
eric sciple 2ff2fbdea4
telemetry for incorrect merge commit (#253) 2020-05-21 11:09:16 -04:00
eric sciple df86c829eb
fix readme (#251) 2020-05-20 10:20:52 -04:00
Peter Evans 97b30c411c
fix prettier glob pattern (#247) 2020-05-19 12:34:05 -04:00
eric sciple 86f86b36ef changelog 2020-05-19 10:27:02 -04:00
eric sciple 7523e23789
switch GITHUB_URL to GITHUB_SERVER_URL (#248) 2020-05-18 13:05:15 -04:00
eric sciple ac455590d1
consume new @actions/github for GHES support (#236) 2020-05-07 12:11:11 -04:00
eric sciple 94c2de77cc Update changelog 2020-04-02 16:04:37 -04:00
eric sciple 01aecccf73
group output (#191) 2020-03-27 13:12:15 -04:00
eric sciple 85b1f35505
changes to support ghes alpha release (#199) 2020-03-25 15:12:22 -04:00
eric sciple 574281d34c update readme 2020-03-19 22:17:25 -04:00
Jef LeCompte fbb30c60ab
Removed doc based on new changes (#192) 2020-03-19 22:03:41 -04:00
eric sciple 58070a9fc3
update input descriptions in adr (#189) 2020-03-19 10:26:00 -04:00
eric sciple 9a3a9ade82
persist core.sshCommand for submodules (#184)
* persist core.sshCommand for submodules

* update verbiage; add comments

* fail when submodules or ssh-key and fallback to REST API
2020-03-12 11:42:38 -04:00
eric sciple b2e6b7ed13
add ssh support (#163) 2020-03-11 15:55:17 -04:00
eric sciple 80602fafba
convert SSH URL to HTTPS (#179) 2020-03-10 10:45:50 -04:00
eric sciple b4626ce19c
revise adr: convert SSH URL to HTTPS (#178) 2020-03-06 17:30:23 -05:00
eric sciple 422dc45671
add support for submodules (#173) 2020-03-05 14:21:59 -05:00
eric sciple 204620207c
revise adr to support submodules (#157) 2020-03-04 13:10:35 -05:00
eric sciple f219062370
more unit tests and corresponding refactoring (#174) 2020-03-02 11:33:30 -05:00
eric sciple 096e927750
revise adr to support ssh (#156) 2020-02-25 09:59:59 -05:00
eric sciple f858c22e96
update adr to match current behavior (#154) 2020-02-13 15:26:25 -05:00
Christopher Sexton 77904fd431
Handle submodules with SSH URLs (#140)
* Handle submodules with SSH URLs

This is just a documentation change, explaining how to fix submodules
that are configured to use SSH URLs instead of HTTPS URLs. Spent a while
banging my head on the wall and hope this saves someone else the pain.

This is helpful for teams that use the SSH protocol for local
development so don't want to change the mechanism that pulls in the
submodules. Using `insteadOf` seems a bit nicer than than setting up a
deploy keypair.

* SSH submodules

Co-authored-by: Chris Patterson <chrispat@github.com>
2020-02-13 14:44:37 -05:00
eric sciple 06218e4404
checkout v2 adr (#153) 2020-02-13 14:43:20 -05:00
eric sciple 61fd8fd0c7
switch to spyOn for mocks (#152) 2020-02-13 13:25:46 -05:00
eric sciple f95f2a3856
Update test.yml 2020-01-27 10:26:27 -05:00
eric sciple f90c7b395d
follow proxy settings (#144) 2020-01-27 10:21:50 -05:00
eric sciple 090d9c9dfd
fix ref for pr closed event when a pr is merged (#141) 2020-01-21 14:17:04 -05:00
eric sciple db41740e12
consume v2 action during build (#131) 2020-01-03 12:49:41 -05:00
eric sciple bc50a995b8
Add link to doc for creating and using encyrpted secrets (#123) 2020-01-03 12:32:17 -05:00
eric sciple dfd70d4a2d
2.0.1 (#129) 2020-01-03 11:24:41 -05:00
eric sciple ae525b2262
fix issue checking detached when git less than 2.22 (#128) 2020-01-03 10:13:01 -05:00
eric sciple f466b96953
improve summary (#127) 2020-01-02 15:40:10 -05:00
eric sciple c85684db76
example fetch all history for all tags and branches (#115) 2019-12-16 10:45:02 -05:00
eric sciple 299dd5064e
add more scenarios (#112) 2019-12-13 16:39:47 -05:00
eric sciple 722adc63f1
update examples to reference v2 tag (#110) 2019-12-13 00:00:48 -05:00
eric sciple 3537747199
fix ref (#109) 2019-12-12 14:44:19 -05:00
eric sciple a6747255bd
do not pass cred on command line (#108) 2019-12-12 14:04:04 -05:00
eric sciple c170eefc26
add input persist-credentials (#107) 2019-12-12 13:49:26 -05:00
eric sciple a572f640b0
fallback to REST API to download repo (#104) 2019-12-12 13:16:16 -05:00
Riddhesh Sanghvi cab31617d8 Document update: Checkout PR head sha (#102) 2019-12-10 11:17:38 -05:00
95 changed files with 33242 additions and 14509 deletions

View File

@ -1,6 +1,6 @@
{
"plugins": ["jest", "@typescript-eslint"],
"extends": ["plugin:github/es6"],
"extends": ["plugin:github/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
@ -16,23 +16,19 @@
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-ignore": "error",
"camelcase": "off",
"@typescript-eslint/camelcase": "error",
"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
"@typescript-eslint/func-call-spacing": ["error", "never"],
"@typescript-eslint/generic-type-naming": ["error", "^[A-Z][A-Za-z]*$"],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-object-literal-type-assertion": "error",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error",
@ -40,7 +36,6 @@
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-interface": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
.licenses/** -diff linguist-generated=true

51
.github/workflows/check-dist.yml vendored Normal file
View File

@ -0,0 +1,51 @@
# `dist/index.js` is a special file in Actions.
# When you reference an action with `uses:` in a workflow,
# `index.js` is the code that will run.
# For our project, we generate this file through a build process
# from other source files.
# We need to make sure the checked-in `index.js` actually matches what we expect it to be.
name: Check dist
on:
push:
branches:
- main
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:
jobs:
check-dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set Node.js 20.x
uses: actions/setup-node@v1
with:
node-version: 20.x
- name: Install dependencies
run: npm ci
- name: Rebuild the index.js file
run: npm run build
- name: Compare the expected and actual dist/ directories
run: |
if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then
echo "Detected uncommitted changes after build. See status below:"
git diff
exit 1
fi
# If dist/ was different than expected, upload the expected version as an artifact
- uses: actions/upload-artifact@v2
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
with:
name: dist
path: dist/

58
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,58 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '28 9 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- run: npm ci
- run: npm run build
- run: rm -rf dist # We want code scanning to analyze lib instead (individual .js files)
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

14
.github/workflows/licensed.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: Licensed
on:
push: {branches: main}
pull_request: {branches: main}
jobs:
test:
runs-on: ubuntu-latest
name: Check licenses
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run licensed-check

View File

@ -4,20 +4,22 @@ on:
pull_request:
push:
branches:
- master
- main
- releases/*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1 # todo: switch to v2
- uses: actions/setup-node@v1
with:
node-version: 20.x
- uses: actions/checkout@v3
- run: npm ci
- run: npm run build
- run: npm run format-check
- run: npm run lint
- run: npm run pack
- run: npm run gendocs
- run: npm test
- name: Verify no unstaged changes
run: __test__/verify-no-unstaged-changes.sh
@ -30,10 +32,10 @@ jobs:
steps:
# Clone this repo
- name: Checkout
uses: actions/checkout@v1 # todo: switch to V2
uses: actions/checkout@v3
# Basic checkout
- name: Basic checkout
- name: Checkout basic
uses: ./
with:
ref: test-data/v2/basic
@ -46,7 +48,7 @@ jobs:
- name: Modify work tree
shell: bash
run: __test__/modify-work-tree.sh
- name: Clean checkout
- name: Checkout clean
uses: ./
with:
ref: test-data/v2/basic
@ -56,12 +58,12 @@ jobs:
run: __test__/verify-clean.sh
# Side by side
- name: Side by side checkout 1
- name: Checkout side by side 1
uses: ./
with:
ref: test-data/v2/side-by-side-1
path: side-by-side-1
- name: Side by side checkout 2
- name: Checkout side by side 2
uses: ./
with:
ref: test-data/v2/side-by-side-2
@ -70,8 +72,45 @@ jobs:
shell: bash
run: __test__/verify-side-by-side.sh
# Filter
- name: Fetch filter
uses: ./
with:
filter: 'blob:none'
path: fetch-filter
- name: Verify fetch filter
run: __test__/verify-fetch-filter.sh
# Sparse checkout
- name: Sparse checkout
uses: ./
with:
sparse-checkout: |
__test__
.github
dist
path: sparse-checkout
- name: Verify sparse checkout
run: __test__/verify-sparse-checkout.sh
# Sparse checkout (non-cone mode)
- name: Sparse checkout (non-cone mode)
uses: ./
with:
sparse-checkout: |
/__test__/
/.github/
/dist/
sparse-checkout-cone-mode: false
path: sparse-checkout-non-cone-mode
- name: Verify sparse checkout (non-cone mode)
run: __test__/verify-sparse-checkout-non-cone-mode.sh
# LFS
- name: LFS checkout
- name: Checkout LFS
uses: ./
with:
repository: actions/checkout # hardcoded, otherwise doesn't work from a fork
@ -81,3 +120,163 @@ jobs:
- name: Verify LFS
shell: bash
run: __test__/verify-lfs.sh
# Submodules false
- name: Checkout submodules false
uses: ./
with:
ref: test-data/v2/submodule-ssh-url
path: submodules-false
- name: Verify submodules false
run: __test__/verify-submodules-false.sh
# Submodules one level
- name: Checkout submodules true
uses: ./
with:
ref: test-data/v2/submodule-ssh-url
path: submodules-true
submodules: true
- name: Verify submodules true
run: __test__/verify-submodules-true.sh
# Submodules recursive
- name: Checkout submodules recursive
uses: ./
with:
ref: test-data/v2/submodule-ssh-url
path: submodules-recursive
submodules: recursive
- name: Verify submodules recursive
run: __test__/verify-submodules-recursive.sh
# Basic checkout using REST API
- name: Remove basic
if: runner.os != 'windows'
run: rm -rf basic
- name: Remove basic (Windows)
if: runner.os == 'windows'
shell: cmd
run: rmdir /s /q basic
- name: Override git version
if: runner.os != 'windows'
run: __test__/override-git-version.sh
- name: Override git version (Windows)
if: runner.os == 'windows'
run: __test__\\override-git-version.cmd
- name: Checkout basic using REST API
uses: ./
with:
ref: test-data/v2/basic
path: basic
- name: Verify basic
run: __test__/verify-basic.sh --archive
test-proxy:
runs-on: ubuntu-latest
container:
image: alpine/git:latest
options: --dns 127.0.0.1
services:
squid-proxy:
image: ubuntu/squid:latest
ports:
- 3128:3128
env:
https_proxy: http://squid-proxy:3128
steps:
# Clone this repo
- name: Checkout
uses: actions/checkout@v3
# Basic checkout using git
- name: Checkout basic
uses: ./
with:
ref: test-data/v2/basic
path: basic
- name: Verify basic
run: __test__/verify-basic.sh
# Basic checkout using REST API
- name: Remove basic
run: rm -rf basic
- name: Override git version
run: __test__/override-git-version.sh
- name: Basic checkout using REST API
uses: ./
with:
ref: test-data/v2/basic
path: basic
- name: Verify basic
run: __test__/verify-basic.sh --archive
test-bypass-proxy:
runs-on: ubuntu-latest
env:
https_proxy: http://no-such-proxy:3128
no_proxy: api.github.com,github.com
steps:
# Clone this repo
- name: Checkout
uses: actions/checkout@v3
# Basic checkout using git
- name: Checkout basic
uses: ./
with:
ref: test-data/v2/basic
path: basic
- name: Verify basic
run: __test__/verify-basic.sh
- name: Remove basic
run: rm -rf basic
# Basic checkout using REST API
- name: Override git version
run: __test__/override-git-version.sh
- name: Checkout basic using REST API
uses: ./
with:
ref: test-data/v2/basic
path: basic
- name: Verify basic
run: __test__/verify-basic.sh --archive
test-git-container:
runs-on: ubuntu-latest
container: bitnami/git:latest
steps:
# Clone this repo
- name: Checkout
uses: actions/checkout@v3
with:
path: v3
# Basic checkout using git
- name: Checkout basic
uses: ./v3
with:
ref: test-data/v2/basic
- name: Verify basic
run: |
if [ ! -f "./basic-file.txt" ]; then
echo "Expected basic file does not exist"
exit 1
fi
# Verify .git folder
if [ ! -d "./.git" ]; then
echo "Expected ./.git folder to exist"
exit 1
fi
# Verify auth token
git config --global --add safe.directory "*"
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
# needed to make checkout post cleanup succeed
- name: Fix Checkout v3
uses: actions/checkout@v3
with:
path: v3

View File

@ -0,0 +1,32 @@
name: Update Main Version
run-name: Move ${{ github.event.inputs.major_version }} to ${{ github.event.inputs.target }}
on:
workflow_dispatch:
inputs:
target:
description: The tag or reference to use
required: true
major_version:
type: choice
description: The major version to update
options:
- v4
- v3
- v2
jobs:
tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Git config
run: |
git config user.name github-actions
git config user.email github-actions@github.com
- name: Tag new target
run: git tag -f ${{ github.event.inputs.major_version }} ${{ github.event.inputs.target }}
- name: Push new tag
run: git push origin ${{ github.event.inputs.major_version }} --force

5
.gitignore vendored
View File

@ -1,2 +1,5 @@
__test__/_temp
_temp/
lib/
node_modules/
node_modules/
.vscode/

14
.licensed.yml Normal file
View File

@ -0,0 +1,14 @@
sources:
npm: true
allowed:
- apache-2.0
- bsd-2-clause
- bsd-3-clause
- isc
- mit
- cc0-1.0
- unlicense
reviewed:
npm:

BIN
.licenses/npm/@actions/core.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@actions/exec.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@actions/github.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@actions/http-client.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@actions/io.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@actions/tool-cache.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@octokit/auth-token.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@octokit/core.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@octokit/endpoint.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@octokit/graphql.dep.yml generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.licenses/npm/@octokit/request.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/@octokit/types.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/before-after-hook.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/call-bind.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/deprecation.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/function-bind.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/get-intrinsic.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/has-symbols.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/has.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/is-plain-object.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/node-fetch.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/object-inspect.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/once.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/qs.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/semver.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/side-channel.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/tr46.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/tunnel.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/typed-rest-client.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/underscore.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/universal-user-agent.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/uuid-3.3.3.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/uuid-8.3.2.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/webidl-conversions.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/whatwg-url.dep.yml generated Normal file

Binary file not shown.

BIN
.licenses/npm/wrappy.dep.yml generated Normal file

Binary file not shown.

View File

@ -1,5 +1,97 @@
# Changelog
## v4.1.0
- [Add support for partial checkout filters](https://github.com/actions/checkout/pull/1396)
## v4.0.0
- [Support fetching without the --progress option](https://github.com/actions/checkout/pull/1067)
- [Update to node20](https://github.com/actions/checkout/pull/1436)
## v3.6.0
- [Fix: Mark test scripts with Bash'isms to be run via Bash](https://github.com/actions/checkout/pull/1377)
- [Add option to fetch tags even if fetch-depth > 0](https://github.com/actions/checkout/pull/579)
## v3.5.3
- [Fix: Checkout fail in self-hosted runners when faulty submodule are checked-in](https://github.com/actions/checkout/pull/1196)
- [Fix typos found by codespell](https://github.com/actions/checkout/pull/1287)
- [Add support for sparse checkouts](https://github.com/actions/checkout/pull/1369)
## v3.5.2
- [Fix api endpoint for GHES](https://github.com/actions/checkout/pull/1289)
## v3.5.1
- [Fix slow checkout on Windows](https://github.com/actions/checkout/pull/1246)
## v3.5.0
* [Add new public key for known_hosts](https://github.com/actions/checkout/pull/1237)
## v3.4.0
- [Upgrade codeql actions to v2](https://github.com/actions/checkout/pull/1209)
- [Upgrade dependencies](https://github.com/actions/checkout/pull/1210)
- [Upgrade @actions/io](https://github.com/actions/checkout/pull/1225)
## v3.3.0
- [Implement branch list using callbacks from exec function](https://github.com/actions/checkout/pull/1045)
- [Add in explicit reference to private checkout options](https://github.com/actions/checkout/pull/1050)
- [Fix comment typos (that got added in #770)](https://github.com/actions/checkout/pull/1057)
## v3.2.0
- [Add GitHub Action to perform release](https://github.com/actions/checkout/pull/942)
- [Fix status badge](https://github.com/actions/checkout/pull/967)
- [Replace datadog/squid with ubuntu/squid Docker image](https://github.com/actions/checkout/pull/1002)
- [Wrap pipeline commands for submoduleForeach in quotes](https://github.com/actions/checkout/pull/964)
- [Update @actions/io to 1.1.2](https://github.com/actions/checkout/pull/1029)
- [Upgrading version to 3.2.0](https://github.com/actions/checkout/pull/1039)
## v3.1.0
- [Use @actions/core `saveState` and `getState`](https://github.com/actions/checkout/pull/939)
- [Add `github-server-url` input](https://github.com/actions/checkout/pull/922)
## v3.0.2
- [Add input `set-safe-directory`](https://github.com/actions/checkout/pull/770)
## v3.0.1
- [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)
- [Bumped various npm package versions](https://github.com/actions/checkout/pull/744)
## v3.0.0
- [Update to node 16](https://github.com/actions/checkout/pull/689)
## v2.3.1
- [Fix default branch resolution for .wiki and when using SSH](https://github.com/actions/checkout/pull/284)
## v2.3.0
- [Fallback to the default branch](https://github.com/actions/checkout/pull/278)
## v2.2.0
- [Fetch all history for all tags and branches when fetch-depth=0](https://github.com/actions/checkout/pull/258)
## v2.1.1
- Changes to support GHES ([here](https://github.com/actions/checkout/pull/236) and [here](https://github.com/actions/checkout/pull/248))
## v2.1.0
- [Group output](https://github.com/actions/checkout/pull/191)
- [Changes to support GHES alpha release](https://github.com/actions/checkout/pull/199)
- [Persist core.sshCommand for submodules](https://github.com/actions/checkout/pull/184)
- [Add support ssh](https://github.com/actions/checkout/pull/163)
- [Convert submodule SSH URL to HTTPS, when not using SSH](https://github.com/actions/checkout/pull/179)
- [Add submodule support](https://github.com/actions/checkout/pull/157)
- [Follow proxy settings](https://github.com/actions/checkout/pull/144)
- [Fix ref for pr closed event when a pr is merged](https://github.com/actions/checkout/pull/141)
- [Fix issue checking detached when git less than 2.22](https://github.com/actions/checkout/pull/128)
## v2.0.0
- [Do not pass cred on command line](https://github.com/actions/checkout/pull/108)
- [Add input persist-credentials](https://github.com/actions/checkout/pull/107)
- [Fallback to REST API to download repo](https://github.com/actions/checkout/pull/104)
## v2 (beta)
- Improved fetch performance

1
CODEOWNERS Normal file
View File

@ -0,0 +1 @@
* @actions/actions-launch

28
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,28 @@
# Contributing
## Submitting a pull request
1. Fork and clone the repository
1. Configure and install the dependencies: `npm install`
1. Create a new branch: `git checkout -b my-branch-name`
1. Make your change, add tests, and make sure the tests still pass: `npm run test`
1. Make sure your code is correctly formatted: `npm run format`
1. Update `dist/index.js` using `npm run build`. This creates a single javascript file that is used as an entrypoint for the action
1. Push to your fork and submit a pull request
1. Pat yourself on the back and wait for your pull request to be reviewed and merged
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
- Write tests.
- 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.
## Resources
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [GitHub Help](https://help.github.com)
- [Writing good commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
Thanks! :heart: :heart: :heart:
GitHub Actions Team :octocat:

265
README.md
View File

@ -1,53 +1,71 @@
<p align="center">
<a href="https://github.com/actions/checkout"><img alt="GitHub Actions status" src="https://github.com/actions/checkout/workflows/test-local/badge.svg"></a>
</p>
[![Build and Test](https://github.com/actions/checkout/actions/workflows/test.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/test.yml)
# Checkout V2 beta
# Checkout V4
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
By default, the repository that triggered the workflow is checked-out, for the ref/SHA that triggered the event.
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.
Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events.
The auth token is persisted in the local git config. This enables your scripts to run authenticated git commands. The token is removed during post-job cleanup. Set `persist-credentials: false` to opt-out.
When Git 2.18 or higher is not in your PATH, falls back to the REST API to download the files.
# What's new
- Improved fetch performance
- The default behavior now fetches only the SHA being checked-out
- Script authenticated git commands
- Persists `with.token` in the local git config
- Enables your scripts to run authenticated git commands
- Post-job cleanup removes the token
- Coming soon: Opt out by setting `with.persist-credentials` to `false`
- Creates a local branch
- No longer detached HEAD when checking out a branch
- A local branch is created with the corresponding upstream branch set
- Improved layout
- `with.path` is always relative to `github.workspace`
- Aligns better with container actions, where `github.workspace` gets mapped in
- Removed input `submodules`
Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous versions.
Please refer to the [release page](https://github.com/actions/checkout/releases/latest) for the latest release notes.
# Usage
<!-- start usage -->
```yaml
- uses: actions/checkout@v2-beta
- uses: actions/checkout@v4
with:
# Repository name with owner. For example, actions/checkout
# Default: ${{ github.repository }}
repository: ''
# The branch, tag or SHA to checkout. When checking out the repository that
# The branch, tag or SHA to checkout. When checking out the repository that
# triggered a workflow, this defaults to the reference or SHA for that event.
# Otherwise, defaults to `master`.
# Otherwise, uses the default branch.
ref: ''
# Access token for clone repository
# Personal access token (PAT) used to fetch the repository. The PAT is configured
# with the local git config, which enables your scripts to run authenticated git
# commands. The post-job step removes the PAT.
#
# We recommend using a service account with the least permissions necessary. Also
# when generating a new PAT, select the least scopes necessary.
#
# [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
#
# Default: ${{ github.token }}
token: ''
# SSH key used to fetch the repository. The SSH key is configured with the local
# git config, which enables your scripts to run authenticated git commands. The
# post-job step removes the SSH key.
#
# We recommend using a service account with the least permissions necessary.
#
# [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
ssh-key: ''
# Known hosts in addition to the user and global host key database. The public SSH
# keys for a host may be obtained using the utility `ssh-keyscan`. For example,
# `ssh-keyscan github.com`. The public key for github.com is always implicitly
# added.
ssh-known-hosts: ''
# Whether to perform strict host key checking. When true, adds the options
# `StrictHostKeyChecking=yes` and `CheckHostIP=no` to the SSH command line. Use
# the input `ssh-known-hosts` to configure additional hosts.
# Default: true
ssh-strict: ''
# Whether to configure the token or SSH key with the local git config
# Default: true
persist-credentials: ''
# Relative path under $GITHUB_WORKSPACE to place the repository
path: ''
@ -55,41 +73,212 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
# Default: true
clean: ''
# Number of commits to fetch. 0 indicates all history.
# Partially clone against a given filter. Overrides sparse-checkout if set.
# Default: null
filter: ''
# Do a sparse checkout on given patterns. Each pattern should be separated with
# new lines.
# Default: null
sparse-checkout: ''
# Specifies whether to use cone-mode when doing a sparse checkout.
# Default: true
sparse-checkout-cone-mode: ''
# Number of commits to fetch. 0 indicates all history for all branches and tags.
# Default: 1
fetch-depth: ''
# Whether to fetch tags, even if fetch-depth > 0.
# Default: false
fetch-tags: ''
# Whether to show progress status output when fetching.
# Default: true
show-progress: ''
# Whether to download Git-LFS files
# Default: false
lfs: ''
# Whether to checkout submodules: `true` to checkout submodules or `recursive` to
# recursively checkout submodules.
#
# When the `ssh-key` input is not provided, SSH URLs beginning with
# `git@github.com:` are converted to HTTPS.
#
# Default: false
submodules: ''
# Add repository path as safe.directory for Git global config by running `git
# config --global --add safe.directory <path>`
# Default: true
set-safe-directory: ''
# 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
github-server-url: ''
```
<!-- end usage -->
# Scenarios
- [Fetch only the root files](#Fetch-only-the-root-files)
- [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder)
- [Fetch only a single file](#Fetch-only-a-single-file)
- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches)
- [Checkout a different branch](#Checkout-a-different-branch)
- [Checkout HEAD^](#Checkout-HEAD)
- [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side)
- [Checkout multiple repos (nested)](#Checkout-multiple-repos-nested)
- [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
- [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit)
- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token)
## Fetch only the root files
```yaml
- uses: actions/checkout@v4
with:
sparse-checkout: .
```
## Fetch only the root files and `.github` and `src` folder
```yaml
- uses: actions/checkout@v4
with:
sparse-checkout: |
.github
src
```
## Fetch only a single file
```yaml
- uses: actions/checkout@v4
with:
sparse-checkout: |
README.md
sparse-checkout-cone-mode: false
```
## Fetch all history for all tags and branches
```yaml
- uses: actions/checkout@v4
with:
fetch-depth: 0
```
## Checkout a different branch
```yaml
- uses: actions/checkout@v2-beta
- uses: actions/checkout@v4
with:
ref: some-branch
ref: my-branch
```
## Checkout a different, private repository
## Checkout HEAD^
```yaml
- uses: actions/checkout@v2-beta
- uses: actions/checkout@v4
with:
repository: myAccount/myRepository
ref: refs/heads/master
token: ${{ secrets.GitHub_PAT }} # `GitHub_PAT` is a secret that contains your PAT
fetch-depth: 2
- run: git checkout HEAD^
```
> - `${{ github.token }}` is scoped to the current repository, so if you want to checkout another repository that is private you will need to provide your own [PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line).
## Checkout the HEAD commit of a PR, rather than the merge commit
## Checkout multiple repos (side by side)
```yaml
- uses: actions/checkout@v2-beta
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.after }}
path: main
- name: Checkout tools repo
uses: actions/checkout@v4
with:
repository: my-org/my-tools
path: my-tools
```
> - If your secondary repository is private you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
## Checkout multiple repos (nested)
```yaml
- name: Checkout
uses: actions/checkout@v4
- name: Checkout tools repo
uses: actions/checkout@v4
with:
repository: my-org/my-tools
path: my-tools
```
> - If your secondary repository is private you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
## Checkout multiple repos (private)
```yaml
- name: Checkout
uses: actions/checkout@v4
with:
path: main
- name: Checkout private tools
uses: actions/checkout@v4
with:
repository: my-org/my-private-tools
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
path: my-tools
```
> - `${{ github.token }}` is scoped to the current repository, so if you want to checkout a different repository that is private you will need to provide your own [PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line).
## Checkout pull request HEAD commit instead of merge commit
```yaml
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
```
## Checkout pull request on closed event
```yaml
on:
pull_request:
branches: [main]
types: [opened, synchronize, closed]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
```
## Push a commit using the built-in token
```yaml
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
date > generated.txt
git config user.name github-actions
git config user.email github-actions@github.com
git add .
git commit -m "generated"
git push
```
# License

View File

@ -0,0 +1,853 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import * as gitAuthHelper from '../lib/git-auth-helper'
import * as io from '@actions/io'
import * as os from 'os'
import * as path from 'path'
import * as stateHelper from '../lib/state-helper'
import {IGitCommandManager} from '../lib/git-command-manager'
import {IGitSourceSettings} from '../lib/git-source-settings'
const isWindows = process.platform === 'win32'
const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper')
const originalRunnerTemp = process.env['RUNNER_TEMP']
const originalHome = process.env['HOME']
let workspace: string
let localGitConfigPath: string
let globalGitConfigPath: string
let runnerTemp: string
let tempHomedir: string
let git: IGitCommandManager & {env: {[key: string]: string}}
let settings: IGitSourceSettings
let sshPath: string
let githubServerUrl: string
describe('git-auth-helper tests', () => {
beforeAll(async () => {
// SSH
sshPath = await io.which('ssh')
// Clear test workspace
await io.rmRF(testWorkspace)
})
beforeEach(() => {
// Mock setSecret
jest.spyOn(core, 'setSecret').mockImplementation((secret: string) => {})
// Mock error/warning/info/debug
jest.spyOn(core, 'error').mockImplementation(jest.fn())
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
jest.spyOn(core, 'info').mockImplementation(jest.fn())
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
// Mock state helper
jest.spyOn(stateHelper, 'setSshKeyPath').mockImplementation(jest.fn())
jest
.spyOn(stateHelper, 'setSshKnownHostsPath')
.mockImplementation(jest.fn())
})
afterEach(() => {
// Unregister mocks
jest.restoreAllMocks()
// Restore HOME
if (originalHome) {
process.env['HOME'] = originalHome
} else {
delete process.env['HOME']
}
})
afterAll(() => {
// Restore RUNNER_TEMP
delete process.env['RUNNER_TEMP']
if (originalRunnerTemp) {
process.env['RUNNER_TEMP'] = originalRunnerTemp
}
})
async function testAuthHeader(
testName: string,
serverUrl: string | undefined = undefined
) {
// Arrange
let expectedServerUrl = 'https://github.com'
if (serverUrl) {
githubServerUrl = serverUrl
expectedServerUrl = githubServerUrl
}
await setup(testName)
expect(settings.authToken).toBeTruthy() // sanity check
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureAuth()
// Assert config
const configContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
const basicCredential = Buffer.from(
`x-access-token:${settings.authToken}`,
'utf8'
).toString('base64')
expect(
configContent.indexOf(
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
}
const configureAuth_configuresAuthHeader =
'configureAuth configures auth header'
it(configureAuth_configuresAuthHeader, async () => {
await testAuthHeader(configureAuth_configuresAuthHeader)
})
const configureAuth_AcceptsGitHubServerUrl =
'inject https://my-ghes-server.com as github server url'
it(configureAuth_AcceptsGitHubServerUrl, async () => {
await testAuthHeader(
configureAuth_AcceptsGitHubServerUrl,
'https://my-ghes-server.com'
)
})
const configureAuth_AcceptsGitHubServerUrlSetToGHEC =
'inject https://github.com as github server url'
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
await testAuthHeader(
configureAuth_AcceptsGitHubServerUrl,
'https://github.com'
)
})
const configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse =
'configureAuth configures auth header even when persist credentials false'
it(
configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse,
async () => {
// Arrange
await setup(
configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse
)
expect(settings.authToken).toBeTruthy() // sanity check
settings.persistCredentials = false
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureAuth()
// Assert config
const configContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
expect(
configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION`
)
).toBeGreaterThanOrEqual(0)
}
)
const configureAuth_copiesUserKnownHosts =
'configureAuth copies user known hosts'
it(configureAuth_copiesUserKnownHosts, async () => {
if (!sshPath) {
process.stdout.write(
`Skipped test "${configureAuth_copiesUserKnownHosts}". Executable 'ssh' not found in the PATH.\n`
)
return
}
// Arange
await setup(configureAuth_copiesUserKnownHosts)
expect(settings.sshKey).toBeTruthy() // sanity check
// Mock fs.promises.readFile
const realReadFile = fs.promises.readFile
jest.spyOn(fs.promises, 'readFile').mockImplementation(
async (file: any, options: any): Promise<Buffer> => {
const userKnownHostsPath = path.join(
os.homedir(),
'.ssh',
'known_hosts'
)
if (file === userKnownHostsPath) {
return Buffer.from('some-domain.com ssh-rsa ABCDEF')
}
return await realReadFile(file, options)
}
)
// Act
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
// Assert known hosts
const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
const actualSshKnownHostsContent = (
await fs.promises.readFile(actualSshKnownHostsPath)
).toString()
expect(actualSshKnownHostsContent).toMatch(
/some-domain\.com ssh-rsa ABCDEF/
)
expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
})
const configureAuth_registersBasicCredentialAsSecret =
'configureAuth registers basic credential as secret'
it(configureAuth_registersBasicCredentialAsSecret, async () => {
// Arrange
await setup(configureAuth_registersBasicCredentialAsSecret)
expect(settings.authToken).toBeTruthy() // sanity check
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureAuth()
// Assert secret
const setSecretSpy = core.setSecret as jest.Mock<any, any>
expect(setSecretSpy).toHaveBeenCalledTimes(1)
const expectedSecret = Buffer.from(
`x-access-token:${settings.authToken}`,
'utf8'
).toString('base64')
expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret)
})
const setsSshCommandEnvVarWhenPersistCredentialsFalse =
'sets SSH command env var when persist-credentials false'
it(setsSshCommandEnvVarWhenPersistCredentialsFalse, async () => {
if (!sshPath) {
process.stdout.write(
`Skipped test "${setsSshCommandEnvVarWhenPersistCredentialsFalse}". Executable 'ssh' not found in the PATH.\n`
)
return
}
// Arrange
await setup(setsSshCommandEnvVarWhenPersistCredentialsFalse)
settings.persistCredentials = false
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureAuth()
// Assert git env var
const actualKeyPath = await getActualSshKeyPath()
const actualKnownHostsPath = await getActualSshKnownHostsPath()
const expectedSshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
actualKeyPath
)}" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
actualKnownHostsPath
)}"`
expect(git.setEnvironmentVariable).toHaveBeenCalledWith(
'GIT_SSH_COMMAND',
expectedSshCommand
)
// Asserty git config
const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
.toString()
.split('\n')
.filter(x => x)
expect(gitConfigLines).toHaveLength(1)
expect(gitConfigLines[0]).toMatch(/^http\./)
})
const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
'sets SSH command when persist-credentials true'
it(configureAuth_setsSshCommandWhenPersistCredentialsTrue, async () => {
if (!sshPath) {
process.stdout.write(
`Skipped test "${configureAuth_setsSshCommandWhenPersistCredentialsTrue}". Executable 'ssh' not found in the PATH.\n`
)
return
}
// Arrange
await setup(configureAuth_setsSshCommandWhenPersistCredentialsTrue)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureAuth()
// Assert git env var
const actualKeyPath = await getActualSshKeyPath()
const actualKnownHostsPath = await getActualSshKnownHostsPath()
const expectedSshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
actualKeyPath
)}" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
actualKnownHostsPath
)}"`
expect(git.setEnvironmentVariable).toHaveBeenCalledWith(
'GIT_SSH_COMMAND',
expectedSshCommand
)
// Asserty git config
expect(git.config).toHaveBeenCalledWith(
'core.sshCommand',
expectedSshCommand
)
})
const configureAuth_writesExplicitKnownHosts = 'writes explicit known hosts'
it(configureAuth_writesExplicitKnownHosts, async () => {
if (!sshPath) {
process.stdout.write(
`Skipped test "${configureAuth_writesExplicitKnownHosts}". Executable 'ssh' not found in the PATH.\n`
)
return
}
// Arrange
await setup(configureAuth_writesExplicitKnownHosts)
expect(settings.sshKey).toBeTruthy() // sanity check
settings.sshKnownHosts = 'my-custom-host.com ssh-rsa ABC123'
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureAuth()
// Assert known hosts
const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
const actualSshKnownHostsContent = (
await fs.promises.readFile(actualSshKnownHostsPath)
).toString()
expect(actualSshKnownHostsContent).toMatch(
/my-custom-host\.com ssh-rsa ABC123/
)
expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
})
const configureAuth_writesSshKeyAndImplicitKnownHosts =
'writes SSH key and implicit known hosts'
it(configureAuth_writesSshKeyAndImplicitKnownHosts, async () => {
if (!sshPath) {
process.stdout.write(
`Skipped test "${configureAuth_writesSshKeyAndImplicitKnownHosts}". Executable 'ssh' not found in the PATH.\n`
)
return
}
// Arrange
await setup(configureAuth_writesSshKeyAndImplicitKnownHosts)
expect(settings.sshKey).toBeTruthy() // sanity check
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureAuth()
// Assert SSH key
const actualSshKeyPath = await getActualSshKeyPath()
expect(actualSshKeyPath).toBeTruthy()
const actualSshKeyContent = (
await fs.promises.readFile(actualSshKeyPath)
).toString()
expect(actualSshKeyContent).toBe(settings.sshKey + '\n')
if (!isWindows) {
// Assert read/write for user, not group or others.
// Otherwise SSH client will error.
expect((await fs.promises.stat(actualSshKeyPath)).mode & 0o777).toBe(
0o600
)
}
// Assert known hosts
const actualSshKnownHostsPath = await getActualSshKnownHostsPath()
const actualSshKnownHostsContent = (
await fs.promises.readFile(actualSshKnownHostsPath)
).toString()
expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/)
})
const configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet =
'configureGlobalAuth configures URL insteadOf when SSH key not set'
it(configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet, async () => {
// Arrange
await setup(configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet)
settings.sshKey = ''
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureAuth()
await authHelper.configureGlobalAuth()
// Assert temporary global config
expect(git.env['HOME']).toBeTruthy()
const configContent = (
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
).toString()
expect(
configContent.indexOf(`url.https://github.com/.insteadOf git@github.com`)
).toBeGreaterThanOrEqual(0)
})
const configureGlobalAuth_copiesGlobalGitConfig =
'configureGlobalAuth copies global git config'
it(configureGlobalAuth_copiesGlobalGitConfig, async () => {
// Arrange
await setup(configureGlobalAuth_copiesGlobalGitConfig)
await fs.promises.writeFile(globalGitConfigPath, 'value-from-global-config')
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureAuth()
await authHelper.configureGlobalAuth()
// Assert original global config not altered
let configContent = (
await fs.promises.readFile(globalGitConfigPath)
).toString()
expect(configContent).toBe('value-from-global-config')
// Assert temporary global config
expect(git.env['HOME']).toBeTruthy()
const basicCredential = Buffer.from(
`x-access-token:${settings.authToken}`,
'utf8'
).toString('base64')
configContent = (
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
).toString()
expect(
configContent.indexOf('value-from-global-config')
).toBeGreaterThanOrEqual(0)
expect(
configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
})
const configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist =
'configureGlobalAuth creates new git config when global does not exist'
it(
configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist,
async () => {
// Arrange
await setup(
configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist
)
await io.rmRF(globalGitConfigPath)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
// Act
await authHelper.configureAuth()
await authHelper.configureGlobalAuth()
// Assert original global config not recreated
try {
await fs.promises.stat(globalGitConfigPath)
throw new Error(
`Did not expect file to exist: '${globalGitConfigPath}'`
)
} catch (err) {
if ((err as any)?.code !== 'ENOENT') {
throw err
}
}
// Assert temporary global config
expect(git.env['HOME']).toBeTruthy()
const basicCredential = Buffer.from(
`x-access-token:${settings.authToken}`,
'utf8'
).toString('base64')
const configContent = (
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
).toString()
expect(
configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
}
)
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet =
'configureSubmoduleAuth configures submodules when persist credentials false and SSH key not set'
it(
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet,
async () => {
// Arrange
await setup(
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeyNotSet
)
settings.persistCredentials = false
settings.sshKey = ''
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
mockSubmoduleForeach.mockClear() // reset calls
// Act
await authHelper.configureSubmoduleAuth()
// Assert
expect(mockSubmoduleForeach).toBeCalledTimes(1)
expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch(
/unset-all.*insteadOf/
)
}
)
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet =
'configureSubmoduleAuth configures submodules when persist credentials false and SSH key set'
it(
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet,
async () => {
if (!sshPath) {
process.stdout.write(
`Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
)
return
}
// Arrange
await setup(
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsFalseAndSshKeySet
)
settings.persistCredentials = false
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
mockSubmoduleForeach.mockClear() // reset calls
// Act
await authHelper.configureSubmoduleAuth()
// Assert
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(1)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
}
)
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet =
'configureSubmoduleAuth configures submodules when persist credentials true and SSH key not set'
it(
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet,
async () => {
// Arrange
await setup(
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeyNotSet
)
settings.sshKey = ''
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
mockSubmoduleForeach.mockClear() // reset calls
// Act
await authHelper.configureSubmoduleAuth()
// Assert
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
/url.*insteadOf.*git@github.com:/
)
expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
/url.*insteadOf.*org-123456@github.com:/
)
}
)
const configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet =
'configureSubmoduleAuth configures submodules when persist credentials true and SSH key set'
it(
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet,
async () => {
if (!sshPath) {
process.stdout.write(
`Skipped test "${configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n`
)
return
}
// Arrange
await setup(
configureSubmoduleAuth_configuresSubmodulesWhenPersistCredentialsTrueAndSshKeySet
)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any>
mockSubmoduleForeach.mockClear() // reset calls
// Act
await authHelper.configureSubmoduleAuth()
// Assert
expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
}
)
const removeAuth_removesSshCommand = 'removeAuth removes SSH command'
it(removeAuth_removesSshCommand, async () => {
if (!sshPath) {
process.stdout.write(
`Skipped test "${removeAuth_removesSshCommand}". Executable 'ssh' not found in the PATH.\n`
)
return
}
// Arrange
await setup(removeAuth_removesSshCommand)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
let gitConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
expect(gitConfigContent.indexOf('core.sshCommand')).toBeGreaterThanOrEqual(
0
) // sanity check
const actualKeyPath = await getActualSshKeyPath()
expect(actualKeyPath).toBeTruthy()
await fs.promises.stat(actualKeyPath)
const actualKnownHostsPath = await getActualSshKnownHostsPath()
expect(actualKnownHostsPath).toBeTruthy()
await fs.promises.stat(actualKnownHostsPath)
// Act
await authHelper.removeAuth()
// Assert git config
gitConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
expect(gitConfigContent.indexOf('core.sshCommand')).toBeLessThan(0)
// Assert SSH key file
try {
await fs.promises.stat(actualKeyPath)
throw new Error('SSH key should have been deleted')
} catch (err) {
if ((err as any)?.code !== 'ENOENT') {
throw err
}
}
// Assert known hosts file
try {
await fs.promises.stat(actualKnownHostsPath)
throw new Error('SSH known hosts should have been deleted')
} catch (err) {
if ((err as any)?.code !== 'ENOENT') {
throw err
}
}
})
const removeAuth_removesToken = 'removeAuth removes token'
it(removeAuth_removesToken, async () => {
// Arrange
await setup(removeAuth_removesToken)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
let gitConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
// Act
await authHelper.removeAuth()
// Assert git config
gitConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
})
const removeGlobalConfig_removesOverride =
'removeGlobalConfig removes override'
it(removeGlobalConfig_removesOverride, async () => {
// Arrange
await setup(removeGlobalConfig_removesOverride)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
await authHelper.configureGlobalAuth()
const homeOverride = git.env['HOME'] // Sanity check
expect(homeOverride).toBeTruthy()
await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig'))
// Act
await authHelper.removeGlobalConfig()
// Assert
expect(git.env['HOME']).toBeUndefined()
try {
await fs.promises.stat(homeOverride)
throw new Error(`Should have been deleted '${homeOverride}'`)
} catch (err) {
if ((err as any)?.code !== 'ENOENT') {
throw err
}
}
})
})
async function setup(testName: string): Promise<void> {
testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-')
// Directories
workspace = path.join(testWorkspace, testName, 'workspace')
runnerTemp = path.join(testWorkspace, testName, 'runner-temp')
tempHomedir = path.join(testWorkspace, testName, 'home-dir')
await fs.promises.mkdir(workspace, {recursive: true})
await fs.promises.mkdir(runnerTemp, {recursive: true})
await fs.promises.mkdir(tempHomedir, {recursive: true})
process.env['RUNNER_TEMP'] = runnerTemp
process.env['HOME'] = tempHomedir
// Create git config
globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
await fs.promises.writeFile(globalGitConfigPath, '')
localGitConfigPath = path.join(workspace, '.git', 'config')
await fs.promises.mkdir(path.dirname(localGitConfigPath), {recursive: true})
await fs.promises.writeFile(localGitConfigPath, '')
git = {
branchDelete: jest.fn(),
branchExists: jest.fn(),
branchList: jest.fn(),
sparseCheckout: jest.fn(),
sparseCheckoutNonConeMode: jest.fn(),
checkout: jest.fn(),
checkoutDetach: jest.fn(),
config: jest.fn(
async (key: string, value: string, globalConfig?: boolean) => {
const configPath = globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath
await fs.promises.appendFile(configPath, `\n${key} ${value}`)
}
),
configExists: jest.fn(
async (key: string, globalConfig?: boolean): Promise<boolean> => {
const configPath = globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath
const content = await fs.promises.readFile(configPath)
const lines = content
.toString()
.split('\n')
.filter(x => x)
return lines.some(x => x.startsWith(key))
}
),
env: {},
fetch: jest.fn(),
getDefaultBranch: jest.fn(),
getWorkingDirectory: jest.fn(() => workspace),
init: jest.fn(),
isDetached: jest.fn(),
lfsFetch: jest.fn(),
lfsInstall: jest.fn(),
log1: jest.fn(),
remoteAdd: jest.fn(),
removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]),
revParse: jest.fn(),
setEnvironmentVariable: jest.fn((name: string, value: string) => {
git.env[name] = value
}),
shaExists: jest.fn(),
submoduleForeach: jest.fn(async () => {
return ''
}),
submoduleSync: jest.fn(),
submoduleStatus: jest.fn(async () => {
return true
}),
submoduleUpdate: jest.fn(),
tagExists: jest.fn(),
tryClean: jest.fn(),
tryConfigUnset: jest.fn(
async (key: string, globalConfig?: boolean): Promise<boolean> => {
const configPath = globalConfig
? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
: localGitConfigPath
let content = await fs.promises.readFile(configPath)
let lines = content
.toString()
.split('\n')
.filter(x => x)
.filter(x => !x.startsWith(key))
await fs.promises.writeFile(configPath, lines.join('\n'))
return true
}
),
tryDisableAutomaticGarbageCollection: jest.fn(),
tryGetFetchUrl: jest.fn(),
tryReset: jest.fn()
}
settings = {
authToken: 'some auth token',
clean: true,
commit: '',
filter: undefined,
sparseCheckout: [],
sparseCheckoutConeMode: true,
fetchDepth: 1,
fetchTags: false,
showProgress: true,
lfs: false,
submodules: false,
nestedSubmodules: false,
persistCredentials: true,
ref: 'refs/heads/main',
repositoryName: 'my-repo',
repositoryOwner: 'my-org',
repositoryPath: '',
sshKey: sshPath ? 'some ssh private key' : '',
sshKnownHosts: '',
sshStrict: true,
workflowOrganizationId: 123456,
setSafeDirectory: true,
githubServerUrl: githubServerUrl
}
}
async function getActualSshKeyPath(): Promise<string> {
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
.sort()
.map(x => path.join(runnerTemp, x))
if (actualTempFiles.length === 0) {
return ''
}
expect(actualTempFiles).toHaveLength(2)
expect(actualTempFiles[0].endsWith('_known_hosts')).toBeFalsy()
return actualTempFiles[0]
}
async function getActualSshKnownHostsPath(): Promise<string> {
let actualTempFiles = (await fs.promises.readdir(runnerTemp))
.sort()
.map(x => path.join(runnerTemp, x))
if (actualTempFiles.length === 0) {
return ''
}
expect(actualTempFiles).toHaveLength(2)
expect(actualTempFiles[1].endsWith('_known_hosts')).toBeTruthy()
expect(actualTempFiles[1].startsWith(actualTempFiles[0])).toBeTruthy()
return actualTempFiles[1]
}

View File

@ -0,0 +1,378 @@
import * as exec from '@actions/exec'
import * as fshelper from '../lib/fs-helper'
import * as commandManager from '../lib/git-command-manager'
let git: commandManager.IGitCommandManager
let mockExec = jest.fn()
describe('git-auth-helper tests', () => {
beforeAll(async () => {})
beforeEach(async () => {
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
})
afterEach(() => {
jest.restoreAllMocks()
})
afterAll(() => {})
it('branch list matches', async () => {
mockExec.mockImplementation((path, args, options) => {
console.log(args, options.listeners.stdout)
if (args.includes('version')) {
options.listeners.stdout(Buffer.from('2.18'))
return 0
}
if (args.includes('rev-parse')) {
options.listeners.stdline(Buffer.from('refs/heads/foo'))
options.listeners.stdline(Buffer.from('refs/heads/bar'))
return 0
}
return 1
})
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
let branches = await git.branchList(false)
expect(branches).toHaveLength(2)
expect(branches.sort()).toEqual(['foo', 'bar'].sort())
})
it('ambiguous ref name output is captured', async () => {
mockExec.mockImplementation((path, args, options) => {
console.log(args, options.listeners.stdout)
if (args.includes('version')) {
options.listeners.stdout(Buffer.from('2.18'))
return 0
}
if (args.includes('rev-parse')) {
options.listeners.stdline(Buffer.from('refs/heads/foo'))
// If refs/tags/v1 and refs/heads/tags/v1 existed on this repository
options.listeners.errline(
Buffer.from("error: refname 'tags/v1' is ambiguous")
)
return 0
}
return 1
})
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
let branches = await git.branchList(false)
expect(branches).toHaveLength(1)
expect(branches.sort()).toEqual(['foo'].sort())
})
})
describe('Test fetchDepth and fetchTags options', () => {
beforeEach(async () => {
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
mockExec.mockImplementation((path, args, options) => {
console.log(args, options.listeners.stdout)
if (args.includes('version')) {
options.listeners.stdout(Buffer.from('2.18'))
}
return 0
})
})
afterEach(() => {
jest.restoreAllMocks()
})
it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is true', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
const refSpec = ['refspec1', 'refspec2']
const options = {
filter: 'filterValue',
fetchDepth: 0,
fetchTags: true
}
await git.fetch(refSpec, options)
expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--prune',
'--no-recurse-submodules',
'--filter=filterValue',
'origin',
'refspec1',
'refspec2'
],
expect.any(Object)
)
})
it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is false', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
const refSpec = ['refspec1', 'refspec2']
const options = {
filter: 'filterValue',
fetchDepth: 0,
fetchTags: false
}
await git.fetch(refSpec, options)
expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--no-tags',
'--prune',
'--no-recurse-submodules',
'--filter=filterValue',
'origin',
'refspec1',
'refspec2'
],
expect.any(Object)
)
})
it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is false', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
const refSpec = ['refspec1', 'refspec2']
const options = {
filter: 'filterValue',
fetchDepth: 1,
fetchTags: false
}
await git.fetch(refSpec, options)
expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--no-tags',
'--prune',
'--no-recurse-submodules',
'--filter=filterValue',
'--depth=1',
'origin',
'refspec1',
'refspec2'
],
expect.any(Object)
)
})
it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is true', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
const refSpec = ['refspec1', 'refspec2']
const options = {
filter: 'filterValue',
fetchDepth: 1,
fetchTags: true
}
await git.fetch(refSpec, options)
expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--prune',
'--no-recurse-submodules',
'--filter=filterValue',
'--depth=1',
'origin',
'refspec1',
'refspec2'
],
expect.any(Object)
)
})
it('should call execGit with the correct arguments when showProgress is true', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
const refSpec = ['refspec1', 'refspec2']
const options = {
filter: 'filterValue',
showProgress: true
}
await git.fetch(refSpec, options)
expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--no-tags',
'--prune',
'--no-recurse-submodules',
'--progress',
'--filter=filterValue',
'origin',
'refspec1',
'refspec2'
],
expect.any(Object)
)
})
it('should call execGit with the correct arguments when fetchDepth is 42 and showProgress is true', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
const refSpec = ['refspec1', 'refspec2']
const options = {
filter: 'filterValue',
fetchDepth: 42,
showProgress: true
}
await git.fetch(refSpec, options)
expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--no-tags',
'--prune',
'--no-recurse-submodules',
'--progress',
'--filter=filterValue',
'--depth=42',
'origin',
'refspec1',
'refspec2'
],
expect.any(Object)
)
})
it('should call execGit with the correct arguments when fetchTags is true and showProgress is true', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
const refSpec = ['refspec1', 'refspec2']
const options = {
filter: 'filterValue',
fetchTags: true,
showProgress: true
}
await git.fetch(refSpec, options)
expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--prune',
'--no-recurse-submodules',
'--progress',
'--filter=filterValue',
'origin',
'refspec1',
'refspec2'
],
expect.any(Object)
)
})
})

View File

@ -0,0 +1,505 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import * as gitDirectoryHelper from '../lib/git-directory-helper'
import * as io from '@actions/io'
import * as path from 'path'
import {IGitCommandManager} from '../lib/git-command-manager'
const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper')
let repositoryPath: string
let repositoryUrl: string
let clean: boolean
let ref: string
let git: IGitCommandManager
describe('git-directory-helper tests', () => {
beforeAll(async () => {
// Clear test workspace
await io.rmRF(testWorkspace)
})
beforeEach(() => {
// Mock error/warning/info/debug
jest.spyOn(core, 'error').mockImplementation(jest.fn())
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
jest.spyOn(core, 'info').mockImplementation(jest.fn())
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
})
afterEach(() => {
// Unregister mocks
jest.restoreAllMocks()
})
const cleansWhenCleanTrue = 'cleans when clean true'
it(cleansWhenCleanTrue, async () => {
// Arrange
await setup(cleansWhenCleanTrue)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files.sort()).toEqual(['.git', 'my-file'])
expect(git.tryClean).toHaveBeenCalled()
expect(git.tryReset).toHaveBeenCalled()
expect(core.warning).not.toHaveBeenCalled()
})
const checkoutDetachWhenNotDetached = 'checkout detach when not detached'
it(checkoutDetachWhenNotDetached, async () => {
// Arrange
await setup(checkoutDetachWhenNotDetached)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files.sort()).toEqual(['.git', 'my-file'])
expect(git.checkoutDetach).toHaveBeenCalled()
})
const doesNotCheckoutDetachWhenNotAlreadyDetached =
'does not checkout detach when already detached'
it(doesNotCheckoutDetachWhenNotAlreadyDetached, async () => {
// Arrange
await setup(doesNotCheckoutDetachWhenNotAlreadyDetached)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
const mockIsDetached = git.isDetached as jest.Mock<any, any>
mockIsDetached.mockImplementation(async () => {
return true
})
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files.sort()).toEqual(['.git', 'my-file'])
expect(git.checkoutDetach).not.toHaveBeenCalled()
})
const doesNotCleanWhenCleanFalse = 'does not clean when clean false'
it(doesNotCleanWhenCleanFalse, async () => {
// Arrange
await setup(doesNotCleanWhenCleanFalse)
clean = false
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files.sort()).toEqual(['.git', 'my-file'])
expect(git.isDetached).toHaveBeenCalled()
expect(git.branchList).toHaveBeenCalled()
expect(core.warning).not.toHaveBeenCalled()
expect(git.tryClean).not.toHaveBeenCalled()
expect(git.tryReset).not.toHaveBeenCalled()
})
const removesContentsWhenCleanFails = 'removes contents when clean fails'
it(removesContentsWhenCleanFails, async () => {
// Arrange
await setup(removesContentsWhenCleanFails)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
let mockTryClean = git.tryClean as jest.Mock<any, any>
mockTryClean.mockImplementation(async () => {
return false
})
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files).toHaveLength(0)
expect(git.tryClean).toHaveBeenCalled()
expect(core.warning).toHaveBeenCalled()
expect(git.tryReset).not.toHaveBeenCalled()
})
const removesContentsWhenDifferentRepositoryUrl =
'removes contents when different repository url'
it(removesContentsWhenDifferentRepositoryUrl, async () => {
// Arrange
await setup(removesContentsWhenDifferentRepositoryUrl)
clean = false
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
const differentRepositoryUrl =
'https://github.com/my-different-org/my-different-repo'
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
differentRepositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files).toHaveLength(0)
expect(core.warning).not.toHaveBeenCalled()
expect(git.isDetached).not.toHaveBeenCalled()
})
const removesContentsWhenNoGitDirectory =
'removes contents when no git directory'
it(removesContentsWhenNoGitDirectory, async () => {
// Arrange
await setup(removesContentsWhenNoGitDirectory)
clean = false
await io.rmRF(path.join(repositoryPath, '.git'))
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files).toHaveLength(0)
expect(core.warning).not.toHaveBeenCalled()
expect(git.isDetached).not.toHaveBeenCalled()
})
const removesContentsWhenResetFails = 'removes contents when reset fails'
it(removesContentsWhenResetFails, async () => {
// Arrange
await setup(removesContentsWhenResetFails)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
let mockTryReset = git.tryReset as jest.Mock<any, any>
mockTryReset.mockImplementation(async () => {
return false
})
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files).toHaveLength(0)
expect(git.tryClean).toHaveBeenCalled()
expect(git.tryReset).toHaveBeenCalled()
expect(core.warning).toHaveBeenCalled()
})
const removesContentsWhenUndefinedGitCommandManager =
'removes contents when undefined git command manager'
it(removesContentsWhenUndefinedGitCommandManager, async () => {
// Arrange
await setup(removesContentsWhenUndefinedGitCommandManager)
clean = false
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
// Act
await gitDirectoryHelper.prepareExistingDirectory(
undefined,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files).toHaveLength(0)
expect(core.warning).not.toHaveBeenCalled()
})
const removesLocalBranches = 'removes local branches'
it(removesLocalBranches, async () => {
// Arrange
await setup(removesLocalBranches)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
const mockBranchList = git.branchList as jest.Mock<any, any>
mockBranchList.mockImplementation(async (remote: boolean) => {
return remote ? [] : ['local-branch-1', 'local-branch-2']
})
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files.sort()).toEqual(['.git', 'my-file'])
expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-1')
expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-2')
})
const cleanWhenSubmoduleStatusIsFalse =
'cleans when submodule status is false'
it(cleanWhenSubmoduleStatusIsFalse, async () => {
// Arrange
await setup(cleanWhenSubmoduleStatusIsFalse)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
//mock bad submodule
const submoduleStatus = git.submoduleStatus as jest.Mock<any, any>
submoduleStatus.mockImplementation(async (remote: boolean) => {
return false
})
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files).toHaveLength(0)
expect(git.tryClean).toHaveBeenCalled()
})
const doesNotCleanWhenSubmoduleStatusIsTrue =
'does not clean when submodule status is true'
it(doesNotCleanWhenSubmoduleStatusIsTrue, async () => {
// Arrange
await setup(doesNotCleanWhenSubmoduleStatusIsTrue)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
const submoduleStatus = git.submoduleStatus as jest.Mock<any, any>
submoduleStatus.mockImplementation(async (remote: boolean) => {
return true
})
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files.sort()).toEqual(['.git', 'my-file'])
expect(git.tryClean).toHaveBeenCalled()
})
const removesLockFiles = 'removes lock files'
it(removesLockFiles, async () => {
// Arrange
await setup(removesLockFiles)
clean = false
await fs.promises.writeFile(
path.join(repositoryPath, '.git', 'index.lock'),
''
)
await fs.promises.writeFile(
path.join(repositoryPath, '.git', 'shallow.lock'),
''
)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
let files = await fs.promises.readdir(path.join(repositoryPath, '.git'))
expect(files).toHaveLength(0)
files = await fs.promises.readdir(repositoryPath)
expect(files.sort()).toEqual(['.git', 'my-file'])
expect(git.isDetached).toHaveBeenCalled()
expect(git.branchList).toHaveBeenCalled()
expect(core.warning).not.toHaveBeenCalled()
expect(git.tryClean).not.toHaveBeenCalled()
expect(git.tryReset).not.toHaveBeenCalled()
})
const removesAncestorRemoteBranch = 'removes ancestor remote branch'
it(removesAncestorRemoteBranch, async () => {
// Arrange
await setup(removesAncestorRemoteBranch)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
const mockBranchList = git.branchList as jest.Mock<any, any>
mockBranchList.mockImplementation(async (remote: boolean) => {
return remote ? ['origin/remote-branch-1', 'origin/remote-branch-2'] : []
})
ref = 'remote-branch-1/conflict'
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files.sort()).toEqual(['.git', 'my-file'])
expect(git.branchDelete).toHaveBeenCalledTimes(1)
expect(git.branchDelete).toHaveBeenCalledWith(
true,
'origin/remote-branch-1'
)
})
const removesDescendantRemoteBranches = 'removes descendant remote branch'
it(removesDescendantRemoteBranches, async () => {
// Arrange
await setup(removesDescendantRemoteBranches)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
const mockBranchList = git.branchList as jest.Mock<any, any>
mockBranchList.mockImplementation(async (remote: boolean) => {
return remote
? ['origin/remote-branch-1/conflict', 'origin/remote-branch-2']
: []
})
ref = 'remote-branch-1'
// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files.sort()).toEqual(['.git', 'my-file'])
expect(git.branchDelete).toHaveBeenCalledTimes(1)
expect(git.branchDelete).toHaveBeenCalledWith(
true,
'origin/remote-branch-1/conflict'
)
})
})
async function setup(testName: string): Promise<void> {
testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-')
// Repository directory
repositoryPath = path.join(testWorkspace, testName)
await fs.promises.mkdir(path.join(repositoryPath, '.git'), {recursive: true})
// Repository URL
repositoryUrl = 'https://github.com/my-org/my-repo'
// Clean
clean = true
// Ref
ref = ''
// Git command manager
git = {
branchDelete: jest.fn(),
branchExists: jest.fn(),
branchList: jest.fn(async () => {
return []
}),
sparseCheckout: jest.fn(),
sparseCheckoutNonConeMode: jest.fn(),
checkout: jest.fn(),
checkoutDetach: jest.fn(),
config: jest.fn(),
configExists: jest.fn(),
fetch: jest.fn(),
getDefaultBranch: jest.fn(),
getWorkingDirectory: jest.fn(() => repositoryPath),
init: jest.fn(),
isDetached: jest.fn(),
lfsFetch: jest.fn(),
lfsInstall: jest.fn(),
log1: jest.fn(),
remoteAdd: jest.fn(),
removeEnvironmentVariable: jest.fn(),
revParse: jest.fn(),
setEnvironmentVariable: jest.fn(),
shaExists: jest.fn(),
submoduleForeach: jest.fn(),
submoduleSync: jest.fn(),
submoduleUpdate: jest.fn(),
submoduleStatus: jest.fn(async () => {
return true
}),
tagExists: jest.fn(),
tryClean: jest.fn(async () => {
return true
}),
tryConfigUnset: jest.fn(),
tryDisableAutomaticGarbageCollection: jest.fn(),
tryGetFetchUrl: jest.fn(async () => {
// Sanity check - this function shouldn't be called when the .git directory doesn't exist
await fs.promises.stat(path.join(repositoryPath, '.git'))
return repositoryUrl
}),
tryReset: jest.fn(async () => {
return true
})
}
}

View File

@ -1,47 +1,55 @@
import * as assert from 'assert'
import * as core from '@actions/core'
import * as fsHelper from '../lib/fs-helper'
import * as github from '@actions/github'
import * as inputHelper from '../lib/input-helper'
import * as path from 'path'
import {ISourceSettings} from '../lib/git-source-provider'
import * as workflowContextHelper from '../lib/workflow-context-helper'
import {IGitSourceSettings} from '../lib/git-source-settings'
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
// Late bind
let inputHelper: any
// Mock @actions/core
// Inputs for mock @actions/core
let inputs = {} as any
const mockCore = jest.genMockFromModule('@actions/core') as any
mockCore.getInput = (name: string) => {
return inputs[name]
}
// Mock @actions/github
const mockGitHub = jest.genMockFromModule('@actions/github') as any
mockGitHub.context = {
repo: {
owner: 'some-owner',
repo: 'some-repo'
},
ref: 'refs/heads/some-ref',
sha: '1234567890123456789012345678901234567890'
}
// Mock ./fs-helper
const mockFSHelper = jest.genMockFromModule('../lib/fs-helper') as any
mockFSHelper.directoryExistsSync = (path: string) => path == gitHubWorkspace
// Shallow clone original @actions/github context
let originalContext = {...github.context}
describe('input-helper tests', () => {
beforeAll(() => {
// Mock getInput
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
return inputs[name]
})
// Mock error/warning/info/debug
jest.spyOn(core, 'error').mockImplementation(jest.fn())
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
jest.spyOn(core, 'info').mockImplementation(jest.fn())
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
// Mock github context
jest.spyOn(github.context, 'repo', 'get').mockImplementation(() => {
return {
owner: 'some-owner',
repo: 'some-repo'
}
})
github.context.ref = 'refs/heads/some-ref'
github.context.sha = '1234567890123456789012345678901234567890'
// Mock ./fs-helper directoryExistsSync()
jest
.spyOn(fsHelper, 'directoryExistsSync')
.mockImplementation((path: string) => path == gitHubWorkspace)
// Mock ./workflowContextHelper getOrganizationId()
jest
.spyOn(workflowContextHelper, 'getOrganizationId')
.mockImplementation(() => Promise.resolve(123456))
// GitHub workspace
process.env['GITHUB_WORKSPACE'] = gitHubWorkspace
// Mocks
jest.setMock('@actions/core', mockCore)
jest.setMock('@actions/github', mockGitHub)
jest.setMock('../lib/fs-helper', mockFSHelper)
// Now import
inputHelper = require('../lib/input-helper')
})
beforeEach(() => {
@ -50,71 +58,90 @@ describe('input-helper tests', () => {
})
afterAll(() => {
// Reset GitHub workspace
// Restore GitHub workspace
delete process.env['GITHUB_WORKSPACE']
if (originalGitHubWorkspace) {
process.env['GITHUB_WORKSPACE'] = originalGitHubWorkspace
}
// Reset modules
jest.resetModules()
// Restore @actions/github context
github.context.ref = originalContext.ref
github.context.sha = originalContext.sha
// Restore
jest.restoreAllMocks()
})
it('sets defaults', () => {
const settings: ISourceSettings = inputHelper.getInputs()
it('sets defaults', async () => {
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings).toBeTruthy()
expect(settings.accessToken).toBeFalsy()
expect(settings.authToken).toBeFalsy()
expect(settings.clean).toBe(true)
expect(settings.commit).toBeTruthy()
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
expect(settings.filter).toBe(undefined)
expect(settings.sparseCheckout).toBe(undefined)
expect(settings.sparseCheckoutConeMode).toBe(true)
expect(settings.fetchDepth).toBe(1)
expect(settings.fetchTags).toBe(false)
expect(settings.showProgress).toBe(true)
expect(settings.lfs).toBe(false)
expect(settings.ref).toBe('refs/heads/some-ref')
expect(settings.repositoryName).toBe('some-repo')
expect(settings.repositoryOwner).toBe('some-owner')
expect(settings.repositoryPath).toBe(gitHubWorkspace)
expect(settings.setSafeDirectory).toBe(true)
})
it('requires qualified repo', () => {
it('qualifies ref', async () => {
let originalRef = github.context.ref
try {
github.context.ref = 'some-unqualified-ref'
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings).toBeTruthy()
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
expect(settings.ref).toBe('refs/heads/some-unqualified-ref')
} finally {
github.context.ref = originalRef
}
})
it('requires qualified repo', async () => {
inputs.repository = 'some-unqualified-repo'
assert.throws(() => {
inputHelper.getInputs()
}, /Invalid repository 'some-unqualified-repo'/)
try {
await inputHelper.getInputs()
throw 'should not reach here'
} catch (err) {
expect(`(${(err as any).message}`).toMatch(
"Invalid repository 'some-unqualified-repo'"
)
}
})
it('roots path', () => {
it('roots path', async () => {
inputs.path = 'some-directory/some-subdirectory'
const settings: ISourceSettings = inputHelper.getInputs()
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.repositoryPath).toBe(
path.join(gitHubWorkspace, 'some-directory', 'some-subdirectory')
)
})
it('sets correct default ref/sha for other repo', () => {
inputs.repository = 'some-owner/some-other-repo'
const settings: ISourceSettings = inputHelper.getInputs()
expect(settings.ref).toBe('refs/heads/master')
expect(settings.commit).toBeFalsy()
})
it('sets ref to empty when explicit sha', () => {
it('sets ref to empty when explicit sha', async () => {
inputs.ref = '1111111111222222222233333333334444444444'
const settings: ISourceSettings = inputHelper.getInputs()
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.ref).toBeFalsy()
expect(settings.commit).toBe('1111111111222222222233333333334444444444')
})
it('sets sha to empty when explicit ref', () => {
it('sets sha to empty when explicit ref', async () => {
inputs.ref = 'refs/heads/some-other-ref'
const settings: ISourceSettings = inputHelper.getInputs()
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.ref).toBe('refs/heads/some-other-ref')
expect(settings.commit).toBeFalsy()
})
it('gives good error message for submodules input', () => {
inputs.submodules = 'true'
assert.throws(() => {
inputHelper.getInputs()
}, /The input 'submodules' is not supported/)
it('sets workflow organization ID', async () => {
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.workflowOrganizationId).toBe(123456)
})
})

View File

@ -0,0 +1,6 @@
mkdir override-git-version
cd override-git-version
echo @echo override git version 1.2.3 > git.cmd
echo "%CD%" >> $GITHUB_PATH
cd ..

View File

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

View File

@ -16,7 +16,7 @@ describe('ref-helper tests', () => {
await refHelper.getCheckoutInfo(git, 'refs/heads/my/branch', commit)
throw new Error('Should not reach here')
} catch (err) {
expect(err.message).toBe('Arg git cannot be empty')
expect((err as any)?.message).toBe('Arg git cannot be empty')
}
})
@ -25,7 +25,9 @@ describe('ref-helper tests', () => {
await refHelper.getCheckoutInfo(git, '', '')
throw new Error('Should not reach here')
} catch (err) {
expect(err.message).toBe('Args ref and commit cannot both be empty')
expect((err as any)?.message).toBe(
'Args ref and commit cannot both be empty'
)
}
})
@ -102,7 +104,7 @@ describe('ref-helper tests', () => {
await refHelper.getCheckoutInfo(git, 'my-ref', '')
throw new Error('Should not reach here')
} catch (err) {
expect(err.message).toBe(
expect((err as any)?.message).toBe(
"A branch or tag with the name 'my-ref' could not be found"
)
}

View File

@ -0,0 +1,87 @@
import * as core from '@actions/core'
import {RetryHelper} from '../lib/retry-helper'
let info: string[]
let retryHelper: any
describe('retry-helper tests', () => {
beforeAll(() => {
// Mock @actions/core info()
jest.spyOn(core, 'info').mockImplementation((message: string) => {
info.push(message)
})
retryHelper = new RetryHelper(3, 0, 0)
})
beforeEach(() => {
// Reset info
info = []
})
afterAll(() => {
// Restore
jest.restoreAllMocks()
})
it('first attempt succeeds', async () => {
const actual = await retryHelper.execute(async () => {
return 'some result'
})
expect(actual).toBe('some result')
expect(info).toHaveLength(0)
})
it('second attempt succeeds', async () => {
let attempts = 0
const actual = await retryHelper.execute(() => {
if (++attempts == 1) {
throw new Error('some error')
}
return Promise.resolve('some result')
})
expect(attempts).toBe(2)
expect(actual).toBe('some result')
expect(info).toHaveLength(2)
expect(info[0]).toBe('some error')
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
})
it('third attempt succeeds', async () => {
let attempts = 0
const actual = await retryHelper.execute(() => {
if (++attempts < 3) {
throw new Error(`some error ${attempts}`)
}
return Promise.resolve('some result')
})
expect(attempts).toBe(3)
expect(actual).toBe('some result')
expect(info).toHaveLength(4)
expect(info[0]).toBe('some error 1')
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
expect(info[2]).toBe('some error 2')
expect(info[3]).toMatch(/Waiting .+ seconds before trying again/)
})
it('all attempts fail succeeds', async () => {
let attempts = 0
let error: Error = (null as unknown) as Error
try {
await retryHelper.execute(() => {
throw new Error(`some error ${++attempts}`)
})
} catch (err) {
error = err as Error
}
expect(error.message).toBe('some error 3')
expect(attempts).toBe(3)
expect(info).toHaveLength(4)
expect(info[0]).toBe('some error 1')
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
expect(info[2]).toBe('some error 2')
expect(info[3]).toMatch(/Waiting .+ seconds before trying again/)
})
})

View File

@ -1,10 +1,24 @@
#!/bin/bash
#!/bin/sh
if [ ! -f "./basic/basic-file.txt" ]; then
echo "Expected basic file does not exist"
exit 1
fi
# Verify auth token
cd basic
git fetch
if [ "$1" = "--archive" ]; then
# Verify no .git folder
if [ -d "./basic/.git" ]; then
echo "Did not expect ./basic/.git folder to exist"
exit 1
fi
else
# Verify .git folder
if [ ! -d "./basic/.git" ]; then
echo "Expected ./basic/.git folder to exist"
exit 1
fi
# Verify auth token
cd basic
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
fi

16
__test__/verify-fetch-filter.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
# Verify .git folder
if [ ! -d "./fetch-filter/.git" ]; then
echo "Expected ./fetch-filter/.git folder to exist"
exit 1
fi
# Verify .git/config contains partialclonefilter
CLONE_FILTER=$(git -C fetch-filter config --local --get remote.origin.partialclonefilter)
if [ "$CLONE_FILTER" != "blob:none" ]; then
echo "Expected ./fetch-filter/.git/config to have 'remote.origin.partialclonefilter' set to 'blob:none'"
exit 1
fi

View File

@ -12,6 +12,6 @@ if [[ "$(git status --porcelain)" != "" ]]; then
echo ----------------------------------------
echo Troubleshooting
echo ----------------------------------------
echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run all"
echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run format && npm run build"
exit 1
fi

View File

@ -0,0 +1,51 @@
#!/bin/bash
# Verify .git folder
if [ ! -d "./sparse-checkout-non-cone-mode/.git" ]; then
echo "Expected ./sparse-checkout-non-cone-mode/.git folder to exist"
exit 1
fi
# Verify sparse-checkout (non-cone-mode)
cd sparse-checkout-non-cone-mode
ENABLED=$(git config --local --get-all core.sparseCheckout)
if [ "$?" != "0" ]; then
echo "Failed to verify that sparse-checkout is enabled"
exit 1
fi
# Check that sparse-checkout is enabled
if [ "$ENABLED" != "true" ]; then
echo "Expected sparse-checkout to be enabled (is: $ENABLED)"
exit 1
fi
SPARSE_CHECKOUT_FILE=$(git rev-parse --git-path info/sparse-checkout)
if [ "$?" != "0" ]; then
echo "Failed to validate sparse-checkout"
exit 1
fi
# Check that sparse-checkout list is not empty
if [ ! -f "$SPARSE_CHECKOUT_FILE" ]; then
echo "Expected sparse-checkout file to exist"
exit 1
fi
# Check that all folders from sparse-checkout exists
for pattern in $(cat "$SPARSE_CHECKOUT_FILE")
do
if [ ! -d "${pattern#/}" ]; then
echo "Expected directory '${pattern#/}' to exist"
exit 1
fi
done
# Verify that the root directory is not checked out
if [ -f README.md ]; then
echo "Expected top-level files not to exist"
exit 1
fi

View File

@ -0,0 +1,63 @@
#!/bin/bash
# Verify .git folder
if [ ! -d "./sparse-checkout/.git" ]; then
echo "Expected ./sparse-checkout/.git folder to exist"
exit 1
fi
# Verify sparse-checkout
cd sparse-checkout
SPARSE=$(git sparse-checkout list)
if [ "$?" != "0" ]; then
echo "Failed to validate sparse-checkout"
exit 1
fi
# Check that sparse-checkout list is not empty
if [ -z "$SPARSE" ]; then
echo "Expected sparse-checkout list to not be empty"
exit 1
fi
# Check that all folders of the sparse checkout exist
for pattern in $SPARSE
do
if [ ! -d "$pattern" ]; then
echo "Expected directory '$pattern' to exist"
exit 1
fi
done
checkSparse () {
if [ ! -d "./$1" ]; then
echo "Expected directory '$1' to exist"
exit 1
fi
for file in $(git ls-tree -r --name-only HEAD $1)
do
if [ ! -f "$file" ]; then
echo "Expected file '$file' to exist"
exit 1
fi
done
}
# Check that all folders and their children have been checked out
checkSparse __test__
checkSparse .github
checkSparse dist
# Check that only sparse-checkout folders have been checked out
for pattern in $(git ls-tree --name-only HEAD)
do
if [ -d "$pattern" ]; then
if [[ "$pattern" != "__test__" && "$pattern" != ".github" && "$pattern" != "dist" ]]; then
echo "Expected directory '$pattern' to not exist"
exit 1
fi
fi
done

View File

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

View File

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

View File

@ -0,0 +1,26 @@
#!/bin/bash
if [ ! -f "./submodules-recursive/regular-file.txt" ]; then
echo "Expected regular file does not exist"
exit 1
fi
if [ ! -f "./submodules-recursive/submodule-level-1/submodule-file.txt" ]; then
echo "Expected submodule file does not exist"
exit 1
fi
if [ ! -f "./submodules-recursive/submodule-level-1/submodule-level-2/nested-submodule-file.txt" ]; then
echo "Expected nested submodule file does not exists"
exit 1
fi
echo "Testing persisted credential"
pushd ./submodules-recursive/submodule-level-1/submodule-level-2
git config --local --name-only --get-regexp http.+extraheader && git fetch
if [ "$?" != "0" ]; then
echo "Failed to validate persisted credential"
popd
exit 1
fi
popd

View File

@ -0,0 +1,26 @@
#!/bin/bash
if [ ! -f "./submodules-true/regular-file.txt" ]; then
echo "Expected regular file does not exist"
exit 1
fi
if [ ! -f "./submodules-true/submodule-level-1/submodule-file.txt" ]; then
echo "Expected submodule file does not exist"
exit 1
fi
if [ -f "./submodules-true/submodule-level-1/submodule-level-2/nested-submodule-file.txt" ]; then
echo "Unexpected nested submodule file exists"
exit 1
fi
echo "Testing persisted credential"
pushd ./submodules-true/submodule-level-1
git config --local --name-only --get-regexp http.+extraheader && git fetch
if [ "$?" != "0" ]; then
echo "Failed to validate persisted credential"
popd
exit 1
fi
popd

View File

@ -1,29 +1,100 @@
name: 'Checkout'
description: 'Checkout a Git repository at a particular version'
inputs:
inputs:
repository:
description: 'Repository name with owner. For example, actions/checkout'
default: ${{ github.repository }}
ref:
description: >
The branch, tag or SHA to checkout. When checking out the repository
that triggered a workflow, this defaults to the reference or SHA for
that event. Otherwise, defaults to `master`.
The branch, tag or SHA to checkout. When checking out the repository that
triggered a workflow, this defaults to the reference or SHA for that
event. Otherwise, uses the default branch.
token:
description: 'Access token for clone repository'
description: >
Personal access token (PAT) used to fetch the repository. The PAT is configured
with the local git config, which enables your scripts to run authenticated git
commands. The post-job step removes the PAT.
We recommend using a service account with the least permissions necessary.
Also when generating a new PAT, select the least scopes necessary.
[Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
default: ${{ github.token }}
ssh-key:
description: >
SSH key used to fetch the repository. The SSH key is configured with the local
git config, which enables your scripts to run authenticated git commands.
The post-job step removes the SSH key.
We recommend using a service account with the least permissions necessary.
[Learn more about creating and using
encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
ssh-known-hosts:
description: >
Known hosts in addition to the user and global host key database. The public
SSH keys for a host may be obtained using the utility `ssh-keyscan`. For example,
`ssh-keyscan github.com`. The public key for github.com is always implicitly added.
ssh-strict:
description: >
Whether to perform strict host key checking. When true, adds the options `StrictHostKeyChecking=yes`
and `CheckHostIP=no` to the SSH command line. Use the input `ssh-known-hosts` to
configure additional hosts.
default: true
persist-credentials:
description: 'Whether to configure the token or SSH key with the local git config'
default: true
path:
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
clean:
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
default: true
filter:
description: >
Partially clone against a given filter.
Overrides sparse-checkout if set.
default: null
sparse-checkout:
description: >
Do a sparse checkout on given patterns.
Each pattern should be separated with new lines.
default: null
sparse-checkout-cone-mode:
description: >
Specifies whether to use cone-mode when doing a sparse checkout.
default: true
fetch-depth:
description: 'Number of commits to fetch. 0 indicates all history.'
description: 'Number of commits to fetch. 0 indicates all history for all branches and tags.'
default: 1
fetch-tags:
description: 'Whether to fetch tags, even if fetch-depth > 0.'
default: false
show-progress:
description: 'Whether to show progress status output when fetching.'
default: true
lfs:
description: 'Whether to download Git-LFS files'
default: false
submodules:
description: >
Whether to checkout submodules: `true` to checkout submodules or `recursive` to
recursively checkout submodules.
When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are
converted to HTTPS.
default: false
set-safe-directory:
description: Add repository path as safe.directory for Git global config by running `git config --global --add safe.directory <path>`
default: true
github-server-url:
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
required: false
runs:
using: node12
using: node20
main: dist/index.js
post: dist/index.js

290
adrs/0153-checkout-v2.md Normal file
View File

@ -0,0 +1,290 @@
# ADR 0153: Checkout v2
**Date**: 2019-10-21
**Status**: Accepted
## Context
This ADR details the behavior for `actions/checkout@v2`.
The new action will be written in typescript. We are moving away from runner-plugin actions.
We want to take this opportunity to make behavioral changes, from v1. This document is scoped to those differences.
## Decision
### Inputs
```yaml
repository:
description: 'Repository name with owner. For example, actions/checkout'
default: ${{ github.repository }}
ref:
description: >
The branch, tag or SHA to checkout. When checking out the repository that
triggered a workflow, this defaults to the reference or SHA for that
event. Otherwise, uses the default branch.
token:
description: >
Personal access token (PAT) used to fetch the repository. The PAT is configured
with the local git config, which enables your scripts to run authenticated git
commands. The post-job step removes the PAT.
We recommend using a service account with the least permissions necessary.
Also when generating a new PAT, select the least scopes necessary.
[Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
default: ${{ github.token }}
ssh-key:
description: >
SSH key used to fetch the repository. The SSH key is configured with the local
git config, which enables your scripts to run authenticated git commands.
The post-job step removes the SSH key.
We recommend using a service account with the least permissions necessary.
[Learn more about creating and using
encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
ssh-known-hosts:
description: >
Known hosts in addition to the user and global host key database. The public
SSH keys for a host may be obtained using the utility `ssh-keyscan`. For example,
`ssh-keyscan github.com`. The public key for github.com is always implicitly added.
ssh-strict:
description: >
Whether to perform strict host key checking. When true, adds the options `StrictHostKeyChecking=yes`
and `CheckHostIP=no` to the SSH command line. Use the input `ssh-known-hosts` to
configure additional hosts.
default: true
persist-credentials:
description: 'Whether to configure the token or SSH key with the local git config'
default: true
path:
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
clean:
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
default: true
fetch-depth:
description: 'Number of commits to fetch. 0 indicates all history for all tags and branches.'
default: 1
lfs:
description: 'Whether to download Git-LFS files'
default: false
submodules:
description: >
Whether to checkout submodules: `true` to checkout submodules or `recursive` to
recursively checkout submodules.
When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are
converted to HTTPS.
default: false
```
Note:
- SSH support is new
- `persist-credentials` is new
- `path` behavior is different (refer [below](#path) for details)
### Fallback to GitHub API
When a sufficient version of git is not in the PATH, fallback to the [web API](https://developer.github.com/v3/repos/contents/#get-archive-link) to download a tarball/zipball.
Note:
- LFS files are not included in the archive. Therefore fail if LFS is set to true.
- Submodules are also not included in the archive.
### Persist credentials
The credentials will be persisted on disk. This will allow users to script authenticated git commands, like `git fetch`.
A post script will remove the credentials (cleanup for self-hosted).
Users may opt-out by specifying `persist-credentials: false`
Note:
- Users scripting `git commit` may need to set the username and email. The service does not provide any reasonable default value. Users can add `git config user.name <NAME>` and `git config user.email <EMAIL>`. We will document this guidance.
#### PAT
When using the `${{github.token}}` or a PAT, the token will be persisted in the local git config. The config key `http.https://github.com/.extraheader` enables an auth header to be specified on all authenticated commands `AUTHORIZATION: basic <BASE64_U:P>`.
Note:
- The auth header is scoped to all of github `http.https://github.com/.extraheader`
- Additional public remotes also just work.
- If users want to authenticate to an additional private remote, they should provide the `token` input.
#### SSH key
The SSH key will be written to disk under the `$RUNNER_TEMP` directory. The SSH key will
be removed by the action's post-job hook. Additionally, RUNNER_TEMP is cleared by the
runner between jobs.
The SSH key must be written with strict file permissions. The SSH client requires the file
to be read/write for the user, and not accessible by others.
The user host key database (`~/.ssh/known_hosts`) will be copied to a unique file under
`$RUNNER_TEMP`. And values from the input `ssh-known-hosts` will be added to the file.
The SSH command will be overridden for the local git config:
```sh
git config core.sshCommand 'ssh -i "$RUNNER_TEMP/path-to-ssh-key" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/path-to-known-hosts"'
```
When the input `ssh-strict` is set to `false`, the options `CheckHostIP` and `StrictHostKeyChecking` will not be overridden.
Note:
- When `ssh-strict` is set to `true` (default), the SSH option `CheckHostIP` can safely be disabled.
Strict host checking verifies the server's public key. Therefore, IP verification is unnecessary
and noisy. For example:
> Warning: Permanently added the RSA host key for IP address '140.82.113.4' to the list of known hosts.
- Since GIT_SSH_COMMAND overrides core.sshCommand, temporarily set the env var when fetching the repo. When creds
are persisted, core.sshCommand is leveraged to avoid multiple checkout steps stomping over each other.
- Modify actions/runner to mount RUNNER_TEMP to enable scripting authenticated git commands from a container action.
- Refer [here](https://linux.die.net/man/5/ssh_config) for SSH config details.
### Fetch behavior
Fetch only the SHA being built and set depth=1. This significantly reduces the fetch time for large repos.
If a SHA isn't available (e.g. multi repo), then fetch only the specified ref with depth=1.
The input `fetch-depth` can be used to control the depth.
Note:
- Fetching a single commit is supported by Git wire protocol version 2. The git client uses protocol version 0 by default. The desired protocol version can be overridden in the git config or on the fetch command line invocation (`-c protocol.version=2`). We will override on the fetch command line, for transparency.
- Git client version 2.18+ (released June 2018) is required for wire protocol version 2.
### Checkout behavior
For CI, checkout will create a local ref with the upstream set. This allows users to script git as they normally would.
For PR, continue to checkout detached head. The PR branch is special - the branch and merge commit are created by the server. It doesn't match a users' local workflow.
Note:
- Consider deleting all local refs during cleanup if that helps avoid collisions. More testing required.
### Path
For the mainline scenario, the disk-layout behavior remains the same.
Remember, given the repo `johndoe/foo`, the mainline disk layout looks like:
```
GITHUB_WORKSPACE=/home/runner/work/foo/foo
RUNNER_WORKSPACE=/home/runner/work/foo
```
V2 introduces a new constraint on the checkout path. The location must now be under `github.workspace`. Whereas the checkout@v1 constraint was one level up, under `runner.workspace`.
V2 no longer changes `github.workspace` to follow wherever the self repo is checked-out.
These behavioral changes align better with container actions. The [documented filesystem contract](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/virtual-environments-for-github-hosted-runners#docker-container-filesystem) is:
- `/github/home`
- `/github/workspace` - Note: GitHub Actions must be run by the default Docker user (root). Ensure your Dockerfile does not set the USER instruction, otherwise you will not be able to access `GITHUB_WORKSPACE`.
- `/github/workflow`
Note:
- The tracking config will not be updated to reflect the path of the workflow repo.
- Any existing workflow repo will not be moved when the checkout path changes. In fact some customers want to checkout the workflow repo twice, side by side against different branches.
- Actions that need to operate only against the root of the self repo, should expose a `path` input.
#### Default value for `path` input
The `path` input will default to `./` which is rooted against `github.workspace`.
This default fits the mainline scenario well: single checkout
For multi-checkout, users must specify the `path` input for at least one of the repositories.
Note:
- An alternative is for the self repo to default to `./` and other repos default to `<REPO_NAME>`. However nested layout is an atypical git layout and therefore is not a good default. Users should supply the path info.
#### Example - Nested layout
The following example checks-out two repositories and creates a nested layout.
```yaml
# Self repo - Checkout to $GITHUB_WORKSPACE
- uses: checkout@v2
# Other repo - Checkout to $GITHUB_WORKSPACE/myscripts
- uses: checkout@v2
with:
repository: myorg/myscripts
path: myscripts
```
#### Example - Side by side layout
The following example checks-out two repositories and creates a side-by-side layout.
```yaml
# Self repo - Checkout to $GITHUB_WORKSPACE/foo
- uses: checkout@v2
with:
path: foo
# Other repo - Checkout to $GITHUB_WORKSPACE/myscripts
- uses: checkout@v2
with:
repository: myorg/myscripts
path: myscripts
```
#### Path impact to problem matchers
Problem matchers associate the source files with annotations.
Today the runner verifies the source file is under the `github.workspace`. Otherwise the source file property is dropped.
Multi-checkout complicates the matter. However even today submodules may cause this heuristic to be inaccurate.
A better solution is:
Given a source file path, walk up the directories until the first `.git/config` is found. Check if it matches the self repo (`url = https://github.com/OWNER/REPO`). If not, drop the source file path.
### Submodules
With both PAT and SSH key support, we should be able to provide frictionless support for
submodules scenarios: recursive, non-recursive, relative submodule paths.
When fetching submodules, follow the `fetch-depth` settings.
Also when fetching submodules, if the `ssh-key` input is not provided then convert SSH URLs to HTTPS: `-c url."https://github.com/".insteadOf "git@github.com:"`
Credentials will be persisted in the submodules local git config too.
### Port to typescript
The checkout action should be a typescript action on the GitHub graph, for the following reasons:
- Enables customers to fork the checkout repo and modify
- Serves as an example for customers
- Demystifies the checkout action manifest
- Simplifies the runner
- Reduce the amount of runner code to port (if we ever do)
Note:
- This means job-container images will need git in the PATH, for checkout.
### Branching strategy and release tags
- Create a servicing branch for V1: `releases/v1`
- Merge the changes into the default branch
- Release using a new tag `preview`
- When stable, release using a new tag `v2`
## Consequences
- Update the checkout action and readme
- Update samples to consume `actions/checkout@v2`
- Job containers now require git in the PATH for checkout, otherwise fallback to REST API
- Minimum git version 2.18
- Update problem matcher logic regarding source file verification (runner)

23017
dist/index.js vendored

File diff suppressed because one or more lines are too long

19047
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,16 @@
{
"name": "checkout",
"version": "2.0.0",
"version": "4.1.0",
"description": "checkout action",
"main": "lib/main.js",
"scripts": {
"build": "tsc",
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts",
"build": "tsc && ncc build && node lib/misc/generate-docs.js",
"format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'",
"lint": "eslint src/**/*.ts",
"pack": "ncc build",
"gendocs": "node lib/misc/generate-docs.js",
"test": "jest",
"all": "npm run build && npm run format && npm run lint && npm run pack && npm run gendocs && npm test"
"licensed-check": "src/misc/licensed-check.sh",
"licensed-generate": "src/misc/licensed-generate.sh"
},
"repository": {
"type": "git",
@ -29,24 +28,28 @@
},
"homepage": "https://github.com/actions/checkout#readme",
"dependencies": {
"@actions/core": "^1.1.3",
"@actions/core": "^1.10.0",
"@actions/exec": "^1.0.1",
"@actions/github": "^1.1.0",
"@actions/io": "^1.0.1"
"@actions/github": "^5.0.0",
"@actions/io": "^1.1.3",
"@actions/tool-cache": "^1.1.2",
"uuid": "^3.3.3"
},
"devDependencies": {
"@types/jest": "^24.0.23",
"@types/node": "^12.7.12",
"@typescript-eslint/parser": "^2.8.0",
"@zeit/ncc": "^0.20.5",
"eslint": "^5.16.0",
"eslint-plugin-github": "^2.0.0",
"eslint-plugin-jest": "^22.21.0",
"jest": "^24.9.0",
"jest-circus": "^24.9.0",
"@types/jest": "^27.0.2",
"@types/node": "^20.5.3",
"@types/uuid": "^3.4.6",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"@vercel/ncc": "^0.36.1",
"eslint": "^7.32.0",
"eslint-plugin-github": "^4.3.2",
"eslint-plugin-jest": "^25.7.0",
"jest": "^27.3.0",
"jest-circus": "^27.3.0",
"js-yaml": "^3.13.1",
"prettier": "^1.19.1",
"ts-jest": "^24.2.0",
"typescript": "^3.6.4"
"ts-jest": "^27.0.7",
"typescript": "^4.4.4"
}
}
}

View File

@ -9,7 +9,7 @@ export function directoryExistsSync(path: string, required?: boolean): boolean {
try {
stats = fs.statSync(path)
} catch (error) {
if (error.code === 'ENOENT') {
if ((error as any)?.code === 'ENOENT') {
if (!required) {
return false
}
@ -18,7 +18,8 @@ export function directoryExistsSync(path: string, required?: boolean): boolean {
}
throw new Error(
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
`Encountered an error when checking whether path '${path}' exists: ${(error as any)
?.message ?? error}`
)
}
@ -39,12 +40,13 @@ export function existsSync(path: string): boolean {
try {
fs.statSync(path)
} catch (error) {
if (error.code === 'ENOENT') {
if ((error as any)?.code === 'ENOENT') {
return false
}
throw new Error(
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
`Encountered an error when checking whether path '${path}' exists: ${(error as any)
?.message ?? error}`
)
}
@ -60,12 +62,13 @@ export function fileExistsSync(path: string): boolean {
try {
stats = fs.statSync(path)
} catch (error) {
if (error.code === 'ENOENT') {
if ((error as any)?.code === 'ENOENT') {
return false
}
throw new Error(
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
`Encountered an error when checking whether path '${path}' exists: ${(error as any)
?.message ?? error}`
)
}

374
src/git-auth-helper.ts Normal file
View File

@ -0,0 +1,374 @@
import * as assert from 'assert'
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as fs from 'fs'
import * as io from '@actions/io'
import * as os from 'os'
import * as path from 'path'
import * as regexpHelper from './regexp-helper'
import * as stateHelper from './state-helper'
import * as urlHelper from './url-helper'
import {default as uuid} from 'uuid/v4'
import {IGitCommandManager} from './git-command-manager'
import {IGitSourceSettings} from './git-source-settings'
const IS_WINDOWS = process.platform === 'win32'
const SSH_COMMAND_KEY = 'core.sshCommand'
export interface IGitAuthHelper {
configureAuth(): Promise<void>
configureGlobalAuth(): Promise<void>
configureSubmoduleAuth(): Promise<void>
configureTempGlobalConfig(): Promise<string>
removeAuth(): Promise<void>
removeGlobalConfig(): Promise<void>
}
export function createAuthHelper(
git: IGitCommandManager,
settings?: IGitSourceSettings
): IGitAuthHelper {
return new GitAuthHelper(git, settings)
}
class GitAuthHelper {
private readonly git: IGitCommandManager
private readonly settings: IGitSourceSettings
private readonly tokenConfigKey: string
private readonly tokenConfigValue: string
private readonly tokenPlaceholderConfigValue: string
private readonly insteadOfKey: string
private readonly insteadOfValues: string[] = []
private sshCommand = ''
private sshKeyPath = ''
private sshKnownHostsPath = ''
private temporaryHomePath = ''
constructor(
gitCommandManager: IGitCommandManager,
gitSourceSettings: IGitSourceSettings | undefined
) {
this.git = gitCommandManager
this.settings = gitSourceSettings || (({} as unknown) as IGitSourceSettings)
// Token auth header
const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl)
this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader` // "origin" is SCHEME://HOSTNAME[:PORT]
const basicCredential = Buffer.from(
`x-access-token:${this.settings.authToken}`,
'utf8'
).toString('base64')
core.setSecret(basicCredential)
this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***`
this.tokenConfigValue = `AUTHORIZATION: basic ${basicCredential}`
// Instead of SSH URL
this.insteadOfKey = `url.${serverUrl.origin}/.insteadOf` // "origin" is SCHEME://HOSTNAME[:PORT]
this.insteadOfValues.push(`git@${serverUrl.hostname}:`)
if (this.settings.workflowOrganizationId) {
this.insteadOfValues.push(
`org-${this.settings.workflowOrganizationId}@github.com:`
)
}
}
async configureAuth(): Promise<void> {
// Remove possible previous values
await this.removeAuth()
// Configure new values
await this.configureSsh()
await this.configureToken()
}
async configureTempGlobalConfig(): Promise<string> {
// Already setup global config
if (this.temporaryHomePath?.length > 0) {
return path.join(this.temporaryHomePath, '.gitconfig')
}
// Create a temp home directory
const runnerTemp = process.env['RUNNER_TEMP'] || ''
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
const uniqueId = uuid()
this.temporaryHomePath = path.join(runnerTemp, uniqueId)
await fs.promises.mkdir(this.temporaryHomePath, {recursive: true})
// Copy the global git config
const gitConfigPath = path.join(
process.env['HOME'] || os.homedir(),
'.gitconfig'
)
const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig')
let configExists = false
try {
await fs.promises.stat(gitConfigPath)
configExists = true
} catch (err) {
if ((err as any)?.code !== 'ENOENT') {
throw err
}
}
if (configExists) {
core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`)
await io.cp(gitConfigPath, newGitConfigPath)
} else {
await fs.promises.writeFile(newGitConfigPath, '')
}
// Override HOME
core.info(
`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
)
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
return newGitConfigPath
}
async configureGlobalAuth(): Promise<void> {
// 'configureTempGlobalConfig' noops if already set, just returns the path
const newGitConfigPath = await this.configureTempGlobalConfig()
try {
// Configure the token
await this.configureToken(newGitConfigPath, true)
// Configure HTTPS instead of SSH
await this.git.tryConfigUnset(this.insteadOfKey, true)
if (!this.settings.sshKey) {
for (const insteadOfValue of this.insteadOfValues) {
await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
}
}
} catch (err) {
// Unset in case somehow written to the real global config
core.info(
'Encountered an error when attempting to configure token. Attempting unconfigure.'
)
await this.git.tryConfigUnset(this.tokenConfigKey, true)
throw err
}
}
async configureSubmoduleAuth(): Promise<void> {
// Remove possible previous HTTPS instead of SSH
await this.removeGitConfig(this.insteadOfKey, true)
if (this.settings.persistCredentials) {
// Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
const output = await this.git.submoduleForeach(
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
this.settings.nestedSubmodules
)
// Replace the placeholder
const configPaths: string[] =
output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
for (const configPath of configPaths) {
core.debug(`Replacing token placeholder in '${configPath}'`)
await this.replaceTokenPlaceholder(configPath)
}
if (this.settings.sshKey) {
// Configure core.sshCommand
await this.git.submoduleForeach(
`git config --local '${SSH_COMMAND_KEY}' '${this.sshCommand}'`,
this.settings.nestedSubmodules
)
} else {
// Configure HTTPS instead of SSH
for (const insteadOfValue of this.insteadOfValues) {
await this.git.submoduleForeach(
`git config --local --add '${this.insteadOfKey}' '${insteadOfValue}'`,
this.settings.nestedSubmodules
)
}
}
}
}
async removeAuth(): Promise<void> {
await this.removeSsh()
await this.removeToken()
}
async removeGlobalConfig(): Promise<void> {
if (this.temporaryHomePath?.length > 0) {
core.debug(`Unsetting HOME override`)
this.git.removeEnvironmentVariable('HOME')
await io.rmRF(this.temporaryHomePath)
}
}
private async configureSsh(): Promise<void> {
if (!this.settings.sshKey) {
return
}
// Write key
const runnerTemp = process.env['RUNNER_TEMP'] || ''
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
const uniqueId = uuid()
this.sshKeyPath = path.join(runnerTemp, uniqueId)
stateHelper.setSshKeyPath(this.sshKeyPath)
await fs.promises.mkdir(runnerTemp, {recursive: true})
await fs.promises.writeFile(
this.sshKeyPath,
this.settings.sshKey.trim() + '\n',
{mode: 0o600}
)
// Remove inherited permissions on Windows
if (IS_WINDOWS) {
const icacls = await io.which('icacls.exe')
await exec.exec(
`"${icacls}" "${this.sshKeyPath}" /grant:r "${process.env['USERDOMAIN']}\\${process.env['USERNAME']}:F"`
)
await exec.exec(`"${icacls}" "${this.sshKeyPath}" /inheritance:r`)
}
// Write known hosts
const userKnownHostsPath = path.join(os.homedir(), '.ssh', 'known_hosts')
let userKnownHosts = ''
try {
userKnownHosts = (
await fs.promises.readFile(userKnownHostsPath)
).toString()
} catch (err) {
if ((err as any)?.code !== 'ENOENT') {
throw err
}
}
let knownHosts = ''
if (userKnownHosts) {
knownHosts += `# Begin from ${userKnownHostsPath}\n${userKnownHosts}\n# End from ${userKnownHostsPath}\n`
}
if (this.settings.sshKnownHosts) {
knownHosts += `# Begin from input known hosts\n${this.settings.sshKnownHosts}\n# end from input known hosts\n`
}
knownHosts += `# Begin implicitly added github.com\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=\n# End implicitly added github.com\n`
this.sshKnownHostsPath = path.join(runnerTemp, `${uniqueId}_known_hosts`)
stateHelper.setSshKnownHostsPath(this.sshKnownHostsPath)
await fs.promises.writeFile(this.sshKnownHostsPath, knownHosts)
// Configure GIT_SSH_COMMAND
const sshPath = await io.which('ssh', true)
this.sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(
this.sshKeyPath
)}"`
if (this.settings.sshStrict) {
this.sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no'
}
this.sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(
this.sshKnownHostsPath
)}"`
core.info(`Temporarily overriding GIT_SSH_COMMAND=${this.sshCommand}`)
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand)
// Configure core.sshCommand
if (this.settings.persistCredentials) {
await this.git.config(SSH_COMMAND_KEY, this.sshCommand)
}
}
private async configureToken(
configPath?: string,
globalConfig?: boolean
): Promise<void> {
// Validate args
assert.ok(
(configPath && globalConfig) || (!configPath && !globalConfig),
'Unexpected configureToken parameter combinations'
)
// Default config path
if (!configPath && !globalConfig) {
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
}
// Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
await this.git.config(
this.tokenConfigKey,
this.tokenPlaceholderConfigValue,
globalConfig
)
// Replace the placeholder
await this.replaceTokenPlaceholder(configPath || '')
}
private async replaceTokenPlaceholder(configPath: string): Promise<void> {
assert.ok(configPath, 'configPath is not defined')
let content = (await fs.promises.readFile(configPath)).toString()
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
if (
placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
) {
throw new Error(`Unable to replace auth placeholder in ${configPath}`)
}
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
content = content.replace(
this.tokenPlaceholderConfigValue,
this.tokenConfigValue
)
await fs.promises.writeFile(configPath, content)
}
private async removeSsh(): Promise<void> {
// SSH key
const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
if (keyPath) {
try {
await io.rmRF(keyPath)
} catch (err) {
core.debug(`${(err as any)?.message ?? err}`)
core.warning(`Failed to remove SSH key '${keyPath}'`)
}
}
// SSH known hosts
const knownHostsPath =
this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
if (knownHostsPath) {
try {
await io.rmRF(knownHostsPath)
} catch {
// Intentionally empty
}
}
// SSH command
await this.removeGitConfig(SSH_COMMAND_KEY)
}
private async removeToken(): Promise<void> {
// HTTP extra header
await this.removeGitConfig(this.tokenConfigKey)
}
private async removeGitConfig(
configKey: string,
submoduleOnly: boolean = false
): Promise<void> {
if (!submoduleOnly) {
if (
(await this.git.configExists(configKey)) &&
!(await this.git.tryConfigUnset(configKey))
) {
// Load the config contents
core.warning(`Failed to remove '${configKey}' from the git config`)
}
}
const pattern = regexpHelper.escape(configKey)
await this.git.submoduleForeach(
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
true
)
}
}

View File

@ -1,39 +1,76 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as fs from 'fs'
import * as fshelper from './fs-helper'
import * as io from '@actions/io'
import * as path from 'path'
import * as refHelper from './ref-helper'
import * as regexpHelper from './regexp-helper'
import * as retryHelper from './retry-helper'
import {GitVersion} from './git-version'
// Auth header not supported before 2.9
// Wire protocol v2 not supported before 2.18
export const MinimumGitVersion = new GitVersion('2.18')
export interface IGitCommandManager {
branchDelete(remote: boolean, branch: string): Promise<void>
branchExists(remote: boolean, pattern: string): Promise<boolean>
branchList(remote: boolean): Promise<string[]>
sparseCheckout(sparseCheckout: string[]): Promise<void>
sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void>
checkout(ref: string, startPoint: string): Promise<void>
checkoutDetach(): Promise<void>
config(configKey: string, configValue: string): Promise<void>
configExists(configKey: string): Promise<boolean>
fetch(fetchDepth: number, refSpec: string[]): Promise<void>
config(
configKey: string,
configValue: string,
globalConfig?: boolean,
add?: boolean
): Promise<void>
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
fetch(
refSpec: string[],
options: {
filter?: string
fetchDepth?: number
fetchTags?: boolean
showProgress?: boolean
}
): Promise<void>
getDefaultBranch(repositoryUrl: string): Promise<string>
getWorkingDirectory(): string
init(): Promise<void>
isDetached(): Promise<boolean>
lfsFetch(ref: string): Promise<void>
lfsInstall(): Promise<void>
log1(): Promise<void>
log1(format?: string): Promise<string>
remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
removeEnvironmentVariable(name: string): void
revParse(ref: string): Promise<string>
setEnvironmentVariable(name: string, value: string): void
shaExists(sha: string): Promise<boolean>
submoduleForeach(command: string, recursive: boolean): Promise<string>
submoduleSync(recursive: boolean): Promise<void>
submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void>
submoduleStatus(): Promise<boolean>
tagExists(pattern: string): Promise<boolean>
tryClean(): Promise<boolean>
tryConfigUnset(configKey: string): Promise<boolean>
tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
tryDisableAutomaticGarbageCollection(): Promise<boolean>
tryGetFetchUrl(): Promise<string>
tryReset(): Promise<boolean>
}
export async function CreateCommandManager(
export async function createCommandManager(
workingDirectory: string,
lfs: boolean
lfs: boolean,
doSparseCheckout: boolean
): Promise<IGitCommandManager> {
return await GitCommandManager.createCommandManager(workingDirectory, lfs)
return await GitCommandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
}
class GitCommandManager {
@ -43,6 +80,7 @@ class GitCommandManager {
}
private gitPath = ''
private lfs = false
private doSparseCheckout = false
private workingDirectory = ''
// Private constructor; use createCommandManager()
@ -72,28 +110,88 @@ class GitCommandManager {
async branchList(remote: boolean): Promise<string[]> {
const result: string[] = []
// Note, this implementation uses "rev-parse --symbolic" because the output from
// Note, this implementation uses "rev-parse --symbolic-full-name" because the output from
// "branch --list" is more difficult when in a detached HEAD state.
const args = ['rev-parse', '--symbolic']
// TODO(https://github.com/actions/checkout/issues/786): this implementation uses
// "rev-parse --symbolic-full-name" because there is a bug
// in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. When
// 2.18 is no longer supported, we can switch back to --symbolic.
const args = ['rev-parse', '--symbolic-full-name']
if (remote) {
args.push('--remotes=origin')
} else {
args.push('--branches')
}
const output = await this.execGit(args)
const stderr: string[] = []
const errline: string[] = []
const stdout: string[] = []
const stdline: string[] = []
for (let branch of output.stdout.trim().split('\n')) {
branch = branch.trim()
if (branch) {
result.push(branch)
const listeners = {
stderr: (data: Buffer) => {
stderr.push(data.toString())
},
errline: (data: Buffer) => {
errline.push(data.toString())
},
stdout: (data: Buffer) => {
stdout.push(data.toString())
},
stdline: (data: Buffer) => {
stdline.push(data.toString())
}
}
// Suppress the output in order to avoid flooding annotations with innocuous errors.
await this.execGit(args, false, true, listeners)
core.debug(`stderr callback is: ${stderr}`)
core.debug(`errline callback is: ${errline}`)
core.debug(`stdout callback is: ${stdout}`)
core.debug(`stdline callback is: ${stdline}`)
for (let branch of stdline) {
branch = branch.trim()
if (!branch) {
continue
}
if (branch.startsWith('refs/heads/')) {
branch = branch.substring('refs/heads/'.length)
} else if (branch.startsWith('refs/remotes/')) {
branch = branch.substring('refs/remotes/'.length)
}
result.push(branch)
}
return result
}
async sparseCheckout(sparseCheckout: string[]): Promise<void> {
await this.execGit(['sparse-checkout', 'set', ...sparseCheckout])
}
async sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void> {
await this.execGit(['config', 'core.sparseCheckout', 'true'])
const output = await this.execGit([
'rev-parse',
'--git-path',
'info/sparse-checkout'
])
const sparseCheckoutPath = path.join(
this.workingDirectory,
output.stdout.trimRight()
)
await fs.promises.appendFile(
sparseCheckoutPath,
`\n${sparseCheckout.join('\n')}\n`
)
}
async checkout(ref: string, startPoint: string): Promise<void> {
const args = ['checkout', '--progress', '--force']
if (startPoint) {
@ -110,33 +208,63 @@ class GitCommandManager {
await this.execGit(args)
}
async config(configKey: string, configValue: string): Promise<void> {
await this.execGit(['config', configKey, configValue])
async config(
configKey: string,
configValue: string,
globalConfig?: boolean,
add?: boolean
): Promise<void> {
const args: string[] = ['config', globalConfig ? '--global' : '--local']
if (add) {
args.push('--add')
}
args.push(...[configKey, configValue])
await this.execGit(args)
}
async configExists(configKey: string): Promise<boolean> {
const pattern = configKey.replace(/[^a-zA-Z0-9_]/g, x => {
return `\\${x}`
})
async configExists(
configKey: string,
globalConfig?: boolean
): Promise<boolean> {
const pattern = regexpHelper.escape(configKey)
const output = await this.execGit(
['config', '--name-only', '--get-regexp', pattern],
[
'config',
globalConfig ? '--global' : '--local',
'--name-only',
'--get-regexp',
pattern
],
true
)
return output.exitCode === 0
}
async fetch(fetchDepth: number, refSpec: string[]): Promise<void> {
const args = [
'-c',
'protocol.version=2',
'fetch',
'--no-tags',
'--prune',
'--progress',
'--no-recurse-submodules'
]
if (fetchDepth > 0) {
args.push(`--depth=${fetchDepth}`)
async fetch(
refSpec: string[],
options: {
filter?: string
fetchDepth?: number
fetchTags?: boolean
showProgress?: boolean
}
): Promise<void> {
const args = ['-c', 'protocol.version=2', 'fetch']
if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
args.push('--no-tags')
}
args.push('--prune', '--no-recurse-submodules')
if (options.showProgress) {
args.push('--progress')
}
if (options.filter) {
args.push(`--filter=${options.filter}`)
}
if (options.fetchDepth && options.fetchDepth > 0) {
args.push(`--depth=${options.fetchDepth}`)
} else if (
fshelper.fileExistsSync(
path.join(this.workingDirectory, '.git', 'shallow')
@ -150,22 +278,38 @@ class GitCommandManager {
args.push(arg)
}
let attempt = 1
const maxAttempts = 3
while (attempt <= maxAttempts) {
const allowAllExitCodes = attempt < maxAttempts
const output = await this.execGit(args, allowAllExitCodes)
if (output.exitCode === 0) {
break
}
const that = this
await retryHelper.execute(async () => {
await that.execGit(args)
})
}
const seconds = this.getRandomIntInclusive(1, 10)
core.warning(
`Git fetch failed with exit code ${output.exitCode}. Waiting ${seconds} seconds before trying again.`
)
await this.sleep(seconds * 1000)
attempt++
async getDefaultBranch(repositoryUrl: string): Promise<string> {
let output: GitOutput | undefined
await retryHelper.execute(async () => {
output = await this.execGit([
'ls-remote',
'--quiet',
'--exit-code',
'--symref',
repositoryUrl,
'HEAD'
])
})
if (output) {
// Satisfy compiler, will always be set
for (let line of output.stdout.trim().split('\n')) {
line = line.trim()
if (line.startsWith('ref:') || line.endsWith('HEAD')) {
return line
.substr('ref:'.length, line.length - 'ref:'.length - 'HEAD'.length)
.trim()
}
}
}
throw new Error('Unexpected output when retrieving default branch')
}
getWorkingDirectory(): string {
@ -177,47 +321,103 @@ class GitCommandManager {
}
async isDetached(): Promise<boolean> {
// Note, this implementation uses "branch --show-current" because
// "rev-parse --symbolic-full-name HEAD" can fail on a new repo
// with nothing checked out.
const output = await this.execGit(['branch', '--show-current'])
return output.stdout.trim() === ''
// Note, "branch --show-current" would be simpler but isn't available until Git 2.22
const output = await this.execGit(
['rev-parse', '--symbolic-full-name', '--verify', '--quiet', 'HEAD'],
true
)
return !output.stdout.trim().startsWith('refs/heads/')
}
async lfsFetch(ref: string): Promise<void> {
const args = ['lfs', 'fetch', 'origin', ref]
let attempt = 1
const maxAttempts = 3
while (attempt <= maxAttempts) {
const allowAllExitCodes = attempt < maxAttempts
const output = await this.execGit(args, allowAllExitCodes)
if (output.exitCode === 0) {
break
}
const seconds = this.getRandomIntInclusive(1, 10)
core.warning(
`Git lfs fetch failed with exit code ${output.exitCode}. Waiting ${seconds} seconds before trying again.`
)
await this.sleep(seconds * 1000)
attempt++
}
const that = this
await retryHelper.execute(async () => {
await that.execGit(args)
})
}
async lfsInstall(): Promise<void> {
await this.execGit(['lfs', 'install', '--local'])
}
async log1(): Promise<void> {
await this.execGit(['log', '-1'])
async log1(format?: string): Promise<string> {
const args = format ? ['log', '-1', format] : ['log', '-1']
const silent = format ? false : true
const output = await this.execGit(args, false, silent)
return output.stdout
}
async remoteAdd(remoteName: string, remoteUrl: string): Promise<void> {
await this.execGit(['remote', 'add', remoteName, remoteUrl])
}
removeEnvironmentVariable(name: string): void {
delete this.gitEnv[name]
}
/**
* Resolves a ref to a SHA. For a branch or lightweight tag, the commit SHA is returned.
* For an annotated tag, the tag SHA is returned.
* @param {string} ref For example: 'refs/heads/main' or '/refs/tags/v1'
* @returns {Promise<string>}
*/
async revParse(ref: string): Promise<string> {
const output = await this.execGit(['rev-parse', ref])
return output.stdout.trim()
}
setEnvironmentVariable(name: string, value: string): void {
this.gitEnv[name] = value
}
async shaExists(sha: string): Promise<boolean> {
const args = ['rev-parse', '--verify', '--quiet', `${sha}^{object}`]
const output = await this.execGit(args, true)
return output.exitCode === 0
}
async submoduleForeach(command: string, recursive: boolean): Promise<string> {
const args = ['submodule', 'foreach']
if (recursive) {
args.push('--recursive')
}
args.push(command)
const output = await this.execGit(args)
return output.stdout
}
async submoduleSync(recursive: boolean): Promise<void> {
const args = ['submodule', 'sync']
if (recursive) {
args.push('--recursive')
}
await this.execGit(args)
}
async submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void> {
const args = ['-c', 'protocol.version=2']
args.push('submodule', 'update', '--init', '--force')
if (fetchDepth > 0) {
args.push(`--depth=${fetchDepth}`)
}
if (recursive) {
args.push('--recursive')
}
await this.execGit(args)
}
async submoduleStatus(): Promise<boolean> {
const output = await this.execGit(['submodule', 'status'], true)
core.debug(output.stdout)
return output.exitCode === 0
}
async tagExists(pattern: string): Promise<boolean> {
const output = await this.execGit(['tag', '--list', pattern])
return !!output.stdout.trim()
@ -228,22 +428,33 @@ class GitCommandManager {
return output.exitCode === 0
}
async tryConfigUnset(configKey: string): Promise<boolean> {
async tryConfigUnset(
configKey: string,
globalConfig?: boolean
): Promise<boolean> {
const output = await this.execGit(
['config', '--unset-all', configKey],
[
'config',
globalConfig ? '--global' : '--local',
'--unset-all',
configKey
],
true
)
return output.exitCode === 0
}
async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
const output = await this.execGit(['config', 'gc.auto', '0'], true)
const output = await this.execGit(
['config', '--local', 'gc.auto', '0'],
true
)
return output.exitCode === 0
}
async tryGetFetchUrl(): Promise<string> {
const output = await this.execGit(
['config', '--get', 'remote.origin.url'],
['config', '--local', '--get', 'remote.origin.url'],
true
)
@ -266,16 +477,23 @@ class GitCommandManager {
static async createCommandManager(
workingDirectory: string,
lfs: boolean
lfs: boolean,
doSparseCheckout: boolean
): Promise<GitCommandManager> {
const result = new GitCommandManager()
await result.initializeCommandManager(workingDirectory, lfs)
await result.initializeCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
return result
}
private async execGit(
args: string[],
allowAllExitCodes = false
allowAllExitCodes = false,
silent = false,
customListeners = {}
): Promise<GitOutput> {
fshelper.directoryExistsSync(this.workingDirectory, true)
@ -289,27 +507,36 @@ class GitCommandManager {
env[key] = this.gitEnv[key]
}
const stdout: string[] = []
const defaultListener = {
stdout: (data: Buffer) => {
stdout.push(data.toString())
}
}
const mergedListeners = {...defaultListener, ...customListeners}
const stdout: string[] = []
const options = {
cwd: this.workingDirectory,
env,
silent,
ignoreReturnCode: allowAllExitCodes,
listeners: {
stdout: (data: Buffer) => {
stdout.push(data.toString())
}
}
listeners: mergedListeners
}
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
result.stdout = stdout.join('')
core.debug(result.exitCode.toString())
core.debug(result.stdout)
return result
}
private async initializeCommandManager(
workingDirectory: string,
lfs: boolean
lfs: boolean,
doSparseCheckout: boolean
): Promise<void> {
this.workingDirectory = workingDirectory
@ -338,13 +565,9 @@ class GitCommandManager {
}
// Minimum git version
// Note:
// - Auth header not supported before 2.9
// - Wire protocol v2 not supported before 2.18
const minimumGitVersion = new GitVersion('2.18')
if (!gitVersion.checkMinimum(minimumGitVersion)) {
if (!gitVersion.checkMinimum(MinimumGitVersion)) {
throw new Error(
`Minimum required git version is ${minimumGitVersion}. Your git ('${this.gitPath}') is ${gitVersion}`
`Minimum required git version is ${MinimumGitVersion}. Your git ('${this.gitPath}') is ${gitVersion}`
)
}
@ -376,21 +599,21 @@ class GitCommandManager {
}
}
this.doSparseCheckout = doSparseCheckout
if (this.doSparseCheckout) {
// The `git sparse-checkout` command was introduced in Git v2.25.0
const minimumGitSparseCheckoutVersion = new GitVersion('2.25')
if (!gitVersion.checkMinimum(minimumGitSparseCheckoutVersion)) {
throw new Error(
`Minimum Git version required for sparse checkout is ${minimumGitSparseCheckoutVersion}. Your git ('${this.gitPath}') is ${gitVersion}`
)
}
}
// Set the user agent
const gitHttpUserAgent = `git/${gitVersion} (github-actions-checkout)`
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
}
private getRandomIntInclusive(minimum: number, maximum: number): number {
minimum = Math.floor(minimum)
maximum = Math.floor(maximum)
return Math.floor(Math.random() * (maximum - minimum + 1)) + minimum
}
private async sleep(milliseconds): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
}
class GitOutput {

125
src/git-directory-helper.ts Normal file
View File

@ -0,0 +1,125 @@
import * as assert from 'assert'
import * as core from '@actions/core'
import * as fs from 'fs'
import * as fsHelper from './fs-helper'
import * as io from '@actions/io'
import * as path from 'path'
import {IGitCommandManager} from './git-command-manager'
export async function prepareExistingDirectory(
git: IGitCommandManager | undefined,
repositoryPath: string,
repositoryUrl: string,
clean: boolean,
ref: string
): Promise<void> {
assert.ok(repositoryPath, 'Expected repositoryPath to be defined')
assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined')
// Indicates whether to delete the directory contents
let remove = false
// Check whether using git or REST API
if (!git) {
remove = true
}
// Fetch URL does not match
else if (
!fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) ||
repositoryUrl !== (await git.tryGetFetchUrl())
) {
remove = true
} else {
// Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process
const lockPaths = [
path.join(repositoryPath, '.git', 'index.lock'),
path.join(repositoryPath, '.git', 'shallow.lock')
]
for (const lockPath of lockPaths) {
try {
await io.rmRF(lockPath)
} catch (error) {
core.debug(
`Unable to delete '${lockPath}'. ${(error as any)?.message ?? error}`
)
}
}
try {
core.startGroup('Removing previously created refs, to avoid conflicts')
// Checkout detached HEAD
if (!(await git.isDetached())) {
await git.checkoutDetach()
}
// Remove all refs/heads/*
let branches = await git.branchList(false)
for (const branch of branches) {
await git.branchDelete(false, branch)
}
// Remove any conflicting refs/remotes/origin/*
// Example 1: Consider ref is refs/heads/foo and previously fetched refs/remotes/origin/foo/bar
// Example 2: Consider ref is refs/heads/foo/bar and previously fetched refs/remotes/origin/foo
if (ref) {
ref = ref.startsWith('refs/') ? ref : `refs/heads/${ref}`
if (ref.startsWith('refs/heads/')) {
const upperName1 = ref.toUpperCase().substr('REFS/HEADS/'.length)
const upperName1Slash = `${upperName1}/`
branches = await git.branchList(true)
for (const branch of branches) {
const upperName2 = branch.substr('origin/'.length).toUpperCase()
const upperName2Slash = `${upperName2}/`
if (
upperName1.startsWith(upperName2Slash) ||
upperName2.startsWith(upperName1Slash)
) {
await git.branchDelete(true, branch)
}
}
}
}
core.endGroup()
// Check for submodules and delete any existing files if submodules are present
if (!(await git.submoduleStatus())) {
remove = true
core.info('Bad Submodules found, removing existing files')
}
// Clean
if (clean) {
core.startGroup('Cleaning the repository')
if (!(await git.tryClean())) {
core.debug(
`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For further investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`
)
remove = true
} else if (!(await git.tryReset())) {
remove = true
}
core.endGroup()
if (remove) {
core.warning(
`Unable to clean or reset the repository. The repository will be recreated instead.`
)
}
}
} catch (error) {
core.warning(
`Unable to prepare the existing repository. The repository will be recreated instead.`
)
remove = true
}
}
if (remove) {
// Delete the contents of the directory. Don't delete the directory itself
// since it might be the current working directory.
core.info(`Deleting the contents of '${repositoryPath}'`)
for (const file of await fs.promises.readdir(repositoryPath)) {
await io.rmRF(path.join(repositoryPath, file))
}
}
}

View File

@ -1,34 +1,23 @@
import * as core from '@actions/core'
import * as coreCommand from '@actions/core/lib/command'
import * as fs from 'fs'
import * as fsHelper from './fs-helper'
import * as gitAuthHelper from './git-auth-helper'
import * as gitCommandManager from './git-command-manager'
import * as gitDirectoryHelper from './git-directory-helper'
import * as githubApiHelper from './github-api-helper'
import * as io from '@actions/io'
import * as path from 'path'
import * as refHelper from './ref-helper'
import * as stateHelper from './state-helper'
import * as urlHelper from './url-helper'
import {IGitCommandManager} from './git-command-manager'
import {IGitSourceSettings} from './git-source-settings'
const authConfigKey = `http.https://github.com/.extraheader`
export interface ISourceSettings {
repositoryPath: string
repositoryOwner: string
repositoryName: string
ref: string
commit: string
clean: boolean
fetchDepth: number
lfs: boolean
accessToken: string
}
export async function getSource(settings: ISourceSettings): Promise<void> {
export async function getSource(settings: IGitSourceSettings): Promise<void> {
// Repository URL
core.info(
`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
)
const repositoryUrl = `https://github.com/${encodeURIComponent(
settings.repositoryOwner
)}/${encodeURIComponent(settings.repositoryName)}`
const repositoryUrl = urlHelper.getFetchUrl(settings)
// Remove conflicting file path
if (fsHelper.fileExistsSync(settings.repositoryPath)) {
@ -43,208 +32,312 @@ export async function getSource(settings: ISourceSettings): Promise<void> {
}
// Git command manager
core.info(`Working directory is '${settings.repositoryPath}'`)
const git = await gitCommandManager.CreateCommandManager(
settings.repositoryPath,
settings.lfs
)
core.startGroup('Getting Git version info')
const git = await getGitCommandManager(settings)
core.endGroup()
// Try prepare existing directory, otherwise recreate
if (
isExisting &&
!(await tryPrepareExistingDirectory(
let authHelper: gitAuthHelper.IGitAuthHelper | null = null
try {
if (git) {
authHelper = gitAuthHelper.createAuthHelper(git, settings)
if (settings.setSafeDirectory) {
// Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail
// Otherwise all git commands we run in a container fail
await authHelper.configureTempGlobalConfig()
core.info(
`Adding repository directory to the temporary git global config as a safe directory`
)
await git
.config('safe.directory', settings.repositoryPath, true, true)
.catch(error => {
core.info(
`Failed to initialize safe directory with error: ${error}`
)
})
stateHelper.setSafeDirectory()
}
}
// Prepare existing directory, otherwise recreate
if (isExisting) {
await gitDirectoryHelper.prepareExistingDirectory(
git,
settings.repositoryPath,
repositoryUrl,
settings.clean,
settings.ref
)
}
if (!git) {
// Downloading using REST API
core.info(`The repository will be downloaded using the GitHub REST API`)
core.info(
`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
)
if (settings.submodules) {
throw new Error(
`Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
)
} else if (settings.sshKey) {
throw new Error(
`Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
)
}
await githubApiHelper.downloadRepository(
settings.authToken,
settings.repositoryOwner,
settings.repositoryName,
settings.ref,
settings.commit,
settings.repositoryPath,
settings.githubServerUrl
)
return
}
// Save state for POST action
stateHelper.setRepositoryPath(settings.repositoryPath)
// Initialize the repository
if (
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
) {
core.startGroup('Initializing the repository')
await git.init()
await git.remoteAdd('origin', repositoryUrl)
core.endGroup()
}
// Disable automatic garbage collection
core.startGroup('Disabling automatic garbage collection')
if (!(await git.tryDisableAutomaticGarbageCollection())) {
core.warning(
`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
)
}
core.endGroup()
// If we didn't initialize it above, do it now
if (!authHelper) {
authHelper = gitAuthHelper.createAuthHelper(git, settings)
}
// Configure auth
core.startGroup('Setting up auth')
await authHelper.configureAuth()
core.endGroup()
// Determine the default branch
if (!settings.ref && !settings.commit) {
core.startGroup('Determining the default branch')
if (settings.sshKey) {
settings.ref = await git.getDefaultBranch(repositoryUrl)
} else {
settings.ref = await githubApiHelper.getDefaultBranch(
settings.authToken,
settings.repositoryOwner,
settings.repositoryName,
settings.githubServerUrl
)
}
core.endGroup()
}
// LFS install
if (settings.lfs) {
await git.lfsInstall()
}
// Fetch
core.startGroup('Fetching the repository')
const fetchOptions: {
filter?: string
fetchDepth?: number
fetchTags?: boolean
showProgress?: boolean
} = {}
if (settings.filter) {
fetchOptions.filter = settings.filter
} else if (settings.sparseCheckout) {
fetchOptions.filter = 'blob:none'
}
if (settings.fetchDepth <= 0) {
// Fetch all branches and tags
let refSpec = refHelper.getRefSpecForAllHistory(
settings.ref,
settings.commit
)
await git.fetch(refSpec, fetchOptions)
// When all history is fetched, the ref we're interested in may have moved to a different
// commit (push or force push). If so, fetch again with a targeted refspec.
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
await git.fetch(refSpec, fetchOptions)
}
} else {
fetchOptions.fetchDepth = settings.fetchDepth
fetchOptions.fetchTags = settings.fetchTags
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
await git.fetch(refSpec, fetchOptions)
}
core.endGroup()
// Checkout info
core.startGroup('Determining the checkout info')
const checkoutInfo = await refHelper.getCheckoutInfo(
git,
settings.repositoryPath,
repositoryUrl,
settings.clean
))
) {
// Delete the contents of the directory. Don't delete the directory itself
// since it may be the current working directory.
core.info(`Deleting the contents of '${settings.repositoryPath}'`)
for (const file of await fs.promises.readdir(settings.repositoryPath)) {
await io.rmRF(path.join(settings.repositoryPath, file))
settings.ref,
settings.commit
)
core.endGroup()
// LFS fetch
// Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
// Explicit lfs fetch will fetch lfs objects in parallel.
// For sparse checkouts, let `checkout` fetch the needed objects lazily.
if (settings.lfs && !settings.sparseCheckout) {
core.startGroup('Fetching LFS objects')
await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
core.endGroup()
}
// Sparse checkout
if (settings.sparseCheckout) {
core.startGroup('Setting up sparse checkout')
if (settings.sparseCheckoutConeMode) {
await git.sparseCheckout(settings.sparseCheckout)
} else {
await git.sparseCheckoutNonConeMode(settings.sparseCheckout)
}
core.endGroup()
}
// Checkout
core.startGroup('Checking out the ref')
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
core.endGroup()
// Submodules
if (settings.submodules) {
// Temporarily override global config
core.startGroup('Setting up auth for fetching submodules')
await authHelper.configureGlobalAuth()
core.endGroup()
// Checkout submodules
core.startGroup('Fetching submodules')
await git.submoduleSync(settings.nestedSubmodules)
await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules)
await git.submoduleForeach(
'git config --local gc.auto 0',
settings.nestedSubmodules
)
core.endGroup()
// Persist credentials
if (settings.persistCredentials) {
core.startGroup('Persisting credentials for submodules')
await authHelper.configureSubmoduleAuth()
core.endGroup()
}
}
// Get commit information
const commitInfo = await git.log1()
// Log commit sha
await git.log1("--format='%H'")
// Check for incorrect pull request merge commit
await refHelper.checkCommitInfo(
settings.authToken,
commitInfo,
settings.repositoryOwner,
settings.repositoryName,
settings.ref,
settings.commit,
settings.githubServerUrl
)
} finally {
// Remove auth
if (authHelper) {
if (!settings.persistCredentials) {
core.startGroup('Removing auth')
await authHelper.removeAuth()
core.endGroup()
}
authHelper.removeGlobalConfig()
}
}
// Initialize the repository
if (
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
) {
await git.init()
await git.remoteAdd('origin', repositoryUrl)
}
// Disable automatic garbage collection
if (!(await git.tryDisableAutomaticGarbageCollection())) {
core.warning(
`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
)
}
// Remove possible previous extraheader
await removeGitConfig(git, authConfigKey)
// Add extraheader (auth)
const base64Credentials = Buffer.from(
`x-access-token:${settings.accessToken}`,
'utf8'
).toString('base64')
core.setSecret(base64Credentials)
const authConfigValue = `AUTHORIZATION: basic ${base64Credentials}`
await git.config(authConfigKey, authConfigValue)
// LFS install
if (settings.lfs) {
await git.lfsInstall()
}
// Fetch
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
await git.fetch(settings.fetchDepth, refSpec)
// Checkout info
const checkoutInfo = await refHelper.getCheckoutInfo(
git,
settings.ref,
settings.commit
)
// LFS fetch
// Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
// Explicit lfs fetch will fetch lfs objects in parallel.
if (settings.lfs) {
await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
}
// Checkout
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
// Dump some info about the checked out commit
await git.log1()
// Set intra-task state for cleanup
coreCommand.issueCommand(
'save-state',
{name: 'repositoryPath'},
settings.repositoryPath
)
}
export async function cleanup(repositoryPath: string): Promise<void> {
// Repo exists?
if (!fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))) {
if (
!repositoryPath ||
!fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))
) {
return
}
fsHelper.directoryExistsSync(repositoryPath, true)
// Remove the config key
const git = await gitCommandManager.CreateCommandManager(
repositoryPath,
false
)
await removeGitConfig(git, authConfigKey)
}
async function tryPrepareExistingDirectory(
git: IGitCommandManager,
repositoryPath: string,
repositoryUrl: string,
clean: boolean
): Promise<boolean> {
// Fetch URL does not match
if (
!fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) ||
repositoryUrl !== (await git.tryGetFetchUrl())
) {
return false
}
// Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process
const lockPaths = [
path.join(repositoryPath, '.git', 'index.lock'),
path.join(repositoryPath, '.git', 'shallow.lock')
]
for (const lockPath of lockPaths) {
try {
await io.rmRF(lockPath)
} catch (error) {
core.debug(`Unable to delete '${lockPath}'. ${error.message}`)
}
}
let git: IGitCommandManager
try {
// Checkout detached HEAD
if (!(await git.isDetached())) {
await git.checkoutDetach()
}
// Remove all refs/heads/*
let branches = await git.branchList(false)
for (const branch of branches) {
await git.branchDelete(false, branch)
}
// Remove all refs/remotes/origin/* to avoid conflicts
branches = await git.branchList(true)
for (const branch of branches) {
await git.branchDelete(true, branch)
}
} catch (error) {
core.warning(
`Unable to prepare the existing repository. The repository will be recreated instead.`
git = await gitCommandManager.createCommandManager(
repositoryPath,
false,
false
)
return false
} catch {
return
}
// Clean
if (clean) {
let succeeded = true
if (!(await git.tryClean())) {
core.debug(
`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`
// Remove auth
const authHelper = gitAuthHelper.createAuthHelper(git)
try {
if (stateHelper.PostSetSafeDirectory) {
// Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail
// Otherwise all git commands we run in a container fail
await authHelper.configureTempGlobalConfig()
core.info(
`Adding repository directory to the temporary git global config as a safe directory`
)
succeeded = false
} else if (!(await git.tryReset())) {
succeeded = false
await git
.config('safe.directory', repositoryPath, true, true)
.catch(error => {
core.info(`Failed to initialize safe directory with error: ${error}`)
})
}
if (!succeeded) {
core.warning(
`Unable to clean or reset the repository. The repository will be recreated instead.`
)
}
return succeeded
await authHelper.removeAuth()
} finally {
await authHelper.removeGlobalConfig()
}
return true
}
async function removeGitConfig(
git: IGitCommandManager,
configKey: string
): Promise<void> {
if (
(await git.configExists(configKey)) &&
!(await git.tryConfigUnset(configKey))
) {
// Load the config contents
core.warning(
`Failed to remove '${configKey}' from the git config. Attempting to remove the config value by editing the file directly.`
async function getGitCommandManager(
settings: IGitSourceSettings
): Promise<IGitCommandManager | undefined> {
core.info(`Working directory is '${settings.repositoryPath}'`)
try {
return await gitCommandManager.createCommandManager(
settings.repositoryPath,
settings.lfs,
settings.sparseCheckout != null
)
const configPath = path.join(git.getWorkingDirectory(), '.git', 'config')
fsHelper.fileExistsSync(configPath)
let contents = fs.readFileSync(configPath).toString() || ''
} catch (err) {
// Git is required for LFS
if (settings.lfs) {
throw err
}
// Filter - only includes lines that do not contain the config key
const upperConfigKey = configKey.toUpperCase()
const split = contents
.split('\n')
.filter(x => !x.toUpperCase().includes(upperConfigKey))
contents = split.join('\n')
// Rewrite the config file
fs.writeFileSync(configPath, contents)
// Otherwise fallback to REST API
return undefined
}
}

116
src/git-source-settings.ts Normal file
View File

@ -0,0 +1,116 @@
export interface IGitSourceSettings {
/**
* The location on disk where the repository will be placed
*/
repositoryPath: string
/**
* The repository owner
*/
repositoryOwner: string
/**
* The repository name
*/
repositoryName: string
/**
* The ref to fetch
*/
ref: string
/**
* The commit to checkout
*/
commit: string
/**
* Indicates whether to clean the repository
*/
clean: boolean
/**
* The filter determining which objects to include
*/
filter: string | undefined
/**
* The array of folders to make the sparse checkout
*/
sparseCheckout: string[]
/**
* Indicates whether to use cone mode in the sparse checkout (if any)
*/
sparseCheckoutConeMode: boolean
/**
* The depth when fetching
*/
fetchDepth: number
/**
* Fetch tags, even if fetchDepth > 0 (default: false)
*/
fetchTags: boolean
/**
* Indicates whether to use the --progress option when fetching
*/
showProgress: boolean
/**
* Indicates whether to fetch LFS objects
*/
lfs: boolean
/**
* Indicates whether to checkout submodules
*/
submodules: boolean
/**
* Indicates whether to recursively checkout submodules
*/
nestedSubmodules: boolean
/**
* The auth token to use when fetching the repository
*/
authToken: string
/**
* The SSH key to configure
*/
sshKey: string
/**
* Additional SSH known hosts
*/
sshKnownHosts: string
/**
* Indicates whether the server must be a known host
*/
sshStrict: boolean
/**
* Indicates whether to persist the credentials on disk to enable scripting authenticated git commands
*/
persistCredentials: boolean
/**
* Organization ID for the currently running workflow (used for auth settings)
*/
workflowOrganizationId: number | undefined
/**
* Indicates whether to add repositoryPath as safe.directory in git global config
*/
setSafeDirectory: boolean
/**
* User override on the GitHub Server/Host URL that hosts the repository to be cloned
*/
githubServerUrl: string | undefined
}

143
src/github-api-helper.ts Normal file
View File

@ -0,0 +1,143 @@
import * as assert from 'assert'
import * as core from '@actions/core'
import * as fs from 'fs'
import * as github from '@actions/github'
import * as io from '@actions/io'
import * as path from 'path'
import * as retryHelper from './retry-helper'
import * as toolCache from '@actions/tool-cache'
import {default as uuid} from 'uuid/v4'
import {getServerApiUrl} from './url-helper'
const IS_WINDOWS = process.platform === 'win32'
export async function downloadRepository(
authToken: string,
owner: string,
repo: string,
ref: string,
commit: string,
repositoryPath: string,
baseUrl?: string
): Promise<void> {
// Determine the default branch
if (!ref && !commit) {
core.info('Determining the default branch')
ref = await getDefaultBranch(authToken, owner, repo, baseUrl)
}
// Download the archive
let archiveData = await retryHelper.execute(async () => {
core.info('Downloading the archive')
return await downloadArchive(authToken, owner, repo, ref, commit, baseUrl)
})
// Write archive to disk
core.info('Writing archive to disk')
const uniqueId = uuid()
const archivePath = path.join(repositoryPath, `${uniqueId}.tar.gz`)
await fs.promises.writeFile(archivePath, archiveData)
archiveData = Buffer.from('') // Free memory
// Extract archive
core.info('Extracting the archive')
const extractPath = path.join(repositoryPath, uniqueId)
await io.mkdirP(extractPath)
if (IS_WINDOWS) {
await toolCache.extractZip(archivePath, extractPath)
} else {
await toolCache.extractTar(archivePath, extractPath)
}
await io.rmRF(archivePath)
// Determine the path of the repository content. The archive contains
// a top-level folder and the repository content is inside.
const archiveFileNames = await fs.promises.readdir(extractPath)
assert.ok(
archiveFileNames.length == 1,
'Expected exactly one directory inside archive'
)
const archiveVersion = archiveFileNames[0] // The top-level folder name includes the short SHA
core.info(`Resolved version ${archiveVersion}`)
const tempRepositoryPath = path.join(extractPath, archiveVersion)
// Move the files
for (const fileName of await fs.promises.readdir(tempRepositoryPath)) {
const sourcePath = path.join(tempRepositoryPath, fileName)
const targetPath = path.join(repositoryPath, fileName)
if (IS_WINDOWS) {
await io.cp(sourcePath, targetPath, {recursive: true}) // Copy on Windows (Windows Defender may have a lock)
} else {
await io.mv(sourcePath, targetPath)
}
}
await io.rmRF(extractPath)
}
/**
* Looks up the default branch name
*/
export async function getDefaultBranch(
authToken: string,
owner: string,
repo: string,
baseUrl?: string
): Promise<string> {
return await retryHelper.execute(async () => {
core.info('Retrieving the default branch name')
const octokit = github.getOctokit(authToken, {
baseUrl: getServerApiUrl(baseUrl)
})
let result: string
try {
// Get the default branch from the repo info
const response = await octokit.rest.repos.get({owner, repo})
result = response.data.default_branch
assert.ok(result, 'default_branch cannot be empty')
} catch (err) {
// Handle .wiki repo
if (
(err as any)?.status === 404 &&
repo.toUpperCase().endsWith('.WIKI')
) {
result = 'master'
}
// Otherwise error
else {
throw err
}
}
// Print the default branch
core.info(`Default branch '${result}'`)
// Prefix with 'refs/heads'
if (!result.startsWith('refs/')) {
result = `refs/heads/${result}`
}
return result
})
}
async function downloadArchive(
authToken: string,
owner: string,
repo: string,
ref: string,
commit: string,
baseUrl?: string
): Promise<Buffer> {
const octokit = github.getOctokit(authToken, {
baseUrl: getServerApiUrl(baseUrl)
})
const download = IS_WINDOWS
? octokit.rest.repos.downloadZipballArchive
: octokit.rest.repos.downloadTarballArchive
const response = await download({
owner: owner,
repo: repo,
ref: commit || ref
})
return Buffer.from(response.data as ArrayBuffer) // response.data is ArrayBuffer
}

View File

@ -2,10 +2,11 @@ import * as core from '@actions/core'
import * as fsHelper from './fs-helper'
import * as github from '@actions/github'
import * as path from 'path'
import {ISourceSettings} from './git-source-provider'
import * as workflowContextHelper from './workflow-context-helper'
import {IGitSourceSettings} from './git-source-settings'
export function getInputs(): ISourceSettings {
const result = ({} as unknown) as ISourceSettings
export async function getInputs(): Promise<IGitSourceSettings> {
const result = ({} as unknown) as IGitSourceSettings
// GitHub workspace
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
@ -61,10 +62,12 @@ export function getInputs(): ISourceSettings {
if (isWorkflowRepository) {
result.ref = github.context.ref
result.commit = github.context.sha
}
if (!result.ref && !result.commit) {
result.ref = 'refs/heads/master'
// Some events have an unqualifed ref. For example when a PR is merged (pull_request closed event),
// the ref is unqualifed like "main" instead of "refs/heads/main".
if (result.commit && result.ref && !result.ref.startsWith('refs/')) {
result.ref = `refs/heads/${result.ref}`
}
}
}
// SHA?
@ -79,13 +82,25 @@ export function getInputs(): ISourceSettings {
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
core.debug(`clean = ${result.clean}`)
// Submodules
if (core.getInput('submodules')) {
throw new Error(
"The input 'submodules' is not supported in actions/checkout@v2"
)
// Filter
const filter = core.getInput('filter')
if (filter) {
result.filter = filter
}
core.debug(`filter = ${result.filter}`)
// Sparse checkout
const sparseCheckout = core.getMultilineInput('sparse-checkout')
if (sparseCheckout.length) {
result.sparseCheckout = sparseCheckout
core.debug(`sparse checkout = ${result.sparseCheckout}`)
}
result.sparseCheckoutConeMode =
(core.getInput('sparse-checkout-cone-mode') || 'true').toUpperCase() ===
'TRUE'
// Fetch depth
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {
@ -93,12 +108,56 @@ export function getInputs(): ISourceSettings {
}
core.debug(`fetch depth = ${result.fetchDepth}`)
// Fetch tags
result.fetchTags =
(core.getInput('fetch-tags') || 'false').toUpperCase() === 'TRUE'
core.debug(`fetch tags = ${result.fetchTags}`)
// Show fetch progress
result.showProgress =
(core.getInput('show-progress') || 'true').toUpperCase() === 'TRUE'
core.debug(`show progress = ${result.showProgress}`)
// LFS
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'
core.debug(`lfs = ${result.lfs}`)
// Access token
result.accessToken = core.getInput('token')
// Submodules
result.submodules = false
result.nestedSubmodules = false
const submodulesString = (core.getInput('submodules') || '').toUpperCase()
if (submodulesString == 'RECURSIVE') {
result.submodules = true
result.nestedSubmodules = true
} else if (submodulesString == 'TRUE') {
result.submodules = true
}
core.debug(`submodules = ${result.submodules}`)
core.debug(`recursive submodules = ${result.nestedSubmodules}`)
// Auth token
result.authToken = core.getInput('token', {required: true})
// SSH
result.sshKey = core.getInput('ssh-key')
result.sshKnownHosts = core.getInput('ssh-known-hosts')
result.sshStrict =
(core.getInput('ssh-strict') || 'true').toUpperCase() === 'TRUE'
// Persist credentials
result.persistCredentials =
(core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE'
// Workflow organization ID
result.workflowOrganizationId = await workflowContextHelper.getOrganizationId()
// Set safe.directory in git global config.
result.setSafeDirectory =
(core.getInput('set-safe-directory') || 'true').toUpperCase() === 'TRUE'
// Determine the GitHub URL that the repository is being hosted from
result.githubServerUrl = core.getInput('github-server-url')
core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
return result
}

View File

@ -3,12 +3,11 @@ import * as coreCommand from '@actions/core/lib/command'
import * as gitSourceProvider from './git-source-provider'
import * as inputHelper from './input-helper'
import * as path from 'path'
const cleanupRepositoryPath = process.env['STATE_repositoryPath'] as string
import * as stateHelper from './state-helper'
async function run(): Promise<void> {
try {
const sourceSettings = inputHelper.getInputs()
const sourceSettings = await inputHelper.getInputs()
try {
// Register problem matcher
@ -25,20 +24,20 @@ async function run(): Promise<void> {
coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '')
}
} catch (error) {
core.setFailed(error.message)
core.setFailed(`${(error as any)?.message ?? error}`)
}
}
async function cleanup(): Promise<void> {
try {
await gitSourceProvider.cleanup(cleanupRepositoryPath)
await gitSourceProvider.cleanup(stateHelper.RepositoryPath)
} catch (error) {
core.warning(error.message)
core.warning(`${(error as any)?.message ?? error}`)
}
}
// Main
if (!cleanupRepositoryPath) {
if (!stateHelper.IsPost) {
run()
}
// Post

View File

@ -10,10 +10,10 @@ import * as yaml from 'js-yaml'
function updateUsage(
actionReference: string,
actionYamlPath: string = 'action.yml',
readmePath: string = 'README.md',
startToken: string = '<!-- start usage -->',
endToken: string = '<!-- end usage -->'
actionYamlPath = 'action.yml',
readmePath = 'README.md',
startToken = '<!-- start usage -->',
endToken = '<!-- end usage -->'
): void {
if (!actionReference) {
throw new Error('Parameter actionReference must not be empty')
@ -59,28 +59,52 @@ function updateUsage(
// Constrain the width of the description
const width = 80
let description = input.description as string
let description = (input.description as string)
.trimRight()
.replace(/\r\n/g, '\n') // Convert CR to LF
.replace(/ +/g, ' ') // Squash consecutive spaces
.replace(/ \n/g, '\n') // Squash space followed by newline
while (description) {
// Longer than width? Find a space to break apart
let segment: string = description
if (description.length > width) {
segment = description.substr(0, width + 1)
while (!segment.endsWith(' ')) {
while (!segment.endsWith(' ') && !segment.endsWith('\n') && segment) {
segment = segment.substr(0, segment.length - 1)
}
// Trimmed too much?
if (segment.length < width * 0.67) {
segment = description
}
} else {
segment = description
}
description = description.substr(segment.length) // Remaining
segment = segment.trimRight() // Trim the trailing space
newReadme.push(` # ${segment}`)
// Check for newline
const newlineIndex = segment.indexOf('\n')
if (newlineIndex >= 0) {
segment = segment.substr(0, newlineIndex + 1)
}
// Append segment
newReadme.push(` # ${segment}`.trimRight())
// Remaining
description = description.substr(segment.length)
}
// Input and default
if (input.default !== undefined) {
// Append blank line if description had paragraphs
if ((input.description as string).trimRight().match(/\n[ ]*\r?\n/)) {
newReadme.push(` #`)
}
// Default
newReadme.push(` # Default: ${input.default}`)
}
// Input name
newReadme.push(` ${key}: ''`)
firstInput = false
@ -96,7 +120,7 @@ function updateUsage(
}
updateUsage(
'actions/checkout@v2-beta',
'actions/checkout@v4',
path.join(__dirname, '..', '..', 'action.yml'),
path.join(__dirname, '..', '..', 'README.md')
)

8
src/misc/licensed-check.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
set -e
src/misc/licensed-download.sh
echo 'Running: licensed cached'
_temp/licensed-3.6.0/licensed status

24
src/misc/licensed-download.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
set -e
if [ ! -f _temp/licensed-3.6.0.done ]; then
echo 'Clearing temp'
rm -rf _temp/licensed-3.6.0 || true
echo 'Downloading licensed'
mkdir -p _temp/licensed-3.6.0
pushd _temp/licensed-3.6.0
if [[ "$OSTYPE" == "darwin"* ]]; then
curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.6.0/licensed-3.6.0-darwin-x64.tar.gz
else
curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.6.0/licensed-3.6.0-linux-x64.tar.gz
fi
echo 'Extracting licenesed'
tar -xzf licensed.tar.gz
popd
touch _temp/licensed-3.6.0.done
else
echo 'Licensed already downloaded'
fi

8
src/misc/licensed-generate.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
set -e
src/misc/licensed-download.sh
echo 'Running: licensed cached'
_temp/licensed-3.6.0/licensed cache

View File

@ -1,4 +1,9 @@
import {IGitCommandManager} from './git-command-manager'
import * as core from '@actions/core'
import * as github from '@actions/github'
import {getServerApiUrl, isGhes} from './url-helper'
export const tagsRefSpec = '+refs/tags/*:refs/tags/*'
export interface ICheckoutInfo {
ref: string
@ -57,6 +62,16 @@ export async function getCheckoutInfo(
return result
}
export function getRefSpecForAllHistory(ref: string, commit: string): string[] {
const result = ['+refs/heads/*:refs/remotes/origin/*', tagsRefSpec]
if (ref && ref.toUpperCase().startsWith('REFS/PULL/')) {
const branch = ref.substring('refs/pull/'.length)
result.push(`+${commit || ref}:refs/remotes/pull/${branch}`)
}
return result
}
export function getRefSpec(ref: string, commit: string): string[] {
if (!ref && !commit) {
throw new Error('Args ref and commit cannot both be empty')
@ -107,3 +122,162 @@ export function getRefSpec(ref: string, commit: string): string[] {
return [`+${ref}:${ref}`]
}
}
/**
* Tests whether the initial fetch created the ref at the expected commit
*/
export async function testRef(
git: IGitCommandManager,
ref: string,
commit: string
): Promise<boolean> {
if (!git) {
throw new Error('Arg git cannot be empty')
}
if (!ref && !commit) {
throw new Error('Args ref and commit cannot both be empty')
}
// No SHA? Nothing to test
if (!commit) {
return true
}
// SHA only?
else if (!ref) {
return await git.shaExists(commit)
}
const upperRef = ref.toUpperCase()
// refs/heads/
if (upperRef.startsWith('REFS/HEADS/')) {
const branch = ref.substring('refs/heads/'.length)
return (
(await git.branchExists(true, `origin/${branch}`)) &&
commit === (await git.revParse(`refs/remotes/origin/${branch}`))
)
}
// refs/pull/
else if (upperRef.startsWith('REFS/PULL/')) {
// Assume matches because fetched using the commit
return true
}
// refs/tags/
else if (upperRef.startsWith('REFS/TAGS/')) {
const tagName = ref.substring('refs/tags/'.length)
return (
(await git.tagExists(tagName)) && commit === (await git.revParse(ref))
)
}
// Unexpected
else {
core.debug(`Unexpected ref format '${ref}' when testing ref info`)
return true
}
}
export async function checkCommitInfo(
token: string,
commitInfo: string,
repositoryOwner: string,
repositoryName: string,
ref: string,
commit: string,
baseUrl?: string
): Promise<void> {
try {
// GHES?
if (isGhes(baseUrl)) {
return
}
// Auth token?
if (!token) {
return
}
// Public PR synchronize, for workflow repo?
if (
fromPayload('repository.private') !== false ||
github.context.eventName !== 'pull_request' ||
fromPayload('action') !== 'synchronize' ||
repositoryOwner !== github.context.repo.owner ||
repositoryName !== github.context.repo.repo ||
ref !== github.context.ref ||
!ref.startsWith('refs/pull/') ||
commit !== github.context.sha
) {
return
}
// Head SHA
const expectedHeadSha = fromPayload('after')
if (!expectedHeadSha) {
core.debug('Unable to determine head sha')
return
}
// Base SHA
const expectedBaseSha = fromPayload('pull_request.base.sha')
if (!expectedBaseSha) {
core.debug('Unable to determine base sha')
return
}
// Expected message?
const expectedMessage = `Merge ${expectedHeadSha} into ${expectedBaseSha}`
if (commitInfo.indexOf(expectedMessage) >= 0) {
return
}
// Extract details from message
const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/)
if (!match) {
core.debug('Unexpected message format')
return
}
// Post telemetry
const actualHeadSha = match[1]
if (actualHeadSha !== expectedHeadSha) {
core.debug(
`Expected head sha ${expectedHeadSha}; actual head sha ${actualHeadSha}`
)
const octokit = github.getOctokit(token, {
baseUrl: getServerApiUrl(baseUrl),
userAgent: `actions-checkout-tracepoint/1.0 (code=STALE_MERGE;owner=${repositoryOwner};repo=${repositoryName};pr=${fromPayload(
'number'
)};run_id=${
process.env['GITHUB_RUN_ID']
};expected_head_sha=${expectedHeadSha};actual_head_sha=${actualHeadSha})`
})
await octokit.rest.repos.get({
owner: repositoryOwner,
repo: repositoryName
})
}
} catch (err) {
core.debug(
`Error when validating commit info: ${(err as any)?.stack ?? err}`
)
}
}
function fromPayload(path: string): any {
return select(github.context.payload, path)
}
function select(obj: any, path: string): any {
if (!obj) {
return undefined
}
const i = path.indexOf('.')
if (i < 0) {
return obj[path]
}
const key = path.substr(0, i)
return select(obj[key], path.substr(i + 1))
}

5
src/regexp-helper.ts Normal file
View File

@ -0,0 +1,5 @@
export function escape(value: string): string {
return value.replace(/[^a-zA-Z0-9_]/g, x => {
return `\\${x}`
})
}

61
src/retry-helper.ts Normal file
View File

@ -0,0 +1,61 @@
import * as core from '@actions/core'
const defaultMaxAttempts = 3
const defaultMinSeconds = 10
const defaultMaxSeconds = 20
export class RetryHelper {
private maxAttempts: number
private minSeconds: number
private maxSeconds: number
constructor(
maxAttempts: number = defaultMaxAttempts,
minSeconds: number = defaultMinSeconds,
maxSeconds: number = defaultMaxSeconds
) {
this.maxAttempts = maxAttempts
this.minSeconds = Math.floor(minSeconds)
this.maxSeconds = Math.floor(maxSeconds)
if (this.minSeconds > this.maxSeconds) {
throw new Error('min seconds should be less than or equal to max seconds')
}
}
async execute<T>(action: () => Promise<T>): Promise<T> {
let attempt = 1
while (attempt < this.maxAttempts) {
// Try
try {
return await action()
} catch (err) {
core.info((err as any)?.message)
}
// Sleep
const seconds = this.getSleepAmount()
core.info(`Waiting ${seconds} seconds before trying again`)
await this.sleep(seconds)
attempt++
}
// Last attempt
return await action()
}
private getSleepAmount(): number {
return (
Math.floor(Math.random() * (this.maxSeconds - this.minSeconds + 1)) +
this.minSeconds
)
}
private async sleep(seconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
}
}
export async function execute<T>(action: () => Promise<T>): Promise<T> {
const retryHelper = new RetryHelper()
return await retryHelper.execute(action)
}

60
src/state-helper.ts Normal file
View File

@ -0,0 +1,60 @@
import * as core from '@actions/core'
/**
* Indicates whether the POST action is running
*/
export const IsPost = !!core.getState('isPost')
/**
* The repository path for the POST action. The value is empty during the MAIN action.
*/
export const RepositoryPath = core.getState('repositoryPath')
/**
* The set-safe-directory for the POST action. The value is set if input: 'safe-directory' is set during the MAIN action.
*/
export const PostSetSafeDirectory = core.getState('setSafeDirectory') === 'true'
/**
* The SSH key path for the POST action. The value is empty during the MAIN action.
*/
export const SshKeyPath = core.getState('sshKeyPath')
/**
* The SSH known hosts path for the POST action. The value is empty during the MAIN action.
*/
export const SshKnownHostsPath = core.getState('sshKnownHostsPath')
/**
* Save the repository path so the POST action can retrieve the value.
*/
export function setRepositoryPath(repositoryPath: string) {
core.saveState('repositoryPath', repositoryPath)
}
/**
* Save the SSH key path so the POST action can retrieve the value.
*/
export function setSshKeyPath(sshKeyPath: string) {
core.saveState('sshKeyPath', sshKeyPath)
}
/**
* Save the SSH known hosts path so the POST action can retrieve the value.
*/
export function setSshKnownHostsPath(sshKnownHostsPath: string) {
core.saveState('sshKnownHostsPath', sshKnownHostsPath)
}
/**
* Save the set-safe-directory input so the POST action can retrieve the value.
*/
export function setSafeDirectory() {
core.saveState('setSafeDirectory', 'true')
}
// Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic.
// This is necessary since we don't have a separate entry point.
if (!IsPost) {
core.saveState('isPost', 'true')
}

45
src/url-helper.ts Normal file
View File

@ -0,0 +1,45 @@
import * as assert from 'assert'
import {URL} from 'url'
import {IGitSourceSettings} from './git-source-settings'
export function getFetchUrl(settings: IGitSourceSettings): string {
assert.ok(
settings.repositoryOwner,
'settings.repositoryOwner must be defined'
)
assert.ok(settings.repositoryName, 'settings.repositoryName must be defined')
const serviceUrl = getServerUrl(settings.githubServerUrl)
const encodedOwner = encodeURIComponent(settings.repositoryOwner)
const encodedName = encodeURIComponent(settings.repositoryName)
if (settings.sshKey) {
return `git@${serviceUrl.hostname}:${encodedOwner}/${encodedName}.git`
}
// "origin" is SCHEME://HOSTNAME[:PORT]
return `${serviceUrl.origin}/${encodedOwner}/${encodedName}`
}
export function getServerUrl(url?: string): URL {
let urlValue =
url && url.trim().length > 0
? url
: process.env['GITHUB_SERVER_URL'] || 'https://github.com'
return new URL(urlValue)
}
export function getServerApiUrl(url?: string): string {
let apiUrl = 'https://api.github.com'
if (isGhes(url)) {
const serverUrl = getServerUrl(url)
apiUrl = new URL(`${serverUrl.origin}/api/v3`).toString()
}
return apiUrl
}
export function isGhes(url?: string): boolean {
const ghUrl = getServerUrl(url)
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
}

View File

@ -0,0 +1,30 @@
import * as core from '@actions/core'
import * as fs from 'fs'
/**
* Gets the organization ID of the running workflow or undefined if the value cannot be loaded from the GITHUB_EVENT_PATH
*/
export async function getOrganizationId(): Promise<number | undefined> {
try {
const eventPath = process.env.GITHUB_EVENT_PATH
if (!eventPath) {
core.debug(`GITHUB_EVENT_PATH is not defined`)
return
}
const content = await fs.promises.readFile(eventPath, {encoding: 'utf8'})
const event = JSON.parse(content)
const id = event?.repository?.owner?.id
if (typeof id !== 'number') {
core.debug('Repository owner ID not found within GITHUB event info')
return
}
return id as number
} catch (err) {
core.debug(
`Unable to load organization ID from GITHUB_EVENT_PATH: ${(err as any)
.message || err}`
)
}
}

View File

@ -10,7 +10,8 @@
"declaration": true,
"strict": true,
"noImplicitAny": false,
"esModuleInterop": true
"esModuleInterop": true,
"skipLibCheck": true
},
"exclude": ["__test__", "lib", "node_modules"]
}