Compare commits

..

145 Commits

Author SHA1 Message Date
Rob Herley eb7ed88d77 Merge branch 'main' into robherley/artifact-digest 2022-05-19 11:45:42 -04:00
Konrad Pabjan 500d0b42fe Bump all packages that have @actions/http-client as a dependency (#1088)
* bump @actions/artifact

* update @actions/cache

* Update @actions/core

* Update @actions/github

* update @actions/tool-cache
2022-05-13 11:12:58 -04:00
Konrad Pabjan 82efa3d285 Bump @actions/http-client in all packages that use it (#1087) 2022-05-13 10:58:36 -04:00
Konrad Pabjan 2abc7c46f8 Bump to version 2.0.1 (#1086) 2022-05-13 10:29:29 -04:00
Konrad Pabjan e48f1d0c54 Make tunnel a prod dependency for http-client (#1085) 2022-05-13 10:19:06 -04:00
Brian Cristante aa676f3cc7 Update other packages to use http-client v2 (#1082)
We moved `@actions/http-client` to be part of the toolkit in https://github.com/actions/toolkit/pull/1062.  We also made some breaking changes to exported types and released v2.

The biggest change in terms of lines of code affected was to get rid of the `I-` prefix for interfaces since TypeScript doesn't follow this convention.

I bumped the patch version of all packages except for `tool-cache`, where I bumped the major version.  The rationale is explained in the release notes for that package.
2022-05-11 17:14:25 -04:00
Rob Herley 925ae6978b Merge pull request #1074 from actions/robherley/core-1.8.0-release
@actions/core 1.8.0 release
2022-05-05 16:24:45 -04:00
Rob Herley e73063a93c @actions/core 1.8.0 release 2022-05-05 20:17:30 +00:00
Rob Herley c4ae214c26 Merge pull request #1073 from actions/robherley/deprecate-markdownsummary
Add & deprecate old markdownSummary export
2022-05-05 16:07:16 -04:00
Rob Herley 07242b37a4 add & deprecate old markdownSummary export 2022-05-05 19:44:13 +00:00
Rob Herley 01aceeaad6 Merge pull request #1072 from actions/robherley/not-markdown-summaries
Rename core's `markdownSummary` extension to `summary`
2022-05-05 14:17:33 -04:00
Rob Herley 3d29fb91d1 sed 's/markdownSummary/summary/g' 2022-05-05 17:29:20 +00:00
Rob Herley 35e5aac523 actually use md5 not sha256 2022-05-05 10:32:09 -04:00
Rob Herley a3c696e88e add md5 and use b64 for digest encodings 2022-05-05 09:26:38 -04:00
Brian Cristante 91b7bf978c Move @actions/http-client into the toolkit (#1062)
💡 See https://github.com/actions/toolkit/pull/1064 for a better diff!

https://github.com/actions/toolkit contains a variety of packages used for building actions.  https://github.com/actions/http-client is one such package, but lives outside of the toolkit.  Moving it inside of the toolkit will improve discoverability and reduce the number of repos we have to keep track of for maintenance tasks (such as github/c2c-actions-service#2937).

I checked with @bryanmacfarlane on the historical decision here.  Apparently it was just inertia from before we released the toolkit as multiple packages.

The benefits here are:
- Have one fewer repo to keep track of
- Signal that this is an HTTP client meant for building actions, not for general use.

## Notes
- `@actions/http-client` will continue to be released as its own package.
- Bumping the package version to **2.0.0**.  Since we're compiling in strict mode now, there are some breaking changes to the exported types.  This is an improvement because the null-unsafe version of`http-client` is currently breaking the safety of null-safe consumers.
- I'm not updating the other packages to use the new version in this PR.  I plan to do that in a follow-up.  We'll hold off on publishing `http-client` v2 to NPM until that's done just in case other changes shake out of it.
2022-05-03 11:10:13 -04:00
Rob Herley b68735e060 misc pr feedback 2022-04-27 22:24:16 +00:00
Rob Herley d5c547c19f update crc header key 2022-04-27 22:22:35 +00:00
Rob Herley 9e285cc3fa crc: update test header data 2022-04-26 23:21:31 +00:00
Rob Herley 3f95e2ea4f crc: update headers & digest stream 2022-04-26 22:59:56 +00:00
Rob Herley fccc5ee6e6 crc: move tbl out of class, more tests 2022-04-26 17:31:17 +00:00
Rob Herley 3d61fe8000 add crc64 & tests 2022-04-26 17:13:40 +00:00
Rob Herley 9387bd7ded Merge branch 'main' into robherley/artifact-digest 2022-04-26 15:54:22 +00:00
Rob Herley 3e2837ddce Merge pull request #1059 from actions/robherley/core-1.7.0-release
@actions/core 1.7.0 release
2022-04-25 09:14:37 -04:00
Rob Herley 3048a9d72c @actions/core 1.7.0 release 2022-04-20 20:42:50 +00:00
Rob Herley 91f9153ca8 Merge pull request #1014 from actions/robherley/md-summaries
feat: @actions/core extensions for markdown summary
2022-04-20 16:28:27 -04:00
Rob Herley eef3e92175 summary: remove limit validation in client 2022-04-20 20:10:56 +00:00
Rob Herley ed87cc6ce3 summary: increase limit to 1MiB 2022-04-20 19:55:54 +00:00
Thomas Boop af45ad8eaa Glob 0.3.0 release (#1056)
* Revert "Exec 1.2.0 patch"

c9f7927778

* glob 0.3.0 release
2022-04-18 15:49:18 -04:00
Thomas Boop 367a6c2423 Exec 1.2.0 patch (#1055) 2022-04-18 14:52:32 -04:00
ruvceskistefan 8f2bd5d713 Added verbose mode in hashFiles (#1052)
* Added verbose mode in hashFiles

* Code formatting

* Change verboseMode arg to verbose

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

* Using verbose instead of verboseMode as arg

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
2022-04-18 14:29:24 -04:00
Anurag Chauhan 7654d97eb6 Merge pull request #1050 from actions/fix_cache_error_type
Fix error type so that cache size limit violation is shown as warning( similar to previous error)
2022-04-07 14:46:23 +05:30
Anurag Chauhan 0b2505c754 Fix error type so that size limit violation is shown as warning 2022-04-07 09:08:16 +00:00
Rob Herley e3549a9c58 artifact: tests for stream digest util 2022-04-05 09:31:49 -04:00
Rob Herley c5d1911357 add digest header for uploads 2022-04-04 17:54:15 -04:00
Deepak Dahiya f8a69bc473 Added cacheSize in ReserveCache API request (#1044)
* Added cacheSize in ReserveCache API request

* minor

* minor

* minor

* Cleanup

* package-lock revert

* Modified tests

* New Response Type

* cleanup

* Linting

* Lint fix

* Resolved comments

* Added tests

* package-lock

* Resolved package-lock mismatch

* Liniting

* Update packages/cache/src/cache.ts

Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com>

* Linting issue

* Resolved few comments

* version upgrade

* Savecache tests

* RequestUtil test

* test

* test

* test

* test

* test

* test

* test

* test

* test

* test

Co-authored-by: Apple <apple@Apples-MacBook-Pro.local>
Co-authored-by: Bishal Prasad <bishal-pdmsft@github.com>
2022-04-04 16:21:58 +05:30
Deepak Dahiya 03eca1b0c7 Revert "T dedah/cache size" (#1042) 2022-04-01 02:10:08 +05:30
Deepak Dahiya 4b12bd3649 Merge pull request #1041 from t-dedah/t-dedah/cacheSize
T dedah/cache size
2022-04-01 02:07:14 +05:30
Apple 6cd8286138 Resolved comments 2022-04-01 01:41:12 +05:30
Apple 3abbc6c24c Lint fix 2022-04-01 01:16:43 +05:30
Luke Tomlinson d594f1e4b3 Fix npm audit (#1040) 2022-03-31 14:40:06 -04:00
Luke Tomlinson 4a2602dd58 Fix test for windows 2022 (#1039) 2022-03-31 14:39:39 -04:00
Luke Tomlinson 745f129332 Update @actions/github to 5.0.1 (#1038)
* Update @actions/github to 5.0.1

* Fix package-lock.json
2022-03-31 13:53:55 -04:00
Luke Tomlinson 7e7e8d4206 Update octokit dependencies (#1037) 2022-03-31 10:36:40 -04:00
Deepak Dahiya daa24d7958 Linting 2022-03-31 10:41:54 +00:00
Deepak Dahiya bda035c74d cleanup 2022-03-31 09:44:40 +00:00
Deepak Dahiya 79acd5bac4 New Response Type 2022-03-31 09:42:59 +00:00
Apple b602df7c05 Modified tests 2022-03-30 13:27:36 +05:30
Deepak Dahiya 80a66f3298 package-lock revert 2022-03-30 07:33:48 +00:00
Deepak Dahiya 7756e7c4cb Cleanup 2022-03-30 07:22:56 +00:00
Deepak Dahiya 6d774fcb59 minor 2022-03-29 22:21:15 +00:00
Deepak Dahiya f05c940e43 minor 2022-03-29 22:20:22 +00:00
Deepak Dahiya a71585a450 minor 2022-03-29 22:19:10 +00:00
Deepak Dahiya 76ac2fcd59 Added cacheSize in ReserveCache API request 2022-03-29 22:17:04 +00:00
Shubham Tiwari b463992869 Adding support in cache package to check if Artifact Cache service is enabled or not (#1028)
* Added support to check if Artifact cache service is enabled or not.

* enablingForGHES

* added ACTIONS_CACHE_URL in fixtures

* Fix CI

* CI fix

* changed function name

* Function rename

* Updated release

* added test case

* Update RELEASES.md

* Lint errors

* lint

* linting

* lint

* update name to actions service

* Update packages/cache/src/internal/cacheUtils.ts

Co-authored-by: Brian Cristante <33549821+brcrista@users.noreply.github.com>

* review comments

* linting

* linting

* push to start CI

* Update RELEASES.md

* remove extra spaces

* reverting version update

* Revert "reverting version update"

This reverts commit af84eba61e.

* Update RELEASES.md

Co-authored-by: Brian Cristante <33549821+brcrista@users.noreply.github.com>
2022-03-25 14:40:02 +05:30
Ashwin Sangem 39b9640642 Merge pull request #1030 from actions/users/ashwinsangem/fix_download_chunk_cap
Cap the cache download chunk to 2 GB
2022-03-24 19:11:52 +05:30
Ashwin Sangem f0a876ab8b bumped up the @action/cache version. 2022-03-24 11:19:52 +00:00
Ashwin Sangem 58406447b5 Fixed toolkit audit by running npm audit fix. 2022-03-23 11:24:15 +00:00
Ashwin Sangem 087191dabd Update downloadUtils.ts 2022-03-23 16:46:52 +05:30
Ashwin Sangem 862c4e9db4 Cap the cache download chunk to 2 GB 2022-03-23 16:44:40 +05:30
Jonathan Tamsut d1abf7dc74 Update lockfileVersion in package-lock.json in tool-cache package (#1025)
* update packages

* update RELEASE

* update RELEASE

* remove extra README line
2022-03-16 11:27:30 -07:00
Jonathan Tamsut 475192a0c3 Update lockfileVersion in package-lock.json in cache package (#1022)
* update versions

* update release notes
2022-03-16 11:25:03 -07:00
Jonathan Tamsut c07c5fc410 Update lockfileVersion in package-lock.json in glob package (#1023)
* update versions

* update RELEASE file
2022-03-16 11:24:24 -07:00
Jonathan Tamsut b820a0ff59 Update lockfileVersion in package-lock.json in exec package (#1024)
* update packages

* update RELEASE
2022-03-16 11:24:01 -07:00
Jonathan Tamsut 72dfadb0c3 Update lockfileVersion in package-lock.json in io package (#1020)
* update lockfileVersion

* Update package
2022-03-16 11:23:44 -07:00
Rob Herley edee7cde32 feedback: add summary write options 2022-03-08 16:37:20 -05:00
Rob Herley 6295f5d25b summary: consistent kB usage and doc links 2022-03-03 11:46:32 -05:00
Rob Herley 339dd63bec summary: method to clear file and buffer 2022-03-02 23:56:30 -05:00
Rob Herley d27bf857e6 add -> addRaw 2022-03-02 23:49:17 -05:00
Rob Herley ec5c955c0a summary: additional check for max size limit 2022-03-02 23:43:51 -05:00
Rob Herley 302a5b31d8 summary: add link/anchor element 2022-03-02 12:10:01 -05:00
Rob Herley ab2b23c50d summary: add tests 2022-03-02 00:58:18 -05:00
Rob Herley 70a01b86d3 summary: self closing tags, additional img attrs & minor fixes 2022-03-02 00:57:46 -05:00
Rob Herley ff80a82f7c Merge branch 'main' into robherley/md-summaries 2022-03-01 21:38:28 -05:00
Rob Herley 0fc0befe24 export markdownSummary singleton from core 2022-03-01 21:32:26 -05:00
Rob Herley 7d95d2cec9 summary.ts -> markdown-summary.ts 2022-03-01 21:16:35 -05:00
Rob Herley c42d30607b add more summary elements, clean up jsdoc 2022-03-01 21:14:58 -05:00
Rob Herley ac58d176ba '\n' -> os.EOL 2022-03-01 20:55:43 -05:00
Rob Herley 518ef1b79e html element wrapper method for md summary 2022-03-01 20:36:04 -05:00
Jonathan Tamsut a502af8759 Merge pull request #1009 from actions/jtamsut/update-artifact-file-version
Update `lockfileVersion` for artifact package
2022-03-01 12:47:16 -08:00
Jonathan Tamsut 5905c6b5c1 Bump major version 2022-03-01 12:36:05 -08:00
Jonathan Tamsut 5e37db2c2b update lockfileVersion for artifact 2022-03-01 12:10:10 -08:00
Rob Herley d496b07cc0 addText -> add, newline by default 2022-02-23 18:15:26 -05:00
Rob Herley 7a2eceac36 initial markdown summary utils 2022-02-23 18:09:05 -05:00
Vipul fcb8c4ca79 Merge pull request #991 from actions/fix-dep-cache
Update ms-rest-js and storage-blog dependencies for cache
2022-02-08 09:58:05 +05:30
vsvipul 4a793fd385 Update RELEASES.md 2022-02-04 14:11:29 +05:30
Brian Cristante 15e2399826 Update CODEOWNERS with new teams (#990)
* Use the actions-cache team as owner of the cache package

* Update CODEOWNERS
2022-02-02 12:43:38 -05:00
vsvipul 39a1ec60b2 Bump up patch version 2022-02-01 17:15:42 +05:30
vsvipul eafa9d39d3 Update ms-rest-js and storage-blog dependencies for cache 2022-02-01 16:24:23 +05:30
Konrad Pabjan daf8bb0060 0.6.1 release (#964) 2021-12-14 16:01:55 -05:00
Zoran Regvart 37f5a85219 fix: drop support for named pipes on Windows (#962)
Seems that folk are having issues with uploading 0-byte files from
Windows agents. This effectively removes the support for Windows for
uploading from named files that, due to `isFIFO` returning `false` on
Windows for named pipes created using MSYS2's `mkfifo` command, resorted
to checking if the file size is 0 - a common trait of named pipes.

See https://github.com/actions/upload-artifact/issues/281
2021-12-14 15:50:50 -05:00
Konrad Pabjan d1a6612b14 Update releases.yml (#960) 2021-12-07 10:38:25 -05:00
Konrad Pabjan 6fcdd6ab0d [Artifacts] Prep for @actions/artifact 0.6.0 release (#958)
* actions-artifact-0.6.0 release

* Fix lint issue

* Update RELEASES.md
2021-12-06 18:39:23 -05:00
Konrad Pabjan 45a3c7bf81 [Artifacts] More detailed information for chunked uploads (#957)
* More detailed information for chunked uploads

* Run npm format
2021-12-06 16:48:14 -05:00
Konrad Pabjan cdd4e107a6 [Artifacts] Exempt certain types of files from gzip compression (#956)
* Exempt certain types of files from gzip compression

* Fix lint issue
2021-12-06 16:47:44 -05:00
Konrad Pabjan 88062ec473 Check for newlines and carriage return in artifact paths and name (#951)
* Check for newlines and carriage return in artifact paths and name

* Fix linting issue

* Update comments

* Add comment about spacing

* Remove extra space
2021-12-01 16:31:37 -05:00
Konrad Pabjan 4df5abb3ee Updates to logging for artifact uploads (#949)
* More details logs during artifact upload

* extra logging

* Updates to artifact logging + clarifications around upload size

* Fix linting errors

* Update packages/artifact/src/internal/artifact-client.ts

Co-authored-by: campersau <buchholz.bastian@googlemail.com>

Co-authored-by: campersau <buchholz.bastian@googlemail.com>
2021-11-30 12:53:24 -05:00
campersau e19e4261da Reset processedCount when downloading all artifacts (#889) 2021-11-29 17:28:03 -05:00
Ichinose Shogo e9b0746ee3 artifact: @types/tmp should be devDependencies (#860) 2021-11-29 17:22:33 -05:00
Zoran Regvart 7932c147a0 Support upload from named pipes (#748)
Named pipes report file size as 0, which leads to reading the whole
content into memory (0 is less than 64K). This adds additional check to
make sure that the passed in path is not a named pipe, and in that case
opts for the create-temp-file-to-gzip code path.

When running on GitHub Actions infrastructure on `windows` node, named
pipes can be created using `mkfifo` from MSYS2. In that case `fs.Stats`s
`isFIFO()` returns `false`, and not `true` as expected. This case is
detected by `process.platform` being `win32` and the passed file having
length of 0.

As a side note, when MSYS2's `mkfifo` is run, a pipe file is created:

```
prw-rw-rw- 1 User None  0 Mar 31 12:58 pipe
```

If `fs.stat` is invoked at this point `ENOENT` error will be thrown. As
soon as the pipe is written to, this pipe file is replaced by two same-
named files:

```
-rw-r--r-- 1 User None  0 Mar 31 13:00 pipe
-rw-r--r-- 1 User None  0 Mar 31 13:00 pipe
```

And at this point `fs.stat` `isFIFO()` returns `false`. Even though the
file acts as a named pipe.
2021-11-29 17:19:02 -05:00
Aparna Ravindra 45d2019161 Cache: Increasing client validation to 10GB (#934)
* increasing client validation limit in cache package to 10gb
2021-11-19 16:34:33 +05:30
Luke Tomlinson e2eeb0a784 Fix high sev in github package (#924) 2021-10-15 15:26:30 -04:00
Luke Tomlinson 6ce349e08c Update High Severity Dev Dependencies (#923)
* Update deps

* More Updates

* Use npm 7

* Update package-lock.json
2021-10-14 09:20:09 -04:00
Thomas Boop 27f76dfe1a Full release of actions/core 1.6.0 with oidc behavior (#919)
* OIDC Client for actions/core

Co-authored-by: Sourav Chanduka <souravchanduka37@gmail.com>
Co-authored-by: Sourav Chanduka <souravchanduka@users.noreply.github.com>
Co-authored-by: Tingluo Huang <tingluohuang@github.com>
2021-09-28 12:55:21 -04:00
Marcono1234 60145e408c Add file property to AnnotationProperties (#896) 2021-09-28 09:47:06 -04:00
Luke Tomlinson ea81280a4d Update release for core 1.5.0 (#873)
* Update release for core 1.5.0

* Update RELEASES.md

* Run npm audit fix
2021-08-18 09:26:19 -04:00
Luke Tomlinson f0b00fd201 Add notice annotation and support more annotation fields (#855)
* Add support for notice annotation and additional properties

* Add additional tests

* Update readme

* Change casing for endLine and endColumn

* Update utils.ts

* Update README.md

* Rename files to have internal- nomenclature

* Revert "Rename files to have internal- nomenclature"

This reverts commit 7911689f29.

* Update utils.ts
2021-07-28 17:34:31 -04:00
Rob Cowsill 4564768940 Delete temporary archive after cache upload (#792)
This is to avoid filling the SSD while saving multiple large caches
2021-06-28 17:27:09 +02:00
Brian Cristante a31b7eca9e Bump artifact package version to v0.5.2 (#845)
* bump version in package*.json

* changelog
2021-06-16 09:37:06 -04:00
Brian Cristante 9167ce1f3a Resolve vulnerabilities found by npm audit (#846) 2021-06-16 09:20:08 -04:00
Thomas Boop 11601c0d2d Release new version of the tool-cache (#838)
* update to latest version of @actions/io

* Release new version and update dependencies

* add pr number
2021-06-07 15:50:05 -04:00
Thomas Boop b9414eecb3 we really shouldn't warn on these errors, action author can decide what to do (#837) 2021-06-07 15:31:03 -04:00
Thomas Boop 243a8bba07 New versions of toolkit packages (#835) 2021-06-07 15:09:34 -04:00
Thomas Boop c5e1af5dc3 Add HashFiles to the toolkit (#830)
* add hash files to the toolkit
2021-06-07 14:26:00 -04:00
Thomas Boop c9af6bb1b3 Update escaping rules in io's rmRF (#828)
* Better Handling of escaping in rmrf
2021-06-07 14:16:16 -04:00
Luke Tomlinson bf4ce74a0f Update @actions/exec to 1.1.0 (#834) 2021-06-07 10:09:34 -04:00
Brian Cristante db21627995 Retry artifact uploads on HTTP 500 (#833)
* Retry on 500

* bump package version

* fix tests

* Remove spurious change

* fix another test

* Roll back package version
2021-06-04 17:09:30 -04:00
Thomas Boop bb2f39337d Sarpik/get input list support (#829)
* feat(core): Create `getInputList` utility

Signed-off-by: Kipras Melnikovas <kipras@kipras.org>

* chore(core): Document usage of '\n' instead of [] @ `getInputList`

Signed-off-by: Kipras Melnikovas <kipras@kipras.org>

* test(core): Create a very simple test for `getInputList`

Signed-off-by: Kipras Melnikovas <kipras@kipras.org>

* run linter

* update commands/readme

Co-authored-by: Kipras Melnikovas <kipras@kipras.org>
2021-06-04 09:28:49 -04:00
Thomas Boop dc4b4dab1d add the lint-fix script (#831) 2021-06-04 09:25:13 -04:00
Luke Tomlinson 8df94d9879 Add test for large stdline output (#827)
* Add test for large stdline output

* Format/Lint

* Update stdlineoutput.js

* Update stdlineoutput.js
2021-06-03 09:31:48 -04:00
Andrey Savitsky c5035362ab Fix broken line buffers (#773)
* Fix broken line buffers

* Code style
2021-06-02 16:29:46 -04:00
Matisse Hack 439eaced07 Add directory filtering to globber (#728)
Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
2021-06-01 15:57:03 -04:00
Thomas Boop 51dc07a106 Only run codeql on main branch pushes (#826) 2021-06-01 10:11:52 -04:00
dependabot[bot] 36b8c66aec Bump ws from 7.4.5 to 7.4.6 in /packages/github (#824)
Bumps [ws](https://github.com/websockets/ws) from 7.4.5 to 7.4.6.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.4.5...7.4.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-01 09:50:37 -04:00
dependabot[bot] aa29345ae8 Bump ws from 7.2.3 to 7.4.6 (#823)
Bumps [ws](https://github.com/websockets/ws) from 7.2.3 to 7.4.6.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.2.3...7.4.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-01 09:50:25 -04:00
Sergey Ukustov e1a7863be6 feat: get linux version from os-release file if available (#594) 2021-05-28 15:40:45 -04:00
Thomas Boop c507914181 Fix debug logging link (#820) (#822)
Co-authored-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2021-05-27 14:15:37 -04:00
Thomas Boop a65bca60a1 Tool Cache 1.7.0 release (#821)
* tc 1.7.0 release

* update verbiage
2021-05-27 11:44:59 -04:00
Luke Tomlinson a1b068ec31 Bugfix: Fix issue with interactive unzip on Linux (#807)
* Add new powershell commands for windows unzip

* Test fails to overwrite file

* Add new windows commands for unzip

* Add Test for failing case for both pwsh and powershell

* Modify test to confirm overwrite behavior for xar

* Delete ._test.txt

* Add fallback case for older windows systems

* Remove try

* Run Tests on windows-2016

* Update tar tests to handle existing files

* Lint

* Update tool-cache.test.ts

* Update tool-cache.test.ts

* Update tool-cache.test.ts

* Update tool-cache.test.ts

* Update from PR feedback
2021-05-21 17:01:42 -04:00
Luke Tomlinson 6e33c78c3d Update @actions/glob to 0.1.2 (#818) 2021-05-21 15:50:31 -04:00
Luke Tomlinson 9ac66375a0 Fix flakey test (#817) 2021-05-21 15:32:09 -04:00
Luke Tomlinson ddd04b6997 Add getExecOutput function (#814)
* Add getExecOutput function

* Add tests for exec output

* Modify tests to not rely on buffer size, but only test larger output

* Handle split multi-byte characters + PR feedback

* Fix tests

* Lint

* Update how split byte are sent for tests
2021-05-21 12:12:16 -04:00
Thomas Boop 566ea66979 prep for actions core 1.3.0 release (#816) 2021-05-21 09:19:53 -04:00
Thomas Boop 0d74e9080a Re-enable the audit tools step and update dependencies (#815)
* update package versions

* run audit

* fix eslint config

* linter updates

* re-enable audit

* update timeouts test

* pass done into callback

* fix format
2021-05-21 09:19:40 -04:00
Chris Mc 8dc2d6eb6a Update location of typescript definitions (#743)
https://github.com/octokit/webhooks.js#typescript
2021-05-20 16:49:57 -04:00
rethab 3bd746139f Describe behaviour of getInput (#808) 2021-05-19 11:08:51 -04:00
Thomas Boop f915ace085 add release notes (#809) 2021-05-14 14:43:08 -04:00
dependabot[bot] 1bafbed467 Bump lodash from 4.17.15 to 4.17.21 (#801)
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-05-14 14:39:47 -04:00
Luke Tomlinson 98549fbf21 Prevent accidental file matching when glob contains trailing slash (#805)
* Draft Solution

* Update internal-globber.ts

* Cleanup

* Fix Test

* Cleanup
2021-05-14 14:12:26 -04:00
Luke Tomlinson b33912b7cc Core: Add trimWhitespace to getInput (#802)
* Add option to not trim whitespace from inputs

* Fix typos

* Add doc clarification

* Rename options
2021-05-11 13:51:36 -04:00
dependabot[bot] cac7db2d19 Bump handlebars from 4.5.3 to 4.7.7 (#799)
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-05-07 16:37:28 -04:00
Luke Tomlinson 1c367e0a26 Export isExplicitVersion and evaluateVersions (#796)
* Export isExplicitVersion and evaluateVersions

* Lint

* Add docs
2021-05-07 16:13:26 -04:00
Luke Tomlinson 09e59b9a5c Exec: throw error when cwd option does not exist (#793)
* Exec: throw error when cwd option does not exist

* Simplify promise rejection
2021-05-07 16:12:40 -04:00
dependabot[bot] fecf6cdd59 Bump hosted-git-info from 2.7.1 to 2.8.9 (#800)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.7.1 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.7.1...v2.8.9)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-07 14:17:30 -04:00
Thomas Boop 2b97eb3192 Include urls in @actions/github context (#794)
* include urls in github context

* fix format
2021-05-07 14:05:08 -04:00
Thomas Boop ed490dc20d Update dependencies of tool-cache to fix npm audit (#795)
* Update dependencies to resolve security issue

* run npm audit fix in `actions/github`

* update jest as well to newest version
2021-05-07 14:04:38 -04:00
124 changed files with 32583 additions and 25192 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
node_modules/
packages/*/node_modules/
packages/*/lib/
packages/*/lib/
packages/glob/__tests__/_temp
+18 -6
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,
@@ -9,20 +9,34 @@
},
"rules": {
"eslint-comments/no-use": "off",
"github/no-then": "off",
"import/no-namespace": "off",
"no-shadow": "off",
"no-unused-vars": "off",
"no-undef": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-ignore": "error",
"@typescript-eslint/ban-ts-comment": "error",
"camelcase": "off",
"@typescript-eslint/camelcase": "off",
"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/consistent-type-assertions": "off",
"@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/naming-convention": [
"error",
{
"format": null,
"filter": {
// you can expand this regex as you find more cases that require quoting that you want to allow
"regex": "^[A-Z][A-Za-z]*$",
"match": true
},
"selector": "memberLike"
}
],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
@@ -32,7 +46,6 @@
"@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 +53,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",
+11 -4
View File
@@ -44,24 +44,27 @@ jobs:
npm ci
npm run tsc
working-directory: packages/artifact
- name: Set artifact file contents
shell: bash
run: |
echo "non-gzip-artifact-content=hello" >> $GITHUB_ENV
echo "gzip-artifact-content=Some large amount of text that has a compression ratio that is greater than 100%. If greater than 100%, gzip is used to upload the file" >> $GITHUB_ENV
echo "empty-artifact-content=_EMPTY_" >> $GITHUB_ENV
- name: Create files that will be uploaded
run: |
mkdir artifact-path
mkdir artifact-path
echo ${{ env.non-gzip-artifact-content }} > artifact-path/world.txt
echo ${{ env.gzip-artifact-content }} > artifact-path/gzip.txt
touch artifact-path/empty.txt
# We're using node -e to call the functions directly available in the @actions/artifact package
- name: Upload artifacts using uploadArtifact()
run: |
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-1',['artifact-path/world.txt'], '${{ github.workspace }}'))"
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-2',['artifact-path/gzip.txt'], '${{ github.workspace }}'))"
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-3',['artifact-path/empty.txt'], '${{ github.workspace }}'))"
- name: Download artifacts using downloadArtifact()
run: |
@@ -69,12 +72,15 @@ jobs:
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-1','artifact-1-directory'))"
mkdir artifact-2-directory
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-2','artifact-2-directory'))"
mkdir artifact-3-directory
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-3','artifact-3-directory'))"
- name: Verify downloadArtifact()
shell: bash
run: |
packages/artifact/__tests__/test-artifact-file.sh "artifact-1-directory/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
packages/artifact/__tests__/test-artifact-file.sh "artifact-2-directory/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
packages/artifact/__tests__/test-artifact-file.sh "artifact-3-directory/artifact-path/empty.txt" "${{ env.empty-artifact-content }}"
- name: Download artifacts using downloadAllArtifacts()
run: |
@@ -85,4 +91,5 @@ jobs:
shell: bash
run: |
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-1/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-2/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-2/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-3/artifact-path/empty.txt" "${{ env.empty-artifact-content }}"
+3 -2
View File
@@ -31,8 +31,9 @@ jobs:
- name: Bootstrap
run: npm run bootstrap
# - name: audit tools #disabled while we wait for https://github.com/actions/toolkit/issues/539
# run: npm audit --audit-level=moderate
- name: audit tools
# `|| npm audit` to pretty-print the output if vulnerabilies are found after filtering.
run: npm audit --audit-level=moderate --json | scripts/audit-allow-list || npm audit --audit-level=moderate
- name: audit packages
run: npm run audit-all
+2
View File
@@ -2,6 +2,8 @@ name: "Code Scanning - Action"
on:
push:
branches:
- main
pull_request:
schedule:
- cron: '0 0 * * 0'
+19 -14
View File
@@ -5,8 +5,8 @@ on:
inputs:
package:
required: true
description: 'core, artifact, cache, exec, github, glob, io, tool-cache'
description: 'core, artifact, cache, exec, github, glob, http-client, io, tool-cache'
jobs:
test:
runs-on: macos-latest
@@ -17,35 +17,40 @@ jobs:
- name: verify package exists
run: ls packages/${{ github.event.inputs.package }}
- name: Set Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: npm install
run: npm install
- name: bootstrap
run: npm run bootstrap
- name: build
run: npm run build
- name: test
run: npm run test
- name: pack
run: npm pack
working-directory: packages/${{ github.event.inputs.package }}
- name: upload artifact
uses: actions/upload-artifact@v2
with:
name: ${{ github.event.inputs.package }}
path: packages/${{ github.event.inputs.package }}/*.tgz
publish:
runs-on: macos-latest
needs: test
environment: npm-publish
steps:
- name: download artifact
uses: actions/download-artifact@v2
with:
@@ -53,7 +58,7 @@ jobs:
- name: setup authentication
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
env:
env:
NPM_TOKEN: ${{ secrets.TOKEN }}
- name: publish
@@ -63,13 +68,13 @@ jobs:
if: failure()
run: |
curl -X POST -H 'Content-type: application/json' --data '{"text":":pb__failed: Failed to publish a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
env:
env:
SLACK_WEBHOOK: ${{ secrets.SLACK }}
- name: notify slack on success
if: success()
run: |
curl -X POST -H 'Content-type: application/json' --data '{"text":":dance: Successfully published a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
env:
env:
SLACK_WEBHOOK: ${{ secrets.SLACK }}
+2 -2
View File
@@ -1,4 +1,4 @@
* @actions/actions-runtime
/packages/artifact/ @actions/actions-service
/packages/cache/ @actions/actions-service
/packages/artifact/ @actions/artifacts-actions
/packages/cache/ @actions/actions-cache
+9
View File
@@ -46,6 +46,15 @@ $ npm install @actions/glob
```
<br/>
:phone: [@actions/http-client](packages/http-client)
A lightweight HTTP client optimized for building actions. Read more [here](packages/http-client)
```bash
$ npm install @actions/http-client
```
<br/>
:pencil2: [@actions/io](packages/io)
Provides disk i/o functions like cp, mv, rmRF, which etc. Read more [here](packages/io)
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+2 -2
View File
@@ -6,7 +6,7 @@ Problem Matchers are a way to scan the output of actions for a specified regex p
Currently, GitHub Actions limit the annotation count in a workflow run.
- 10 warning annotations and 10 error annotations per step
- 10 warning annotations, 10 error annotations, and 10 notice annotations per step
- 50 annotations per job (sum of annotations from all the steps)
- 50 annotations per run (separate from the job annotations, these annotations arent created by users)
@@ -144,6 +144,6 @@ Use ECMAScript regular expression syntax when testing patterns.
### File property getting dropped
[Enable debug logging](https://help.github.com/en/actions/configuring-and-managing-workflows/managing-a-workflow-run#enabling-debug-logging) to determine why the file is getting dropped.
[Enable debug logging](https://docs.github.com/en/actions/managing-workflow-runs/enabling-debug-logging) to determine why the file is getting dropped.
This usually happens when the file does not exist or is not under the workflow repo.
-1
View File
@@ -4,7 +4,6 @@ module.exports = {
roots: ['<rootDir>/packages'],
testEnvironment: 'node',
testMatch: ['**/__tests__/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
+24321 -15358
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -9,24 +9,24 @@
"format": "prettier --write packages/**/*.ts",
"format-check": "prettier --check packages/**/*.ts",
"lint": "eslint packages/**/*.ts",
"lint-fix": "eslint packages/**/*.ts --fix",
"new-package": "scripts/create-package",
"test": "jest --testTimeout 10000"
},
"devDependencies": {
"@types/jest": "^24.0.11",
"@types/node": "^12.12.47",
"@types/signale": "^1.2.1",
"@typescript-eslint/parser": "^2.2.7",
"concurrently": "^4.1.0",
"eslint": "^5.16.0",
"eslint-plugin-github": "^2.0.0",
"eslint-plugin-jest": "^22.5.1",
"@types/jest": "^27.0.2",
"@types/node": "^12.20.13",
"@types/signale": "^1.4.1",
"@typescript-eslint/parser": "^4.0.0",
"concurrently": "^6.1.0",
"eslint": "^7.23.0",
"eslint-plugin-github": "^4.1.3",
"eslint-plugin-jest": "^22.21.0",
"flow-bin": "^0.115.0",
"jest": "^25.1.0",
"jest-circus": "^24.7.1",
"lerna": "^3.18.4",
"jest": "^27.2.5",
"lerna": "^4.0.0",
"prettier": "^1.19.1",
"ts-jest": "^25.4.0",
"ts-jest": "^27.0.5",
"typescript": "^3.9.9"
}
}
+28
View File
@@ -58,3 +58,31 @@
- Bump @actions/http-client to version 1.0.11 to fix proxy related issues during artifact upload and download
### 0.5.2
- Add HTTP 500 as a retryable status code for artifact upload and download.
### 0.6.0
- Support upload from named pipes [#748](https://github.com/actions/toolkit/pull/748)
- Fixes to percentage values being greater than 100% when downloading all artifacts [#889](https://github.com/actions/toolkit/pull/889)
- Improved logging and output during artifact upload [#949](https://github.com/actions/toolkit/pull/949)
- Improvements to client-side validation for certain invalid characters not allowed during upload: [#951](https://github.com/actions/toolkit/pull/951)
- Faster upload speeds for certain types of large files by exempting gzip compression [#956](https://github.com/actions/toolkit/pull/956)
- More detailed logging when dealing with chunked uploads [#957](https://github.com/actions/toolkit/pull/957)
### 0.6.1
- Fix for failing 0 byte file uploads on Windows [#962](https://github.com/actions/toolkit/pull/962)
### 1.0.0
- Update `lockfileVersion` to `v2` in `package-lock.json` [#1009](https://github.com/actions/toolkit/pull/1009)
### 1.0.1
- Update to v2.0.0 of `@actions/http-client`
### 1.0.2
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
+57
View File
@@ -0,0 +1,57 @@
import CRC64, {CRC64DigestEncoding} from '../src/internal/crc64'
const fixtures = {
data:
'🚀 👉😎👉 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n',
expected: {
hex: '846CE4ADAD6223ED',
base64: '7SNira3kbIQ=',
buffer: Buffer.from([0xed, 0x23, 0x62, 0xad, 0xad, 0xe4, 0x6c, 0x84])
}
}
function assertEncodings(crc: CRC64): void {
const encodings = Object.keys(fixtures.expected) as CRC64DigestEncoding[]
for (const encoding of encodings) {
expect(crc.digest(encoding)).toEqual(fixtures.expected[encoding])
}
}
describe('@actions/artifact/src/internal/crc64', () => {
it('CRC64 from string', async () => {
const crc = new CRC64()
crc.update(fixtures.data)
assertEncodings(crc)
})
it('CRC64 from buffer', async () => {
const crc = new CRC64()
const buf = Buffer.from(fixtures.data)
crc.update(buf)
assertEncodings(crc)
})
it('CRC64 from split data', async () => {
const crc = new CRC64()
const splits = fixtures.data.split('\n').slice(0, -1)
for (const split of splits) {
crc.update(`${split}\n`)
}
assertEncodings(crc)
})
it('flips 64 bits', async () => {
const tests = [
[BigInt(0), BigInt('0xffffffffffffffff')],
[BigInt('0xffffffffffffffff'), BigInt(0)],
[BigInt('0xdeadbeef'), BigInt('0xffffffff21524110')]
]
for (const [input, expected] of tests) {
expect(CRC64.flip64Bits(input)).toEqual(expected)
}
})
})
+5 -5
View File
@@ -71,7 +71,7 @@ describe('Download Tests', () => {
setupFailedResponse()
const downloadHttpClient = new DownloadHttpClient()
expect(downloadHttpClient.listArtifacts()).rejects.toThrow(
'List Artifacts failed: Artifact service responded with 500'
'List Artifacts failed: Artifact service responded with 400'
)
})
@@ -113,7 +113,7 @@ describe('Download Tests', () => {
configVariables.getRuntimeUrl()
)
).rejects.toThrow(
`Get Container Items failed: Artifact service responded with 500`
`Get Container Items failed: Artifact service responded with 400`
)
})
@@ -166,7 +166,7 @@ describe('Download Tests', () => {
it('Test retryable status codes during artifact download', async () => {
// The first http response should return a retryable status call while the subsequent call should return a 200 so
// the download should successfully finish
const retryableStatusCodes = [429, 502, 503, 504]
const retryableStatusCodes = [429, 500, 502, 503, 504]
for (const statusCode of retryableStatusCodes) {
const fileContents = Buffer.from('try, try again\n', defaultEncoding)
const targetPath = path.join(root, `FileC-${statusCode}.txt`)
@@ -357,7 +357,7 @@ describe('Download Tests', () => {
plaintext: Buffer | string
): Promise<Buffer> {
if (isGzip) {
return <Buffer>await promisify(gzip)(plaintext)
return await promisify(gzip)(plaintext)
} else if (typeof plaintext === 'string') {
return Buffer.from(plaintext, defaultEncoding)
} else {
@@ -468,7 +468,7 @@ describe('Download Tests', () => {
function setupFailedResponse(): void {
jest.spyOn(HttpClient.prototype, 'get').mockImplementationOnce(async () => {
const mockMessage = new http.IncomingMessage(new net.Socket())
mockMessage.statusCode = 500
mockMessage.statusCode = 400
return new Promise<HttpClientResponse>(resolve => {
resolve({
message: mockMessage,
@@ -0,0 +1,78 @@
import {
checkArtifactName,
checkArtifactFilePath
} from '../src/internal/path-and-artifact-name-validation'
import * as core from '@actions/core'
describe('Path and artifact name validation', () => {
beforeAll(() => {
// mock all output so that there is less noise when running tests
jest.spyOn(console, 'log').mockImplementation(() => {})
jest.spyOn(core, 'debug').mockImplementation(() => {})
jest.spyOn(core, 'info').mockImplementation(() => {})
jest.spyOn(core, 'warning').mockImplementation(() => {})
})
it('Check Artifact Name for any invalid characters', () => {
const invalidNames = [
'my\\artifact',
'my/artifact',
'my"artifact',
'my:artifact',
'my<artifact',
'my>artifact',
'my|artifact',
'my*artifact',
'my?artifact',
''
]
for (const invalidName of invalidNames) {
expect(() => {
checkArtifactName(invalidName)
}).toThrow()
}
const validNames = [
'my-normal-artifact',
'myNormalArtifact',
'm¥ñðrmålÄr†ï£å¢†'
]
for (const validName of validNames) {
expect(() => {
checkArtifactName(validName)
}).not.toThrow()
}
})
it('Check Artifact File Path for any invalid characters', () => {
const invalidNames = [
'some/invalid"artifact/path',
'some/invalid:artifact/path',
'some/invalid<artifact/path',
'some/invalid>artifact/path',
'some/invalid|artifact/path',
'some/invalid*artifact/path',
'some/invalid?artifact/path',
'some/invalid\rartifact/path',
'some/invalid\nartifact/path',
'some/invalid\r\nartifact/path',
''
]
for (const invalidName of invalidNames) {
expect(() => {
checkArtifactFilePath(invalidName)
}).toThrow()
}
const validNames = [
'my/perfectly-normal/artifact-path',
'my/perfectly\\Normal/Artifact-path',
'm¥/ñðrmål/Är†ï£å¢†'
]
for (const validName of validNames) {
expect(() => {
checkArtifactFilePath(validName)
}).not.toThrow()
}
})
})
+5 -6
View File
@@ -3,7 +3,6 @@ import * as net from 'net'
import * as core from '@actions/core'
import * as configVariables from '../src/internal/config-variables'
import {retry} from '../src/internal/requestUtils'
import {IHttpClientResponse} from '@actions/http-client/interfaces'
import {HttpClientResponse} from '@actions/http-client'
jest.mock('../src/internal/config-variables')
@@ -42,7 +41,7 @@ async function testRetry(
async function handleResponse(
testResponseCode: number | undefined
): Promise<IHttpClientResponse> {
): Promise<HttpClientResponse> {
if (!testResponseCode) {
throw new Error(
'Test incorrectly set up. reverse.pop() was called too many times so not enough test response codes were supplied'
@@ -72,7 +71,7 @@ async function emptyMockReadBody(): Promise<string> {
async function setupSingleMockResponse(
statusCode: number
): Promise<IHttpClientResponse> {
): Promise<HttpClientResponse> {
const mockMessage = new http.IncomingMessage(new net.Socket())
const mockReadBody = emptyMockReadBody
mockMessage.statusCode = statusCode
@@ -107,8 +106,8 @@ test('retry fails after exhausting retries', async () => {
})
test('retry fails after non-retryable status code', async () => {
await testRetry([500, 200], {
responseCode: 500,
errorMessage: 'test failed: Artifact service responded with 500'
await testRetry([400, 200], {
responseCode: 400,
errorMessage: 'test failed: Artifact service responded with 400'
})
})
@@ -18,8 +18,10 @@ if [ ! -f "$path" ]; then
exit 1
fi
actualContent=$(cat $path)
if [ "$actualContent" != "$expectedContent" ];then
actualContent=$(cat "$path")
if [ "$expectedContent" == "_EMPTY_" ] && [ ! -s "$path" ]; then
exit 0
elif [ "$actualContent" != "$expectedContent" ]; then
echo "File contents are not correct, expected $expectedContent, received $actualContent"
exit 1
fi
@@ -0,0 +1,79 @@
import * as core from '@actions/core'
import * as tmp from 'tmp-promise'
import * as path from 'path'
import * as io from '../../io/src/io'
import {promises as fs} from 'fs'
import {createGZipFileOnDisk} from '../src/internal/upload-gzip'
const root = path.join(__dirname, '_temp', 'upload-gzip')
const tempGzipFilePath = path.join(root, 'file1.gzip')
const tempZipFilePath = path.join(root, 'file2.zip')
const tempTarlzFilePath = path.join(root, 'file3.tar.lz')
const tempGzFilePath = path.join(root, 'file4.tar.gz')
const tempBz2FilePath = path.join(root, 'file5.tar.bz2')
const temp7zFilePath = path.join(root, 'file6.7z')
const tempNormalFilePath = path.join(root, 'file6.txt')
jest.mock('../src/internal/config-variables')
beforeAll(async () => {
// mock all output so that there is less noise when running tests
jest.spyOn(console, 'log').mockImplementation(() => {})
jest.spyOn(core, 'debug').mockImplementation(() => {})
jest.spyOn(core, 'info').mockImplementation(() => {})
jest.spyOn(core, 'warning').mockImplementation(() => {})
jest.spyOn(core, 'error').mockImplementation(() => {})
// clear temp directory and create files that will be "uploaded"
await io.rmRF(root)
await fs.mkdir(path.join(root))
await fs.writeFile(tempGzipFilePath, 'a file with a .gzip file extension')
await fs.writeFile(tempZipFilePath, 'a file with a .zip file extension')
await fs.writeFile(tempTarlzFilePath, 'a file with a tar.lz file extension')
await fs.writeFile(tempGzFilePath, 'a file with a gz file file extension')
await fs.writeFile(tempBz2FilePath, 'a file with a .bz2 file extension')
await fs.writeFile(temp7zFilePath, 'a file with a .7z file extension')
await fs.writeFile(tempNormalFilePath, 'a file with a .txt file extension')
})
test('Number.MAX_SAFE_INTEGER is returned when an existing compressed file is used', async () => {
// create temporary file
const tempFile = await tmp.file()
expect(await createGZipFileOnDisk(tempGzipFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempZipFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempTarlzFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempGzFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempBz2FilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(temp7zFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(
await createGZipFileOnDisk(tempNormalFilePath, tempFile.path)
).not.toEqual(Number.MAX_SAFE_INTEGER)
})
test('gzip file on disk gets successfully created', async () => {
// create temporary file
const tempFile = await tmp.file()
const gzipFileSize = await createGZipFileOnDisk(
tempNormalFilePath,
tempFile.path
)
const fileStat = await fs.stat(tempNormalFilePath)
const totalFileSize = fileStat.size
// original file and gzip file should not be equal in size
expect(gzipFileSize).not.toEqual(totalFileSize)
})
@@ -2,6 +2,10 @@ import * as http from 'http'
import * as io from '../../io/src/io'
import * as net from 'net'
import * as path from 'path'
import {mocked} from 'ts-jest/utils'
import {exec, execSync} from 'child_process'
import {createGunzip} from 'zlib'
import {promisify} from 'util'
import {UploadHttpClient} from '../src/internal/upload-http-client'
import * as core from '@actions/core'
import {promises as fs} from 'fs'
@@ -174,6 +178,59 @@ describe('Upload Tests', () => {
expect(uploadResult.uploadSize).toEqual(expectedTotalSize)
})
function hasMkfifo(): boolean {
try {
// make sure we drain the stdout
return (
process.platform !== 'win32' &&
execSync('which mkfifo').toString().length > 0
)
} catch (e) {
return false
}
}
const withMkfifoIt = hasMkfifo() ? it : it.skip
withMkfifoIt(
'Upload Artifact with content from named pipe - Success',
async () => {
// create a named pipe 'pipe' with content 'hello pipe'
const content = Buffer.from('hello pipe')
const pipeFilePath = path.join(root, 'pipe')
await promisify(exec)('mkfifo pipe', {cwd: root})
// don't want to await here as that would block until read
fs.writeFile(pipeFilePath, content)
const artifactName = 'successful-artifact'
const uploadSpecification: UploadSpecification[] = [
{
absoluteFilePath: pipeFilePath,
uploadFilePath: `${artifactName}/pipe`
}
]
const uploadUrl = `${getRuntimeUrl()}_apis/resources/Containers/13`
const uploadHttpClient = new UploadHttpClient()
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
uploadUrl,
uploadSpecification
)
// accesses the ReadableStream that was passed into sendStream
// eslint-disable-next-line @typescript-eslint/unbound-method
const stream = mocked(HttpClient.prototype.sendStream).mock.calls[0][2]
expect(stream).not.toBeNull()
// decompresses the passed stream
const data: Buffer[] = []
for await (const chunk of stream.pipe(createGunzip())) {
data.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string))
}
const uploaded = Buffer.concat(data)
expect(uploadResult.failedItems.length).toEqual(0)
expect(uploaded).toEqual(content)
}
)
it('Upload Artifact - Failed Single File Upload', async () => {
const uploadSpecification: UploadSpecification[] = [
{
+19 -62
View File
@@ -10,6 +10,7 @@ import {
getInitialRetryIntervalInMilliseconds,
getRetryMultiplier
} from '../src/internal/config-variables'
import {Readable} from 'stream'
jest.mock('../src/internal/config-variables')
@@ -46,66 +47,6 @@ describe('Utils', () => {
}
})
it('Check Artifact Name for any invalid characters', () => {
const invalidNames = [
'my\\artifact',
'my/artifact',
'my"artifact',
'my:artifact',
'my<artifact',
'my>artifact',
'my|artifact',
'my*artifact',
'my?artifact',
''
]
for (const invalidName of invalidNames) {
expect(() => {
utils.checkArtifactName(invalidName)
}).toThrow()
}
const validNames = [
'my-normal-artifact',
'myNormalArtifact',
'm¥ñðrmålÄr†ï£å¢†'
]
for (const validName of validNames) {
expect(() => {
utils.checkArtifactName(validName)
}).not.toThrow()
}
})
it('Check Artifact File Path for any invalid characters', () => {
const invalidNames = [
'some/invalid"artifact/path',
'some/invalid:artifact/path',
'some/invalid<artifact/path',
'some/invalid>artifact/path',
'some/invalid|artifact/path',
'some/invalid*artifact/path',
'some/invalid?artifact/path',
''
]
for (const invalidName of invalidNames) {
expect(() => {
utils.checkArtifactFilePath(invalidName)
}).toThrow()
}
const validNames = [
'my/perfectly-normal/artifact-path',
'my/perfectly\\Normal/Artifact-path',
'm¥/ñðrmål/Är†ï£å¢†'
]
for (const validName of validNames) {
expect(() => {
utils.checkArtifactFilePath(validName)
}).not.toThrow()
}
})
it('Test negative artifact retention throws', () => {
expect(() => {
utils.getProperRetention(-1, undefined)
@@ -134,15 +75,20 @@ describe('Utils', () => {
const size = 24
const uncompressedLength = 100
const range = 'bytes 0-199/200'
const digest = {
crc64: 'bSzITYnW/P8=',
md5: 'Xiv1fT9AxLbfadrxk2y3ZvgyN0tPwCWafL/wbi9w8mk='
}
const headers = utils.getUploadHeaders(
contentType,
true,
true,
uncompressedLength,
size,
range
range,
digest
)
expect(Object.keys(headers).length).toEqual(8)
expect(Object.keys(headers).length).toEqual(10)
expect(headers['Accept']).toEqual(
`application/json;api-version=${utils.getApiVersion()}`
)
@@ -153,6 +99,8 @@ describe('Utils', () => {
expect(headers['x-tfs-filelength']).toEqual(uncompressedLength)
expect(headers['Content-Length']).toEqual(size)
expect(headers['Content-Range']).toEqual(range)
expect(headers['x-actions-results-crc64']).toEqual(digest.crc64)
expect(headers['x-actions-results-md5']).toEqual(digest.md5)
})
it('Test constructing upload headers with only required parameter', () => {
@@ -279,4 +227,13 @@ describe('Utils', () => {
const size2 = (await fs.promises.stat(emptyFile2)).size
expect(size2).toEqual(0)
})
it('Creates a digest from a readable stream', async () => {
const data = 'lorem ipsum'
const stream = Readable.from(data)
const digest = await utils.digestForStream(stream)
expect(digest.crc64).toBe('bSzITYnW/P8=')
expect(digest.md5).toBe('gKdR/eV3AoZAxBkADjPrpg==')
})
})
+239 -35
View File
@@ -1,31 +1,235 @@
{
"name": "@actions/artifact",
"version": "0.5.1",
"lockfileVersion": 1,
"version": "1.0.2",
"lockfileVersion": 2,
"requires": true,
"dependencies": {
"@actions/core": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
"packages": {
"": {
"name": "@actions/artifact",
"version": "1.0.1",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.2.6",
"@actions/http-client": "^2.0.1",
"tmp": "^0.2.1",
"tmp-promise": "^3.0.2"
},
"devDependencies": {
"@types/tmp": "^0.2.1",
"typescript": "^3.8.3"
}
},
"@actions/http-client": {
"node_modules/@actions/core": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.0.tgz",
"integrity": "sha512-XirM+Zo/PFlA+1h+i4bkfvagujta+LIM2AOSzPbt8JqXbbuxb1HTB+FqIyaKmue9yiCx/JIJY6pXsOl3+T8JGw==",
"dependencies": {
"@actions/http-client": "^1.0.11"
}
},
"node_modules/@actions/core/node_modules/@actions/http-client": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"requires": {
"dependencies": {
"tunnel": "0.0.6"
}
},
"node_modules/@actions/http-client": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
"dependencies": {
"tunnel": "^0.0.6"
}
},
"node_modules/@types/tmp": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.3.tgz",
"integrity": "sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==",
"dev": true
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"node_modules/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"dependencies": {
"rimraf": "^3.0.0"
},
"engines": {
"node": ">=8.17.0"
}
},
"node_modules/tmp-promise": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
"integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
"dependencies": {
"tmp": "^0.2.0"
}
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
"engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
}
},
"node_modules/typescript": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
},
"dependencies": {
"@actions/core": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.0.tgz",
"integrity": "sha512-XirM+Zo/PFlA+1h+i4bkfvagujta+LIM2AOSzPbt8JqXbbuxb1HTB+FqIyaKmue9yiCx/JIJY6pXsOl3+T8JGw==",
"requires": {
"@actions/http-client": "^1.0.11"
},
"dependencies": {
"@actions/http-client": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"requires": {
"tunnel": "0.0.6"
}
}
}
},
"@actions/http-client": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
"requires": {
"tunnel": "^0.0.6"
}
},
"@types/tmp": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.1.0.tgz",
"integrity": "sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA=="
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.3.tgz",
"integrity": "sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==",
"dev": true
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"brace-expansion": {
"version": "1.1.11",
@@ -47,9 +251,9 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -74,9 +278,9 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -95,27 +299,27 @@
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
}
},
"tmp": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz",
"integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"requires": {
"rimraf": "^2.6.3"
"rimraf": "^3.0.0"
}
},
"tmp-promise": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-2.0.2.tgz",
"integrity": "sha512-zl71nFWjPKW2KXs+73gEk8RmqvtAeXPxhWDkTUoa3MSMkjq3I+9OeknjF178MQoMYsdqL730hfzvNfEkePxq9Q==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
"integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
"requires": {
"tmp": "0.1.0"
"tmp": "^0.2.0"
}
},
"tunnel": {
@@ -124,9 +328,9 @@
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
},
"typescript": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
"dev": true
},
"wrappy": {
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/artifact",
"version": "0.5.1",
"version": "1.0.2",
"preview": true,
"description": "Actions artifact lib",
"keywords": [
@@ -38,12 +38,12 @@
},
"dependencies": {
"@actions/core": "^1.2.6",
"@actions/http-client": "^1.0.11",
"@types/tmp": "^0.1.0",
"tmp": "^0.1.0",
"tmp-promise": "^2.0.2"
"@actions/http-client": "^2.0.1",
"tmp": "^0.2.1",
"tmp-promise": "^3.0.2"
},
"devDependencies": {
"@types/tmp": "^0.2.1",
"typescript": "^3.8.3"
}
}
@@ -9,10 +9,10 @@ import {UploadOptions} from './upload-options'
import {DownloadOptions} from './download-options'
import {DownloadResponse} from './download-response'
import {
checkArtifactName,
createDirectoriesForArtifact,
createEmptyFilesForArtifact
} from './utils'
import {checkArtifactName} from './path-and-artifact-name-validation'
import {DownloadHttpClient} from './download-http-client'
import {getDownloadSpecification} from './download-specification'
import {getWorkSpaceDirectory} from './config-variables'
@@ -72,6 +72,10 @@ export class DefaultArtifactClient implements ArtifactClient {
rootDirectory: string,
options?: UploadOptions | undefined
): Promise<UploadResponse> {
core.info(
`Starting artifact upload
For more detailed logs during the artifact upload process, enable step-debugging: https://docs.github.com/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging#enabling-step-debug-logging`
)
checkArtifactName(name)
// Get specification for the files being uploaded
@@ -103,7 +107,11 @@ export class DefaultArtifactClient implements ArtifactClient {
'No URL provided by the Artifact Service to upload an artifact to'
)
}
core.debug(`Upload Resource URL: ${response.fileContainerResourceUrl}`)
core.info(
`Container for artifact "${name}" successfully created. Starting upload of file(s)`
)
// Upload each of the files that were found concurrently
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
@@ -114,10 +122,27 @@ export class DefaultArtifactClient implements ArtifactClient {
// Update the size of the artifact to indicate we are done uploading
// The uncompressed size is used for display when downloading a zip of the artifact from the UI
core.info(
`File upload process has finished. Finalizing the artifact upload`
)
await uploadHttpClient.patchArtifactSize(uploadResult.totalSize, name)
if (uploadResult.failedItems.length > 0) {
core.info(
`Upload finished. There were ${uploadResult.failedItems.length} items that failed to upload`
)
} else {
core.info(
`Artifact has been finalized. All files have been successfully uploaded!`
)
}
core.info(
`Finished uploading artifact ${name}. Reported size is ${uploadResult.uploadSize} bytes. There were ${uploadResult.failedItems.length} items that failed to upload`
`
The raw size of all the files that were specified for upload is ${uploadResult.totalSize} bytes
The size of all the files that were uploaded is ${uploadResult.uploadSize} bytes. This takes into account any gzip compression used to reduce the upload size, time and storage
Note: The size of downloaded zips can differ significantly from the reported size. For more information see: https://github.com/actions/upload-artifact#zipped-artifact-downloads \r\n`
)
uploadResponse.artifactItems = uploadSpecification.map(
@@ -215,6 +240,9 @@ export class DefaultArtifactClient implements ArtifactClient {
while (downloadedArtifacts < artifacts.count) {
const currentArtifactToDownload = artifacts.value[downloadedArtifacts]
downloadedArtifacts += 1
core.info(
`starting download of artifact ${currentArtifactToDownload.name} : ${downloadedArtifacts}/${artifacts.count}`
)
// Get container entries for the specific artifact
const items = await downloadHttpClient.getContainerItems(
@@ -29,8 +29,20 @@ export interface PatchArtifactSizeSuccessResponse {
}
export interface UploadResults {
/**
* The size in bytes of data that was transferred during the upload process to the actions backend service. This takes into account possible
* gzip compression to reduce the amount of data that needs to be transferred
*/
uploadSize: number
/**
* The raw size of the files that were specified for upload
*/
totalSize: number
/**
* An array of files that failed to upload
*/
failedItems: string[]
}
+317
View File
@@ -0,0 +1,317 @@
/**
* CRC64: cyclic redundancy check, 64-bits
*
* In order to validate that artifacts are not being corrupted over the wire, this redundancy check allows us to
* validate that there was no corruption during transmission. The implementation here is based on Go's hash/crc64 pkg,
* but without the slicing-by-8 optimization: https://cs.opensource.google/go/go/+/master:src/hash/crc64/crc64.go
*
* This implementation uses a pregenerated table based on 0x9A6C9329AC4BC9B5 as the polynomial, the same polynomial that
* is used for Azure Storage: https://github.com/Azure/azure-storage-net/blob/cbe605f9faa01bfc3003d75fc5a16b2eaccfe102/Lib/Common/Core/Util/Crc64.cs#L27
*/
// when transpile target is >= ES2020 (after dropping node 12) these can be changed to bigint literals - ts(2737)
const PREGEN_POLY_TABLE = [
BigInt('0x0000000000000000'),
BigInt('0x7F6EF0C830358979'),
BigInt('0xFEDDE190606B12F2'),
BigInt('0x81B31158505E9B8B'),
BigInt('0xC962E5739841B68F'),
BigInt('0xB60C15BBA8743FF6'),
BigInt('0x37BF04E3F82AA47D'),
BigInt('0x48D1F42BC81F2D04'),
BigInt('0xA61CECB46814FE75'),
BigInt('0xD9721C7C5821770C'),
BigInt('0x58C10D24087FEC87'),
BigInt('0x27AFFDEC384A65FE'),
BigInt('0x6F7E09C7F05548FA'),
BigInt('0x1010F90FC060C183'),
BigInt('0x91A3E857903E5A08'),
BigInt('0xEECD189FA00BD371'),
BigInt('0x78E0FF3B88BE6F81'),
BigInt('0x078E0FF3B88BE6F8'),
BigInt('0x863D1EABE8D57D73'),
BigInt('0xF953EE63D8E0F40A'),
BigInt('0xB1821A4810FFD90E'),
BigInt('0xCEECEA8020CA5077'),
BigInt('0x4F5FFBD87094CBFC'),
BigInt('0x30310B1040A14285'),
BigInt('0xDEFC138FE0AA91F4'),
BigInt('0xA192E347D09F188D'),
BigInt('0x2021F21F80C18306'),
BigInt('0x5F4F02D7B0F40A7F'),
BigInt('0x179EF6FC78EB277B'),
BigInt('0x68F0063448DEAE02'),
BigInt('0xE943176C18803589'),
BigInt('0x962DE7A428B5BCF0'),
BigInt('0xF1C1FE77117CDF02'),
BigInt('0x8EAF0EBF2149567B'),
BigInt('0x0F1C1FE77117CDF0'),
BigInt('0x7072EF2F41224489'),
BigInt('0x38A31B04893D698D'),
BigInt('0x47CDEBCCB908E0F4'),
BigInt('0xC67EFA94E9567B7F'),
BigInt('0xB9100A5CD963F206'),
BigInt('0x57DD12C379682177'),
BigInt('0x28B3E20B495DA80E'),
BigInt('0xA900F35319033385'),
BigInt('0xD66E039B2936BAFC'),
BigInt('0x9EBFF7B0E12997F8'),
BigInt('0xE1D10778D11C1E81'),
BigInt('0x606216208142850A'),
BigInt('0x1F0CE6E8B1770C73'),
BigInt('0x8921014C99C2B083'),
BigInt('0xF64FF184A9F739FA'),
BigInt('0x77FCE0DCF9A9A271'),
BigInt('0x08921014C99C2B08'),
BigInt('0x4043E43F0183060C'),
BigInt('0x3F2D14F731B68F75'),
BigInt('0xBE9E05AF61E814FE'),
BigInt('0xC1F0F56751DD9D87'),
BigInt('0x2F3DEDF8F1D64EF6'),
BigInt('0x50531D30C1E3C78F'),
BigInt('0xD1E00C6891BD5C04'),
BigInt('0xAE8EFCA0A188D57D'),
BigInt('0xE65F088B6997F879'),
BigInt('0x9931F84359A27100'),
BigInt('0x1882E91B09FCEA8B'),
BigInt('0x67EC19D339C963F2'),
BigInt('0xD75ADABD7A6E2D6F'),
BigInt('0xA8342A754A5BA416'),
BigInt('0x29873B2D1A053F9D'),
BigInt('0x56E9CBE52A30B6E4'),
BigInt('0x1E383FCEE22F9BE0'),
BigInt('0x6156CF06D21A1299'),
BigInt('0xE0E5DE5E82448912'),
BigInt('0x9F8B2E96B271006B'),
BigInt('0x71463609127AD31A'),
BigInt('0x0E28C6C1224F5A63'),
BigInt('0x8F9BD7997211C1E8'),
BigInt('0xF0F5275142244891'),
BigInt('0xB824D37A8A3B6595'),
BigInt('0xC74A23B2BA0EECEC'),
BigInt('0x46F932EAEA507767'),
BigInt('0x3997C222DA65FE1E'),
BigInt('0xAFBA2586F2D042EE'),
BigInt('0xD0D4D54EC2E5CB97'),
BigInt('0x5167C41692BB501C'),
BigInt('0x2E0934DEA28ED965'),
BigInt('0x66D8C0F56A91F461'),
BigInt('0x19B6303D5AA47D18'),
BigInt('0x980521650AFAE693'),
BigInt('0xE76BD1AD3ACF6FEA'),
BigInt('0x09A6C9329AC4BC9B'),
BigInt('0x76C839FAAAF135E2'),
BigInt('0xF77B28A2FAAFAE69'),
BigInt('0x8815D86ACA9A2710'),
BigInt('0xC0C42C4102850A14'),
BigInt('0xBFAADC8932B0836D'),
BigInt('0x3E19CDD162EE18E6'),
BigInt('0x41773D1952DB919F'),
BigInt('0x269B24CA6B12F26D'),
BigInt('0x59F5D4025B277B14'),
BigInt('0xD846C55A0B79E09F'),
BigInt('0xA72835923B4C69E6'),
BigInt('0xEFF9C1B9F35344E2'),
BigInt('0x90973171C366CD9B'),
BigInt('0x1124202993385610'),
BigInt('0x6E4AD0E1A30DDF69'),
BigInt('0x8087C87E03060C18'),
BigInt('0xFFE938B633338561'),
BigInt('0x7E5A29EE636D1EEA'),
BigInt('0x0134D92653589793'),
BigInt('0x49E52D0D9B47BA97'),
BigInt('0x368BDDC5AB7233EE'),
BigInt('0xB738CC9DFB2CA865'),
BigInt('0xC8563C55CB19211C'),
BigInt('0x5E7BDBF1E3AC9DEC'),
BigInt('0x21152B39D3991495'),
BigInt('0xA0A63A6183C78F1E'),
BigInt('0xDFC8CAA9B3F20667'),
BigInt('0x97193E827BED2B63'),
BigInt('0xE877CE4A4BD8A21A'),
BigInt('0x69C4DF121B863991'),
BigInt('0x16AA2FDA2BB3B0E8'),
BigInt('0xF86737458BB86399'),
BigInt('0x8709C78DBB8DEAE0'),
BigInt('0x06BAD6D5EBD3716B'),
BigInt('0x79D4261DDBE6F812'),
BigInt('0x3105D23613F9D516'),
BigInt('0x4E6B22FE23CC5C6F'),
BigInt('0xCFD833A67392C7E4'),
BigInt('0xB0B6C36E43A74E9D'),
BigInt('0x9A6C9329AC4BC9B5'),
BigInt('0xE50263E19C7E40CC'),
BigInt('0x64B172B9CC20DB47'),
BigInt('0x1BDF8271FC15523E'),
BigInt('0x530E765A340A7F3A'),
BigInt('0x2C608692043FF643'),
BigInt('0xADD397CA54616DC8'),
BigInt('0xD2BD67026454E4B1'),
BigInt('0x3C707F9DC45F37C0'),
BigInt('0x431E8F55F46ABEB9'),
BigInt('0xC2AD9E0DA4342532'),
BigInt('0xBDC36EC59401AC4B'),
BigInt('0xF5129AEE5C1E814F'),
BigInt('0x8A7C6A266C2B0836'),
BigInt('0x0BCF7B7E3C7593BD'),
BigInt('0x74A18BB60C401AC4'),
BigInt('0xE28C6C1224F5A634'),
BigInt('0x9DE29CDA14C02F4D'),
BigInt('0x1C518D82449EB4C6'),
BigInt('0x633F7D4A74AB3DBF'),
BigInt('0x2BEE8961BCB410BB'),
BigInt('0x548079A98C8199C2'),
BigInt('0xD53368F1DCDF0249'),
BigInt('0xAA5D9839ECEA8B30'),
BigInt('0x449080A64CE15841'),
BigInt('0x3BFE706E7CD4D138'),
BigInt('0xBA4D61362C8A4AB3'),
BigInt('0xC52391FE1CBFC3CA'),
BigInt('0x8DF265D5D4A0EECE'),
BigInt('0xF29C951DE49567B7'),
BigInt('0x732F8445B4CBFC3C'),
BigInt('0x0C41748D84FE7545'),
BigInt('0x6BAD6D5EBD3716B7'),
BigInt('0x14C39D968D029FCE'),
BigInt('0x95708CCEDD5C0445'),
BigInt('0xEA1E7C06ED698D3C'),
BigInt('0xA2CF882D2576A038'),
BigInt('0xDDA178E515432941'),
BigInt('0x5C1269BD451DB2CA'),
BigInt('0x237C997575283BB3'),
BigInt('0xCDB181EAD523E8C2'),
BigInt('0xB2DF7122E51661BB'),
BigInt('0x336C607AB548FA30'),
BigInt('0x4C0290B2857D7349'),
BigInt('0x04D364994D625E4D'),
BigInt('0x7BBD94517D57D734'),
BigInt('0xFA0E85092D094CBF'),
BigInt('0x856075C11D3CC5C6'),
BigInt('0x134D926535897936'),
BigInt('0x6C2362AD05BCF04F'),
BigInt('0xED9073F555E26BC4'),
BigInt('0x92FE833D65D7E2BD'),
BigInt('0xDA2F7716ADC8CFB9'),
BigInt('0xA54187DE9DFD46C0'),
BigInt('0x24F29686CDA3DD4B'),
BigInt('0x5B9C664EFD965432'),
BigInt('0xB5517ED15D9D8743'),
BigInt('0xCA3F8E196DA80E3A'),
BigInt('0x4B8C9F413DF695B1'),
BigInt('0x34E26F890DC31CC8'),
BigInt('0x7C339BA2C5DC31CC'),
BigInt('0x035D6B6AF5E9B8B5'),
BigInt('0x82EE7A32A5B7233E'),
BigInt('0xFD808AFA9582AA47'),
BigInt('0x4D364994D625E4DA'),
BigInt('0x3258B95CE6106DA3'),
BigInt('0xB3EBA804B64EF628'),
BigInt('0xCC8558CC867B7F51'),
BigInt('0x8454ACE74E645255'),
BigInt('0xFB3A5C2F7E51DB2C'),
BigInt('0x7A894D772E0F40A7'),
BigInt('0x05E7BDBF1E3AC9DE'),
BigInt('0xEB2AA520BE311AAF'),
BigInt('0x944455E88E0493D6'),
BigInt('0x15F744B0DE5A085D'),
BigInt('0x6A99B478EE6F8124'),
BigInt('0x224840532670AC20'),
BigInt('0x5D26B09B16452559'),
BigInt('0xDC95A1C3461BBED2'),
BigInt('0xA3FB510B762E37AB'),
BigInt('0x35D6B6AF5E9B8B5B'),
BigInt('0x4AB846676EAE0222'),
BigInt('0xCB0B573F3EF099A9'),
BigInt('0xB465A7F70EC510D0'),
BigInt('0xFCB453DCC6DA3DD4'),
BigInt('0x83DAA314F6EFB4AD'),
BigInt('0x0269B24CA6B12F26'),
BigInt('0x7D0742849684A65F'),
BigInt('0x93CA5A1B368F752E'),
BigInt('0xECA4AAD306BAFC57'),
BigInt('0x6D17BB8B56E467DC'),
BigInt('0x12794B4366D1EEA5'),
BigInt('0x5AA8BF68AECEC3A1'),
BigInt('0x25C64FA09EFB4AD8'),
BigInt('0xA4755EF8CEA5D153'),
BigInt('0xDB1BAE30FE90582A'),
BigInt('0xBCF7B7E3C7593BD8'),
BigInt('0xC399472BF76CB2A1'),
BigInt('0x422A5673A732292A'),
BigInt('0x3D44A6BB9707A053'),
BigInt('0x759552905F188D57'),
BigInt('0x0AFBA2586F2D042E'),
BigInt('0x8B48B3003F739FA5'),
BigInt('0xF42643C80F4616DC'),
BigInt('0x1AEB5B57AF4DC5AD'),
BigInt('0x6585AB9F9F784CD4'),
BigInt('0xE436BAC7CF26D75F'),
BigInt('0x9B584A0FFF135E26'),
BigInt('0xD389BE24370C7322'),
BigInt('0xACE74EEC0739FA5B'),
BigInt('0x2D545FB4576761D0'),
BigInt('0x523AAF7C6752E8A9'),
BigInt('0xC41748D84FE75459'),
BigInt('0xBB79B8107FD2DD20'),
BigInt('0x3ACAA9482F8C46AB'),
BigInt('0x45A459801FB9CFD2'),
BigInt('0x0D75ADABD7A6E2D6'),
BigInt('0x721B5D63E7936BAF'),
BigInt('0xF3A84C3BB7CDF024'),
BigInt('0x8CC6BCF387F8795D'),
BigInt('0x620BA46C27F3AA2C'),
BigInt('0x1D6554A417C62355'),
BigInt('0x9CD645FC4798B8DE'),
BigInt('0xE3B8B53477AD31A7'),
BigInt('0xAB69411FBFB21CA3'),
BigInt('0xD407B1D78F8795DA'),
BigInt('0x55B4A08FDFD90E51'),
BigInt('0x2ADA5047EFEC8728')
]
export type CRC64DigestEncoding = 'hex' | 'base64' | 'buffer'
class CRC64 {
private _crc: bigint
constructor() {
this._crc = BigInt(0)
}
update(data: Buffer | string): void {
const buffer = typeof data === 'string' ? Buffer.from(data) : data
let crc = CRC64.flip64Bits(this._crc)
for (const dataByte of buffer) {
const crcByte = Number(crc & BigInt(0xff))
crc = PREGEN_POLY_TABLE[crcByte ^ dataByte] ^ (crc >> BigInt(8))
}
this._crc = CRC64.flip64Bits(crc)
}
digest(encoding?: CRC64DigestEncoding): string | Buffer {
switch (encoding) {
case 'hex':
return this._crc.toString(16).toUpperCase()
case 'base64':
return this.toBuffer().toString('base64')
default:
return this.toBuffer()
}
}
private toBuffer(): Buffer {
return Buffer.from(
[0, 8, 16, 24, 32, 40, 48, 56].map(s =>
Number((this._crc >> BigInt(s)) & BigInt(0xff))
)
)
}
static flip64Bits(n: bigint): bigint {
return (BigInt(1) << BigInt(64)) - BigInt(1) - n
}
}
export default CRC64
@@ -18,7 +18,7 @@ import {URL} from 'url'
import {StatusReporter} from './status-reporter'
import {performance} from 'perf_hooks'
import {ListArtifactsResponse, QueryArtifactResponse} from './contracts'
import {IHttpClientResponse} from '@actions/http-client/interfaces'
import {HttpClientResponse} from '@actions/http-client'
import {HttpManager} from './http-manager'
import {DownloadItem} from './download-specification'
import {getDownloadFileConcurrency, getRetryLimit} from './config-variables'
@@ -152,7 +152,7 @@ export class DownloadHttpClient {
const headers = getDownloadHeaders('application/json', true, true)
// a single GET request is used to download a file
const makeDownloadRequest = async (): Promise<IHttpClientResponse> => {
const makeDownloadRequest = async (): Promise<HttpClientResponse> => {
const client = this.downloadHttpManager.getClient(httpClientIndex)
return await client.get(artifactLocation, headers)
}
@@ -225,12 +225,9 @@ export class DownloadHttpClient {
// keep trying to download a file until a retry limit has been reached
while (retryCount <= retryLimit) {
let response: IHttpClientResponse
let response: HttpClientResponse
try {
response = await makeDownloadRequest()
if (core.isDebug()) {
displayHttpDiagnostics(response)
}
} catch (error) {
// if an error is caught, it is usually indicative of a timeout so retry the download
core.info('An error occurred while attempting to download a file')
@@ -298,7 +295,7 @@ export class DownloadHttpClient {
* @param isGzip a boolean denoting if the content is compressed using gzip and if we need to decode it
*/
async pipeResponseToFile(
response: IHttpClientResponse,
response: HttpClientResponse,
destinationStream: fs.WriteStream,
isGzip: boolean
): Promise<void> {
@@ -1,4 +1,4 @@
import {HttpClient} from '@actions/http-client/index'
import {HttpClient} from '@actions/http-client'
import {createHttpClient} from './utils'
/**
@@ -0,0 +1,82 @@
import {info} from '@actions/core'
/**
* Invalid characters that cannot be in the artifact name or an uploaded file. Will be rejected
* from the server if attempted to be sent over. These characters are not allowed due to limitations with certain
* file systems such as NTFS. To maintain platform-agnostic behavior, all characters that are not supported by an
* individual filesystem/platform will not be supported on all fileSystems/platforms
*
* FilePaths can include characters such as \ and / which are not permitted in the artifact name alone
*/
const invalidArtifactFilePathCharacters = new Map<string, string>([
['"', ' Double quote "'],
[':', ' Colon :'],
['<', ' Less than <'],
['>', ' Greater than >'],
['|', ' Vertical bar |'],
['*', ' Asterisk *'],
['?', ' Question mark ?'],
['\r', ' Carriage return \\r'],
['\n', ' Line feed \\n']
])
const invalidArtifactNameCharacters = new Map<string, string>([
...invalidArtifactFilePathCharacters,
['\\', ' Backslash \\'],
['/', ' Forward slash /']
])
/**
* Scans the name of the artifact to make sure there are no illegal characters
*/
export function checkArtifactName(name: string): void {
if (!name) {
throw new Error(`Artifact name: ${name}, is incorrectly provided`)
}
for (const [
invalidCharacterKey,
errorMessageForCharacter
] of invalidArtifactNameCharacters) {
if (name.includes(invalidCharacterKey)) {
throw new Error(
`Artifact name is not valid: ${name}. Contains the following character: ${errorMessageForCharacter}
Invalid characters include: ${Array.from(
invalidArtifactNameCharacters.values()
).toString()}
These characters are not allowed in the artifact name due to limitations with certain file systems such as NTFS. To maintain file system agnostic behavior, these characters are intentionally not allowed to prevent potential problems with downloads on different file systems.`
)
}
}
info(`Artifact name is valid!`)
}
/**
* Scans the name of the filePath used to make sure there are no illegal characters
*/
export function checkArtifactFilePath(path: string): void {
if (!path) {
throw new Error(`Artifact path: ${path}, is incorrectly provided`)
}
for (const [
invalidCharacterKey,
errorMessageForCharacter
] of invalidArtifactFilePathCharacters) {
if (path.includes(invalidCharacterKey)) {
throw new Error(
`Artifact path is not valid: ${path}. Contains the following character: ${errorMessageForCharacter}
Invalid characters include: ${Array.from(
invalidArtifactFilePathCharacters.values()
).toString()}
The following characters are not allowed in files that are uploaded due to limitations with certain file systems such as NTFS. To maintain file system agnostic behavior, these characters are intentionally not allowed to prevent potential problems with downloads on different file systems.
`
)
}
}
}
@@ -1,4 +1,4 @@
import {IHttpClientResponse} from '@actions/http-client/interfaces'
import {HttpClientResponse} from '@actions/http-client'
import {
isRetryableStatusCode,
isSuccessStatusCode,
@@ -11,11 +11,11 @@ import {getRetryLimit} from './config-variables'
export async function retry(
name: string,
operation: () => Promise<IHttpClientResponse>,
operation: () => Promise<HttpClientResponse>,
customErrorMessages: Map<number, string>,
maxAttempts: number
): Promise<IHttpClientResponse> {
let response: IHttpClientResponse | undefined = undefined
): Promise<HttpClientResponse> {
let response: HttpClientResponse | undefined = undefined
let statusCode: number | undefined = undefined
let isRetryable = false
let errorMessage = ''
@@ -69,11 +69,11 @@ export async function retry(
throw Error(`${name} failed: ${errorMessage}`)
}
export async function retryHttpClientRequest<T>(
export async function retryHttpClientRequest(
name: string,
method: () => Promise<IHttpClientResponse>,
method: () => Promise<HttpClientResponse>,
customErrorMessages: Map<number, string> = new Map(),
maxAttempts = getRetryLimit()
): Promise<IHttpClientResponse> {
): Promise<HttpClientResponse> {
return await retry(name, method, customErrorMessages, maxAttempts)
}
@@ -14,16 +14,15 @@ export class StatusReporter {
private displayFrequencyInMilliseconds: number
private largeFiles = new Map<string, string>()
private totalFileStatus: NodeJS.Timeout | undefined
private largeFileStatus: NodeJS.Timeout | undefined
constructor(displayFrequencyInMilliseconds: number) {
this.totalFileStatus = undefined
this.largeFileStatus = undefined
this.displayFrequencyInMilliseconds = displayFrequencyInMilliseconds
}
setTotalNumberOfFilesToProcess(fileTotal: number): void {
this.totalNumberOfFilesToProcess = fileTotal
this.processedCount = 0
}
start(): void {
@@ -43,42 +42,29 @@ export class StatusReporter {
)}%)`
)
}, this.displayFrequencyInMilliseconds)
// displays extra information about any large files that take a significant amount of time to upload or download every 1 second
this.largeFileStatus = setInterval(() => {
for (const value of Array.from(this.largeFiles.values())) {
info(value)
}
// delete all entries in the map after displaying the information so it will not be displayed again unless explicitly added
this.largeFiles.clear()
}, 1000)
}
// if there is a large file that is being uploaded in chunks, this is used to display extra information about the status of the upload
updateLargeFileStatus(
fileName: string,
numerator: number,
denominator: number
chunkStartIndex: number,
chunkEndIndex: number,
totalUploadFileSize: number
): void {
// display 1 decimal place without any rounding
const percentage = this.formatPercentage(numerator, denominator)
const displayInformation = `Uploading ${fileName} (${percentage.slice(
0,
percentage.indexOf('.') + 2
)}%)`
// any previously added display information should be overwritten for the specific large file because a map is being used
this.largeFiles.set(fileName, displayInformation)
const percentage = this.formatPercentage(chunkEndIndex, totalUploadFileSize)
info(
`Uploaded ${fileName} (${percentage.slice(
0,
percentage.indexOf('.') + 2
)}%) bytes ${chunkStartIndex}:${chunkEndIndex}`
)
}
stop(): void {
if (this.totalFileStatus) {
clearInterval(this.totalFileStatus)
}
if (this.largeFileStatus) {
clearInterval(this.largeFileStatus)
}
}
incrementProcessedCount(): void {
@@ -3,6 +3,20 @@ import * as zlib from 'zlib'
import {promisify} from 'util'
const stat = promisify(fs.stat)
/**
* GZipping certain files that are already compressed will likely not yield further size reductions. Creating large temporary gzip
* files then will just waste a lot of time before ultimately being discarded (especially for very large files).
* If any of these types of files are encountered then on-disk gzip creation will be skipped and the original file will be uploaded as-is
*/
const gzipExemptFileExtensions = [
'.gzip',
'.zip',
'.tar.lz',
'.tar.gz',
'.tar.bz2',
'.7z'
]
/**
* Creates a Gzip compressed file of an original file at the provided temporary filepath location
* @param {string} originalFilePath filepath of whatever will be compressed. The original file will be unmodified
@@ -13,6 +27,13 @@ export async function createGZipFileOnDisk(
originalFilePath: string,
tempFilePath: string
): Promise<number> {
for (const gzipExemptExtension of gzipExemptFileExtensions) {
if (originalFilePath.endsWith(gzipExemptExtension)) {
// return a really large number so that the original file gets uploaded
return Number.MAX_SAFE_INTEGER
}
}
return new Promise((resolve, reject) => {
const inputStream = fs.createReadStream(originalFilePath)
const gzip = zlib.createGzip()
@@ -9,6 +9,7 @@ import {
UploadResults
} from './contracts'
import {
digestForStream,
getArtifactUrl,
getContentRange,
getUploadHeaders,
@@ -31,8 +32,7 @@ import {promisify} from 'util'
import {URL} from 'url'
import {performance} from 'perf_hooks'
import {StatusReporter} from './status-reporter'
import {HttpCodes} from '@actions/http-client'
import {IHttpClientResponse} from '@actions/http-client/interfaces'
import {HttpCodes, HttpClientResponse} from '@actions/http-client'
import {HttpManager} from './http-manager'
import {UploadSpecification} from './upload-specification'
import {UploadOptions} from './upload-options'
@@ -219,29 +219,41 @@ export class UploadHttpClient {
httpClientIndex: number,
parameters: UploadFileParameters
): Promise<UploadFileResult> {
const totalFileSize: number = (await stat(parameters.file)).size
const fileStat: fs.Stats = await stat(parameters.file)
const totalFileSize = fileStat.size
const isFIFO = fileStat.isFIFO()
let offset = 0
let isUploadSuccessful = true
let failedChunkSizes = 0
let uploadFileSize = 0
let isGzip = true
// the file that is being uploaded is less than 64k in size, to increase throughput and to minimize disk I/O
// the file that is being uploaded is less than 64k in size to increase throughput and to minimize disk I/O
// for creating a new GZip file, an in-memory buffer is used for compression
if (totalFileSize < 65536) {
// with named pipes the file size is reported as zero in that case don't read the file in memory
if (!isFIFO && totalFileSize < 65536) {
core.debug(
`${parameters.file} is less than 64k in size. Creating a gzip file in-memory to potentially reduce the upload size`
)
const buffer = await createGZipFileInBuffer(parameters.file)
//An open stream is needed in the event of a failure and we need to retry. If a NodeJS.ReadableStream is directly passed in,
// An open stream is needed in the event of a failure and we need to retry. If a NodeJS.ReadableStream is directly passed in,
// it will not properly get reset to the start of the stream if a chunk upload needs to be retried
let openUploadStream: () => NodeJS.ReadableStream
if (totalFileSize < buffer.byteLength) {
// compression did not help with reducing the size, use a readable stream from the original file for upload
core.debug(
`The gzip file created for ${parameters.file} did not help with reducing the size of the file. The original file will be uploaded as-is`
)
openUploadStream = () => fs.createReadStream(parameters.file)
isGzip = false
uploadFileSize = totalFileSize
} else {
// create a readable stream using a PassThrough stream that is both readable and writable
core.debug(
`A gzip file created for ${parameters.file} helped with reducing the size of the original file. The file will be uploaded using gzip.`
)
openUploadStream = () => {
const passThrough = new stream.PassThrough()
passThrough.end(buffer)
@@ -277,6 +289,9 @@ export class UploadHttpClient {
// the file that is being uploaded is greater than 64k in size, a temporary file gets created on disk using the
// npm tmp-promise package and this file gets used to create a GZipped file
const tempFile = await tmp.file()
core.debug(
`${parameters.file} is greater than 64k in size. Creating a gzip file on-disk ${tempFile.path} to potentially reduce the upload size`
)
// create a GZip file of the original file being uploaded, the original file should not be modified in any way
uploadFileSize = await createGZipFileOnDisk(
@@ -287,10 +302,18 @@ export class UploadHttpClient {
let uploadFilePath = tempFile.path
// compression did not help with size reduction, use the original file for upload and delete the temp GZip file
if (totalFileSize < uploadFileSize) {
// for named pipes totalFileSize is zero, this assumes compression did help
if (!isFIFO && totalFileSize < uploadFileSize) {
core.debug(
`The gzip file created for ${parameters.file} did not help with reducing the size of the file. The original file will be uploaded as-is`
)
uploadFileSize = totalFileSize
uploadFilePath = parameters.file
isGzip = false
} else {
core.debug(
`The gzip file created for ${parameters.file} is smaller than the original file. The file will be uploaded using gzip.`
)
}
let abortFileUpload = false
@@ -301,17 +324,8 @@ export class UploadHttpClient {
parameters.maxChunkSize
)
// if an individual file is greater than 100MB (1024*1024*100) in size, display extra information about the upload status
if (uploadFileSize > 104857600) {
this.statusReporter.updateLargeFileStatus(
parameters.file,
offset,
uploadFileSize
)
}
const start = offset
const end = offset + chunkSize - 1
const startChunkIndex = offset
const endChunkIndex = offset + chunkSize - 1
offset += parameters.maxChunkSize
if (abortFileUpload) {
@@ -325,12 +339,12 @@ export class UploadHttpClient {
parameters.resourceUrl,
() =>
fs.createReadStream(uploadFilePath, {
start,
end,
start: startChunkIndex,
end: endChunkIndex,
autoClose: false
}),
start,
end,
startChunkIndex,
endChunkIndex,
uploadFileSize,
isGzip,
totalFileSize
@@ -343,11 +357,22 @@ export class UploadHttpClient {
failedChunkSizes += chunkSize
core.warning(`Aborting upload for ${parameters.file} due to failure`)
abortFileUpload = true
} else {
// if an individual file is greater than 8MB (1024*1024*8) in size, display extra information about the upload status
if (uploadFileSize > 8388608) {
this.statusReporter.updateLargeFileStatus(
parameters.file,
startChunkIndex,
endChunkIndex,
uploadFileSize
)
}
}
}
// Delete the temporary file that was created as part of the upload. If the temp file does not get manually deleted by
// calling cleanup, it gets removed when the node process exits. For more info see: https://www.npmjs.com/package/tmp-promise#about
core.debug(`deleting temporary gzip file ${tempFile.path}`)
await tempFile.cleanup()
return {
@@ -381,6 +406,9 @@ export class UploadHttpClient {
isGzip: boolean,
totalFileSize: number
): Promise<boolean> {
// open a new stream and read it to compute the digest
const digest = await digestForStream(openStream())
// prepare all the necessary headers before making any http call
const headers = getUploadHeaders(
'application/octet-stream',
@@ -388,10 +416,11 @@ export class UploadHttpClient {
isGzip,
totalFileSize,
end - start + 1,
getContentRange(start, end, uploadFileSize)
getContentRange(start, end, uploadFileSize),
digest
)
const uploadChunkRequest = async (): Promise<IHttpClientResponse> => {
const uploadChunkRequest = async (): Promise<HttpClientResponse> => {
const client = this.uploadHttpManager.getClient(httpClientIndex)
return await client.sendStream('PUT', resourceUrl, openStream(), headers)
}
@@ -402,7 +431,7 @@ export class UploadHttpClient {
// Increments the current retry count and then checks if the retry limit has been reached
// If there have been too many retries, fail so the download stops
const incrementAndCheckRetryLimit = (
response?: IHttpClientResponse
response?: HttpClientResponse
): boolean => {
retryCount++
if (retryCount > retryLimit) {
@@ -439,7 +468,7 @@ export class UploadHttpClient {
// allow for failed chunks to be retried multiple times
while (retryCount <= retryLimit) {
let response: IHttpClientResponse
let response: HttpClientResponse
try {
response = await uploadChunkRequest()
@@ -1,7 +1,7 @@
import * as fs from 'fs'
import {debug} from '@actions/core'
import {join, normalize, resolve} from 'path'
import {checkArtifactName, checkArtifactFilePath} from './utils'
import {checkArtifactFilePath} from './path-and-artifact-name-validation'
export interface UploadSpecification {
absoluteFilePath: string
@@ -19,8 +19,7 @@ export function getUploadSpecification(
rootDirectory: string,
artifactFiles: string[]
): UploadSpecification[] {
checkArtifactName(artifactName)
// artifact name was checked earlier on, no need to check again
const specifications: UploadSpecification[] = []
if (!fs.existsSync(rootDirectory)) {
+46 -62
View File
@@ -1,9 +1,9 @@
import {debug, info, warning} from '@actions/core'
import crypto from 'crypto'
import {promises as fs} from 'fs'
import {HttpCodes, HttpClient} from '@actions/http-client'
import {BearerCredentialHandler} from '@actions/http-client/auth'
import {IHeaders, IHttpClientResponse} from '@actions/http-client/interfaces'
import {IncomingHttpHeaders} from 'http'
import {IncomingHttpHeaders, OutgoingHttpHeaders} from 'http'
import {debug, info, warning} from '@actions/core'
import {HttpCodes, HttpClient, HttpClientResponse} from '@actions/http-client'
import {BearerCredentialHandler} from '@actions/http-client/lib/auth'
import {
getRuntimeToken,
getRuntimeUrl,
@@ -11,6 +11,7 @@ import {
getRetryMultiplier,
getInitialRetryIntervalInMilliseconds
} from './config-variables'
import CRC64 from './crc64'
/**
* Returns a retry time in milliseconds that exponentially gets larger
@@ -30,7 +31,7 @@ export function getExponentialRetryTimeInMilliseconds(
const maxTime = minTime * getRetryMultiplier()
// returns a random number between the minTime (inclusive) and the maxTime (exclusive)
return Math.random() * (maxTime - minTime) + minTime
return Math.trunc(Math.random() * (maxTime - minTime) + minTime)
}
/**
@@ -72,8 +73,9 @@ export function isRetryableStatusCode(statusCode: number | undefined): boolean {
const retryableStatusCodes = [
HttpCodes.BadGateway,
HttpCodes.ServiceUnavailable,
HttpCodes.GatewayTimeout,
HttpCodes.InternalServerError,
HttpCodes.ServiceUnavailable,
HttpCodes.TooManyRequests,
413 // Payload Too Large
]
@@ -138,8 +140,8 @@ export function getDownloadHeaders(
contentType: string,
isKeepAlive?: boolean,
acceptGzip?: boolean
): IHeaders {
const requestOptions: IHeaders = {}
): OutgoingHttpHeaders {
const requestOptions: OutgoingHttpHeaders = {}
if (contentType) {
requestOptions['Content-Type'] = contentType
@@ -179,9 +181,10 @@ export function getUploadHeaders(
isGzip?: boolean,
uncompressedLength?: number,
contentLength?: number,
contentRange?: string
): IHeaders {
const requestOptions: IHeaders = {}
contentRange?: string,
digest?: StreamDigest
): OutgoingHttpHeaders {
const requestOptions: OutgoingHttpHeaders = {}
requestOptions['Accept'] = `application/json;api-version=${getApiVersion()}`
if (contentType) {
requestOptions['Content-Type'] = contentType
@@ -201,6 +204,10 @@ export function getUploadHeaders(
if (contentRange) {
requestOptions['Content-Range'] = contentRange
}
if (digest) {
requestOptions['x-actions-results-crc64'] = digest.crc64
requestOptions['x-actions-results-md5'] = digest.md5
}
return requestOptions
}
@@ -226,7 +233,7 @@ export function getArtifactUrl(): string {
* Certain information such as the TLSSocket and the Readable state are not really useful for diagnostic purposes so they can be avoided.
* Other information such as the headers, the response code and message might be useful, so this is displayed.
*/
export function displayHttpDiagnostics(response: IHttpClientResponse): void {
export function displayHttpDiagnostics(response: HttpClientResponse): void {
info(
`##### Begin Diagnostic HTTP information #####
Status Code: ${response.message.statusCode}
@@ -236,55 +243,6 @@ Header Information: ${JSON.stringify(response.message.headers, undefined, 2)}
)
}
/**
* Invalid characters that cannot be in the artifact name or an uploaded file. Will be rejected
* from the server if attempted to be sent over. These characters are not allowed due to limitations with certain
* file systems such as NTFS. To maintain platform-agnostic behavior, all characters that are not supported by an
* individual filesystem/platform will not be supported on all fileSystems/platforms
*
* FilePaths can include characters such as \ and / which are not permitted in the artifact name alone
*/
const invalidArtifactFilePathCharacters = ['"', ':', '<', '>', '|', '*', '?']
const invalidArtifactNameCharacters = [
...invalidArtifactFilePathCharacters,
'\\',
'/'
]
/**
* Scans the name of the artifact to make sure there are no illegal characters
*/
export function checkArtifactName(name: string): void {
if (!name) {
throw new Error(`Artifact name: ${name}, is incorrectly provided`)
}
for (const invalidChar of invalidArtifactNameCharacters) {
if (name.includes(invalidChar)) {
throw new Error(
`Artifact name is not valid: ${name}. Contains character: "${invalidChar}". Invalid artifact name characters include: ${invalidArtifactNameCharacters.toString()}.`
)
}
}
}
/**
* Scans the name of the filePath used to make sure there are no illegal characters
*/
export function checkArtifactFilePath(path: string): void {
if (!path) {
throw new Error(`Artifact path: ${path}, is incorrectly provided`)
}
for (const invalidChar of invalidArtifactFilePathCharacters) {
if (path.includes(invalidChar)) {
throw new Error(
`Artifact path is not valid: ${path}. Contains character: "${invalidChar}". Invalid characters include: ${invalidArtifactFilePathCharacters.toString()}.`
)
}
}
}
export async function createDirectoriesForArtifact(
directories: string[]
): Promise<void> {
@@ -339,3 +297,29 @@ export function getProperRetention(
export async function sleep(milliseconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
export interface StreamDigest {
crc64: string
md5: string
}
export async function digestForStream(
stream: NodeJS.ReadableStream
): Promise<StreamDigest> {
return new Promise((resolve, reject) => {
const crc64 = new CRC64()
const md5 = crypto.createHash('md5')
stream
.on('data', data => {
crc64.update(data)
md5.update(data)
})
.on('end', () =>
resolve({
crc64: crc64.digest('base64') as string,
md5: md5.digest('base64')
})
)
.on('error', reject)
})
}
+1 -1
View File
@@ -4,7 +4,7 @@
See ["Caching dependencies to speed up workflows"](https://help.github.com/github/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows) for how caching works.
Note that GitHub will remove any cache entries that have not been accessed in over 7 days. There is no limit on the number of caches you can store, but the total size of all caches in a repository is limited to 5 GB. If you exceed this limit, GitHub will save your cache but will begin evicting caches until the total size is less than 5 GB.
Note that GitHub will remove any cache entries that have not been accessed in over 7 days. There is no limit on the number of caches you can store, but the total size of all caches in a repository is limited to 10 GB. If you exceed this limit, GitHub will save your cache but will begin evicting caches until the total size is less than 10 GB.
## Usage
+22
View File
@@ -40,3 +40,25 @@
### 1.0.7
- Fixes permissions issue extracting archives with GNU tar on macOS ([issue](https://github.com/actions/cache/issues/527))
### 1.0.8
- Increase the allowed artifact cache size from 5GB to 10GB ([issue](https://github.com/actions/cache/discussions/497))
### 1.0.9
- Use @azure/ms-rest-js v2.6.0
- Use @azure/storage-blob v12.8.0
### 1.0.10
- Update `lockfileVersion` to `v2` in `package-lock.json [#1022](https://github.com/actions/toolkit/pull/1022)
### 1.0.11
- Fix file downloads > 2GB([issue](https://github.com/actions/cache/issues/773))
### 2.0.0
- Added support to check if Actions cache service feature is available or not [#1028](https://github.com/actions/toolkit/pull/1028)
### 2.0.3
- Update to v2.0.0 of `@actions/http-client`
### 2.0.4
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
+2 -2
View File
@@ -3,10 +3,10 @@
const fs = require('fs');
const os = require('os');
const filePath = process.env[`GITHUB_ENV`]
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_URL=${process.env.ACTIONS_RUNTIME_URL}${os.EOL}`, {
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_TOKEN=${process.env.ACTIONS_RUNTIME_TOKEN}${os.EOL}`, {
encoding: 'utf8'
})
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_TOKEN=${process.env.ACTIONS_RUNTIME_TOKEN}${os.EOL}`, {
fs.appendFileSync(filePath, `ACTIONS_CACHE_URL=${process.env.ACTIONS_CACHE_URL}${os.EOL}`, {
encoding: 'utf8'
})
fs.appendFileSync(filePath, `GITHUB_RUN_ID=${process.env.GITHUB_RUN_ID}${os.EOL}`, {
+14
View File
@@ -0,0 +1,14 @@
import * as cache from '../src/cache'
test('isFeatureAvailable returns true if server url is set', () => {
try {
process.env['ACTIONS_CACHE_URL'] = 'http://cache.com'
expect(cache.isFeatureAvailable()).toBe(true)
} finally {
delete process.env['ACTIONS_CACHE_URL']
}
})
test('isFeatureAvailable returns false if server url is not set', () => {
expect(cache.isFeatureAvailable()).toBe(false)
})
+4 -3
View File
@@ -87,7 +87,7 @@ test('download progress tracked correctly', () => {
expect(progress.isDone()).toBe(true)
})
test('display timer works correctly', () => {
test('display timer works correctly', done => {
const progress = new DownloadProgress(1000)
const infoMock = jest.spyOn(core, 'info')
@@ -103,6 +103,7 @@ test('display timer works correctly', () => {
const test2 = (): void => {
check()
expect(progress.timeoutHandle).toBeUndefined()
done()
}
// Validate the progress is displayed, stop the timer, and call test2.
@@ -112,7 +113,7 @@ test('display timer works correctly', () => {
progress.stopDisplayTimer()
progress.setReceivedBytes(1000)
setTimeout(() => test2(), 100)
setTimeout(() => test2(), 500)
}
// Start the timer, update the received bytes, and call test1.
@@ -122,7 +123,7 @@ test('display timer works correctly', () => {
progress.setReceivedBytes(500)
setTimeout(() => test1(), 100)
setTimeout(() => test1(), 500)
}
start()
+33 -2
View File
@@ -1,5 +1,6 @@
import {retry} from '../src/internal/requestUtils'
import {retry, retryTypedResponse} from '../src/internal/requestUtils'
import {HttpClientError} from '@actions/http-client'
import * as requestUtils from '../src/internal/requestUtils'
interface ITestResponse {
statusCode: number
@@ -30,7 +31,6 @@ async function handleResponse(
response: ITestResponse | undefined
): Promise<ITestResponse> {
if (!response) {
// eslint-disable-next-line no-undef
fail('Retry method called too many times')
}
@@ -146,3 +146,34 @@ test('retry converts errors to response object', async () => {
null
)
})
test('retryTypedResponse gives an error with error message', async () => {
const httpClientError = new HttpClientError(
'The cache filesize must be between 0 and 10 * 1024 * 1024 bytes',
400
)
jest.spyOn(requestUtils, 'retry').mockReturnValue(
new Promise(resolve => {
resolve(httpClientError)
})
)
try {
await retryTypedResponse<string>(
'reserveCache',
async () =>
new Promise(resolve => {
resolve({
statusCode: 400,
result: '',
headers: {},
error: httpClientError
})
})
)
} catch (error) {
expect(error).toHaveProperty(
'message',
'The cache filesize must be between 0 and 10 * 1024 * 1024 bytes'
)
}
})
-1
View File
@@ -18,7 +18,6 @@ beforeAll(() => {
jest.spyOn(core, 'warning').mockImplementation(() => {})
jest.spyOn(core, 'error').mockImplementation(() => {})
// eslint-disable-next-line @typescript-eslint/promise-function-async
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
const actualUtils = jest.requireActual('../src/internal/cacheUtils')
return actualUtils.getCacheFileName(cm)
+119 -16
View File
@@ -5,6 +5,12 @@ import * as cacheHttpClient from '../src/internal/cacheHttpClient'
import * as cacheUtils from '../src/internal/cacheUtils'
import {CacheFilename, CompressionMethod} from '../src/internal/constants'
import * as tar from '../src/internal/tar'
import {TypedResponse} from '@actions/http-client/lib/interfaces'
import {
ReserveCacheResponse,
ITypedResponseWithError
} from '../src/internal/contracts'
import {HttpClientError} from '@actions/http-client'
jest.mock('../src/internal/cacheHttpClient')
jest.mock('../src/internal/cacheUtils')
@@ -16,17 +22,13 @@ beforeAll(() => {
jest.spyOn(core, 'info').mockImplementation(() => {})
jest.spyOn(core, 'warning').mockImplementation(() => {})
jest.spyOn(core, 'error').mockImplementation(() => {})
// eslint-disable-next-line @typescript-eslint/promise-function-async
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
const actualUtils = jest.requireActual('../src/internal/cacheUtils')
return actualUtils.getCacheFileName(cm)
})
jest.spyOn(cacheUtils, 'resolvePaths').mockImplementation(async filePaths => {
return filePaths.map(x => path.resolve(x))
})
jest.spyOn(cacheUtils, 'createTempDirectory').mockImplementation(async () => {
return Promise.resolve('/foo/bar')
})
@@ -47,7 +49,7 @@ test('save with large cache outputs should fail', async () => {
const createTarMock = jest.spyOn(tar, 'createTar')
const cacheSize = 6 * 1024 * 1024 * 1024 //~6GB, over the 5GB limit
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
jest
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
.mockReturnValueOnce(cacheSize)
@@ -57,7 +59,7 @@ test('save with large cache outputs should fail', async () => {
.mockReturnValueOnce(Promise.resolve(compression))
await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
'Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache.'
'Cache size of ~11264 MB (11811160064 B) is over the 10GB limit, not saving cache.'
)
const archiveFolder = '/foo/bar'
@@ -71,6 +73,98 @@ test('save with large cache outputs should fail', async () => {
expect(getCompressionMock).toHaveBeenCalledTimes(1)
})
test('save with large cache outputs should fail in GHES with error message', async () => {
const filePath = 'node_modules'
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
const cachePaths = [path.resolve(filePath)]
const createTarMock = jest.spyOn(tar, 'createTar')
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
jest
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
.mockReturnValueOnce(cacheSize)
const compression = CompressionMethod.Gzip
const getCompressionMock = jest
.spyOn(cacheUtils, 'getCompressionMethod')
.mockReturnValueOnce(Promise.resolve(compression))
jest.spyOn(cacheUtils, 'isGhes').mockReturnValueOnce(true)
const reserveCacheMock = jest
.spyOn(cacheHttpClient, 'reserveCache')
.mockImplementation(async () => {
const response: ITypedResponseWithError<ReserveCacheResponse> = {
statusCode: 400,
result: null,
headers: {},
error: new HttpClientError(
'The cache filesize must be between 0 and 1073741824 bytes',
400
)
}
return response
})
await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
'The cache filesize must be between 0 and 1073741824 bytes'
)
const archiveFolder = '/foo/bar'
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
expect(createTarMock).toHaveBeenCalledTimes(1)
expect(createTarMock).toHaveBeenCalledWith(
archiveFolder,
cachePaths,
compression
)
expect(getCompressionMock).toHaveBeenCalledTimes(1)
})
test('save with large cache outputs should fail in GHES without error message', async () => {
const filePath = 'node_modules'
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
const cachePaths = [path.resolve(filePath)]
const createTarMock = jest.spyOn(tar, 'createTar')
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
jest
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
.mockReturnValueOnce(cacheSize)
const compression = CompressionMethod.Gzip
const getCompressionMock = jest
.spyOn(cacheUtils, 'getCompressionMethod')
.mockReturnValueOnce(Promise.resolve(compression))
jest.spyOn(cacheUtils, 'isGhes').mockReturnValueOnce(true)
const reserveCacheMock = jest
.spyOn(cacheHttpClient, 'reserveCache')
.mockImplementation(async () => {
const response: ITypedResponseWithError<ReserveCacheResponse> = {
statusCode: 400,
result: null,
headers: {}
}
return response
})
await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
'Cache size of ~11264 MB (11811160064 B) is over the data cap limit, not saving cache.'
)
const archiveFolder = '/foo/bar'
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
expect(createTarMock).toHaveBeenCalledTimes(1)
expect(createTarMock).toHaveBeenCalledWith(
archiveFolder,
cachePaths,
compression
)
expect(getCompressionMock).toHaveBeenCalledTimes(1)
})
test('save with reserve cache failure should fail', async () => {
const paths = ['node_modules']
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
@@ -78,7 +172,12 @@ test('save with reserve cache failure should fail', async () => {
const reserveCacheMock = jest
.spyOn(cacheHttpClient, 'reserveCache')
.mockImplementation(async () => {
return -1
const response: TypedResponse<ReserveCacheResponse> = {
statusCode: 500,
result: null,
headers: {}
}
return response
})
const createTarMock = jest.spyOn(tar, 'createTar')
@@ -95,7 +194,7 @@ test('save with reserve cache failure should fail', async () => {
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, paths, {
compressionMethod: compression
})
expect(createTarMock).toHaveBeenCalledTimes(0)
expect(createTarMock).toHaveBeenCalledTimes(1)
expect(saveCacheMock).toHaveBeenCalledTimes(0)
expect(getCompressionMock).toHaveBeenCalledTimes(1)
})
@@ -109,7 +208,12 @@ test('save with server error should fail', async () => {
const reserveCacheMock = jest
.spyOn(cacheHttpClient, 'reserveCache')
.mockImplementation(async () => {
return cacheId
const response: TypedResponse<ReserveCacheResponse> = {
statusCode: 500,
result: {cacheId},
headers: {}
}
return response
})
const createTarMock = jest.spyOn(tar, 'createTar')
@@ -131,17 +235,14 @@ test('save with server error should fail', async () => {
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], {
compressionMethod: compression
})
const archiveFolder = '/foo/bar'
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
expect(createTarMock).toHaveBeenCalledTimes(1)
expect(createTarMock).toHaveBeenCalledWith(
archiveFolder,
cachePaths,
compression
)
expect(saveCacheMock).toHaveBeenCalledTimes(1)
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile, undefined)
expect(getCompressionMock).toHaveBeenCalledTimes(1)
@@ -156,7 +257,12 @@ test('save with valid inputs uploads a cache', async () => {
const reserveCacheMock = jest
.spyOn(cacheHttpClient, 'reserveCache')
.mockImplementation(async () => {
return cacheId
const response: TypedResponse<ReserveCacheResponse> = {
statusCode: 500,
result: {cacheId},
headers: {}
}
return response
})
const createTarMock = jest.spyOn(tar, 'createTar')
@@ -172,17 +278,14 @@ test('save with valid inputs uploads a cache', async () => {
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], {
compressionMethod: compression
})
const archiveFolder = '/foo/bar'
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
expect(createTarMock).toHaveBeenCalledTimes(1)
expect(createTarMock).toHaveBeenCalledWith(
archiveFolder,
cachePaths,
compression
)
expect(saveCacheMock).toHaveBeenCalledTimes(1)
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile, undefined)
expect(getCompressionMock).toHaveBeenCalledTimes(1)
+816 -4333
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/cache",
"version": "1.0.7",
"version": "2.0.4",
"preview": true,
"description": "Actions cache lib",
"keywords": [
@@ -40,16 +40,16 @@
"@actions/core": "^1.2.6",
"@actions/exec": "^1.0.1",
"@actions/glob": "^0.1.0",
"@actions/http-client": "^1.0.9",
"@actions/http-client": "^2.0.1",
"@actions/io": "^1.0.1",
"@azure/ms-rest-js": "^2.0.7",
"@azure/storage-blob": "^12.1.2",
"@azure/ms-rest-js": "^2.6.0",
"@azure/storage-blob": "^12.8.0",
"semver": "^6.1.0",
"uuid": "^3.3.3"
},
"devDependencies": {
"typescript": "^3.8.3",
"@types/semver": "^6.0.0",
"@types/uuid": "^3.4.5"
"@types/uuid": "^3.4.5",
"typescript": "^3.8.3"
}
}
+61 -26
View File
@@ -43,6 +43,16 @@ function checkKey(key: string): void {
}
}
/**
* isFeatureAvailable to check the presence of Actions cache service
*
* @returns boolean return true if Actions cache service feature is available, otherwise false
*/
export function isFeatureAvailable(): boolean {
return !!process.env['ACTIONS_CACHE_URL']
}
/**
* Restores cache from keys
*
@@ -142,17 +152,7 @@ export async function saveCache(
checkKey(key)
const compressionMethod = await utils.getCompressionMethod()
core.debug('Reserving Cache')
const cacheId = await cacheHttpClient.reserveCache(key, paths, {
compressionMethod
})
if (cacheId === -1) {
throw new ReserveCacheError(
`Unable to reserve cache with key ${key}, another job may be creating this cache.`
)
}
core.debug(`Cache ID: ${cacheId}`)
let cacheId = null
const cachePaths = await utils.resolvePaths(paths)
core.debug('Cache Paths:')
@@ -166,24 +166,59 @@ export async function saveCache(
core.debug(`Archive Path: ${archivePath}`)
await createTar(archiveFolder, cachePaths, compressionMethod)
if (core.isDebug()) {
await listTar(archivePath, compressionMethod)
}
try {
await createTar(archiveFolder, cachePaths, compressionMethod)
if (core.isDebug()) {
await listTar(archivePath, compressionMethod)
}
const fileSizeLimit = 10 * 1024 * 1024 * 1024 // 10GB per repo limit
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
core.debug(`File Size: ${archiveFileSize}`)
const fileSizeLimit = 5 * 1024 * 1024 * 1024 // 5GB per repo limit
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
core.debug(`File Size: ${archiveFileSize}`)
if (archiveFileSize > fileSizeLimit) {
throw new Error(
`Cache size of ~${Math.round(
archiveFileSize / (1024 * 1024)
)} MB (${archiveFileSize} B) is over the 5GB limit, not saving cache.`
// For GHES, this check will take place in ReserveCache API with enterprise file size limit
if (archiveFileSize > fileSizeLimit && !utils.isGhes()) {
throw new Error(
`Cache size of ~${Math.round(
archiveFileSize / (1024 * 1024)
)} MB (${archiveFileSize} B) is over the 10GB limit, not saving cache.`
)
}
core.debug('Reserving Cache')
const reserveCacheResponse = await cacheHttpClient.reserveCache(
key,
paths,
{
compressionMethod,
cacheSize: archiveFileSize
}
)
}
core.debug(`Saving Cache (ID: ${cacheId})`)
await cacheHttpClient.saveCache(cacheId, archivePath, options)
if (reserveCacheResponse?.result?.cacheId) {
cacheId = reserveCacheResponse?.result?.cacheId
} else if (reserveCacheResponse?.statusCode === 400) {
throw new Error(
reserveCacheResponse?.error?.message ??
`Cache size of ~${Math.round(
archiveFileSize / (1024 * 1024)
)} MB (${archiveFileSize} B) is over the data cap limit, not saving cache.`
)
} else {
throw new ReserveCacheError(
`Unable to reserve cache with key ${key}, another job may be creating this cache. More details: ${reserveCacheResponse?.error?.message}`
)
}
core.debug(`Saving Cache (ID: ${cacheId})`)
await cacheHttpClient.saveCache(cacheId, archivePath, options)
} finally {
// Try to delete the archive to save space
try {
await utils.unlinkFile(archivePath)
} catch (error) {
core.debug(`Failed to delete archive: ${error}`)
}
}
return cacheId
}
+15 -15
View File
@@ -1,7 +1,10 @@
import * as core from '@actions/core'
import {HttpClient} from '@actions/http-client'
import {BearerCredentialHandler} from '@actions/http-client/auth'
import {IRequestOptions, ITypedResponse} from '@actions/http-client/interfaces'
import {BearerCredentialHandler} from '@actions/http-client/lib/auth'
import {
RequestOptions,
TypedResponse
} from '@actions/http-client/lib/interfaces'
import * as crypto from 'crypto'
import * as fs from 'fs'
import {URL} from 'url'
@@ -13,7 +16,8 @@ import {
InternalCacheOptions,
CommitCacheRequest,
ReserveCacheRequest,
ReserveCacheResponse
ReserveCacheResponse,
ITypedResponseWithError
} from './contracts'
import {downloadCacheHttpClient, downloadCacheStorageSDK} from './downloadUtils'
import {
@@ -31,12 +35,7 @@ import {
const versionSalt = '1.0'
function getCacheApiUrl(resource: string): string {
// Ideally we just use ACTIONS_CACHE_URL
const baseUrl: string = (
process.env['ACTIONS_CACHE_URL'] ||
process.env['ACTIONS_RUNTIME_URL'] ||
''
).replace('pipelines', 'artifactcache')
const baseUrl: string = process.env['ACTIONS_CACHE_URL'] || ''
if (!baseUrl) {
throw new Error('Cache Service Url not found, unable to restore cache.')
}
@@ -50,8 +49,8 @@ function createAcceptHeader(type: string, apiVersion: string): string {
return `${type};api-version=${apiVersion}`
}
function getRequestOptions(): IRequestOptions {
const requestOptions: IRequestOptions = {
function getRequestOptions(): RequestOptions {
const requestOptions: RequestOptions = {
headers: {
Accept: createAcceptHeader('application/json', '6.0-preview.1')
}
@@ -148,13 +147,14 @@ export async function reserveCache(
key: string,
paths: string[],
options?: InternalCacheOptions
): Promise<number> {
): Promise<ITypedResponseWithError<ReserveCacheResponse>> {
const httpClient = createHttpClient()
const version = getCacheVersion(paths, options?.compressionMethod)
const reserveCacheRequest: ReserveCacheRequest = {
key,
version
version,
cacheSize: options?.cacheSize
}
const response = await retryTypedResponse('reserveCache', async () =>
httpClient.postJson<ReserveCacheResponse>(
@@ -162,7 +162,7 @@ export async function reserveCache(
reserveCacheRequest
)
)
return response?.result?.cacheId ?? -1
return response
}
function getContentRange(start: number, end: number): string {
@@ -278,7 +278,7 @@ async function commitCache(
httpClient: HttpClient,
cacheId: number,
filesize: number
): Promise<ITypedResponse<null>> {
): Promise<TypedResponse<null>> {
const commitCacheRequest: CommitCacheRequest = {size: filesize}
return await retryTypedResponse('commitCache', async () =>
httpClient.postJson<null>(
+7
View File
@@ -123,3 +123,10 @@ export function assertDefined<T>(name: string, value?: T): T {
return value
}
export function isGhes(): boolean {
const ghUrl = new URL(
process.env['GITHUB_SERVER_URL'] || 'https://github.com'
)
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
}
+8
View File
@@ -1,4 +1,10 @@
import {CompressionMethod} from './constants'
import {TypedResponse} from '@actions/http-client/lib/interfaces'
import {HttpClientError} from '@actions/http-client'
export interface ITypedResponseWithError<T> extends TypedResponse<T> {
error?: HttpClientError
}
export interface ArtifactCacheEntry {
cacheKey?: string
@@ -14,6 +20,7 @@ export interface CommitCacheRequest {
export interface ReserveCacheRequest {
key: string
version?: string
cacheSize?: number
}
export interface ReserveCacheResponse {
@@ -22,4 +29,5 @@ export interface ReserveCacheResponse {
export interface InternalCacheOptions {
compressionMethod?: CompressionMethod
cacheSize?: number
}
+5 -5
View File
@@ -1,6 +1,5 @@
import * as core from '@actions/core'
import {HttpClient} from '@actions/http-client'
import {IHttpClientResponse} from '@actions/http-client/interfaces'
import {HttpClient, HttpClientResponse} from '@actions/http-client'
import {BlockBlobClient} from '@azure/storage-blob'
import {TransferProgressEvent} from '@azure/ms-rest-js'
import * as buffer from 'buffer'
@@ -20,7 +19,7 @@ import {retryHttpClientResponse} from './requestUtils'
* @param output the writable stream
*/
async function pipeResponseToStream(
response: IHttpClientResponse,
response: HttpClientResponse,
output: NodeJS.WritableStream
): Promise<void> {
const pipeline = util.promisify(stream.pipeline)
@@ -133,7 +132,7 @@ export class DownloadProgress {
*
* @param delayInMs the delay between each write
*/
startDisplayTimer(delayInMs: number = 1000): void {
startDisplayTimer(delayInMs = 1000): void {
const displayCallback = (): void => {
this.display()
@@ -240,7 +239,8 @@ export async function downloadCacheStorageSDK(
//
// If the file exceeds the buffer maximum length (~1 GB on 32-bit systems and ~2 GB
// on 64-bit systems), split the download into multiple segments
const maxSegmentSize = buffer.constants.MAX_LENGTH
// ~2 GB = 2147483647, beyond this, we start getting out of range error. So, capping it accordingly.
const maxSegmentSize = Math.min(2147483647, buffer.constants.MAX_LENGTH)
const downloadProgress = new DownloadProgress(contentLength)
const fd = fs.openSync(archivePath, 'w')
+15 -13
View File
@@ -1,10 +1,11 @@
import * as core from '@actions/core'
import {HttpCodes, HttpClientError} from '@actions/http-client'
import {
IHttpClientResponse,
ITypedResponse
} from '@actions/http-client/interfaces'
HttpCodes,
HttpClientError,
HttpClientResponse
} from '@actions/http-client'
import {DefaultRetryDelay, DefaultRetryAttempts} from './constants'
import {ITypedResponseWithError} from './contracts'
export function isSuccessStatusCode(statusCode?: number): boolean {
if (!statusCode) {
@@ -94,24 +95,25 @@ export async function retry<T>(
export async function retryTypedResponse<T>(
name: string,
method: () => Promise<ITypedResponse<T>>,
method: () => Promise<ITypedResponseWithError<T>>,
maxAttempts = DefaultRetryAttempts,
delay = DefaultRetryDelay
): Promise<ITypedResponse<T>> {
): Promise<ITypedResponseWithError<T>> {
return await retry(
name,
method,
(response: ITypedResponse<T>) => response.statusCode,
(response: ITypedResponseWithError<T>) => response.statusCode,
maxAttempts,
delay,
// If the error object contains the statusCode property, extract it and return
// an ITypedResponse<T> so it can be processed by the retry logic.
// an TypedResponse<T> so it can be processed by the retry logic.
(error: Error) => {
if (error instanceof HttpClientError) {
return {
statusCode: error.statusCode,
result: null,
headers: {}
headers: {},
error
}
} else {
return undefined
@@ -120,16 +122,16 @@ export async function retryTypedResponse<T>(
)
}
export async function retryHttpClientResponse<T>(
export async function retryHttpClientResponse(
name: string,
method: () => Promise<IHttpClientResponse>,
method: () => Promise<HttpClientResponse>,
maxAttempts = DefaultRetryAttempts,
delay = DefaultRetryDelay
): Promise<IHttpClientResponse> {
): Promise<HttpClientResponse> {
return await retry(
name,
method,
(response: IHttpClientResponse) => response.message.statusCode,
(response: HttpClientResponse) => response.message.statusCode,
maxAttempts,
delay
)
+104
View File
@@ -23,6 +23,7 @@ Outputs can be set with `setOutput` which makes them available to be mapped into
```js
const myInput = core.getInput('inputName', { required: true });
const myBooleanInput = core.getBooleanInput('booleanInputName', { required: true });
const myMultilineInput = core.getMultilineInput('multilineInputName', { required: true });
core.setOutput('outputKey', 'outputVal');
```
@@ -91,6 +92,8 @@ try {
// Do stuff
core.info('Output to the actions build log')
core.notice('This is a message that will also emit an annotation')
}
catch (err) {
core.error(`Error ${err}, action may still succeed though`);
@@ -114,6 +117,59 @@ const result = await core.group('Do something async', async () => {
})
```
#### Annotations
This library has 3 methods that will produce [annotations](https://docs.github.com/en/rest/reference/checks#create-a-check-run).
```js
core.error('This is a bad error. This will also fail the build.')
core.warning('Something went wrong, but it\'s not bad enough to fail the build.')
core.notice('Something happened that you might want to know about.')
```
These will surface to the UI in the Actions page and on Pull Requests. They look something like this:
![Annotations Image](../../docs/assets/annotations.png)
These annotations can also be attached to particular lines and columns of your source files to show exactly where a problem is occuring.
These options are:
```typescript
export interface AnnotationProperties {
/**
* A title for the annotation.
*/
title?: string
/**
* The name of the file for which the annotation should be created.
*/
file?: string
/**
* The start line for the annotation.
*/
startLine?: number
/**
* The end line for the annotation. Defaults to `startLine` when `startLine` is provided.
*/
endLine?: number
/**
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
*/
startColumn?: number
/**
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
* Defaults to `startColumn` when `startColumn` is provided.
*/
endColumn?: number
}
```
#### Styling output
Colored output is supported in the Action logs via standard [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). 3/4 bit, 8 bit and 24 bit colors are all supported.
@@ -206,3 +262,51 @@ var pid = core.getState("pidToKill");
process.kill(pid);
```
#### OIDC Token
You can use these methods to interact with the GitHub OIDC provider and get a JWT ID token which would help to get access token from third party cloud providers.
**Method Name**: getIDToken()
**Inputs**
audience : optional
**Outputs**
A [JWT](https://jwt.io/) ID Token
In action's `main.ts`:
```js
const core = require('@actions/core');
async function getIDTokenAction(): Promise<void> {
const audience = core.getInput('audience', {required: false})
const id_token1 = await core.getIDToken() // ID Token with default audience
const id_token2 = await core.getIDToken(audience) // ID token with custom audience
// this id_token can be used to get access token from third party cloud providers
}
getIDTokenAction()
```
In action's `actions.yml`:
```yaml
name: 'GetIDToken'
description: 'Get ID token from Github OIDC provider'
inputs:
audience:
description: 'Audience for which the ID token is intended for'
required: false
outputs:
id_token1:
description: 'ID token obtained from OIDC provider'
id_token2:
description: 'ID token obtained from OIDC provider'
runs:
using: 'node12'
main: 'dist/index.js'
```
+28
View File
@@ -1,5 +1,33 @@
# @actions/core Releases
### 1.8.2
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
### 1.8.1
- Update to v2.0.0 of `@actions/http-client`
### 1.8.0
- Deprecate `markdownSummary` extension export in favor of `summary`
- https://github.com/actions/toolkit/pull/1072
- https://github.com/actions/toolkit/pull/1073
### 1.7.0
- [Added `markdownSummary` extension](https://github.com/actions/toolkit/pull/1014)
### 1.6.0
- [Added OIDC Client function `getIDToken`](https://github.com/actions/toolkit/pull/919)
- [Added `file` parameter to `AnnotationProperties`](https://github.com/actions/toolkit/pull/896)
### 1.5.0
- [Added support for notice annotations and more annotation fields](https://github.com/actions/toolkit/pull/855)
### 1.4.0
- [Added the `getMultilineInput` function](https://github.com/actions/toolkit/pull/829)
### 1.3.0
- [Added the trimWhitespace option to getInput](https://github.com/actions/toolkit/pull/802)
- [Added the getBooleanInput function](https://github.com/actions/toolkit/pull/725)
### 1.2.7
- [Prepend newline for set-output](https://github.com/actions/toolkit/pull/772)
+127
View File
@@ -2,6 +2,8 @@ import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import * as core from '../src/core'
import {HttpClient} from '@actions/http-client'
import {toCommandProperties} from '../src/utils'
/* eslint-disable @typescript-eslint/unbound-method */
@@ -27,6 +29,9 @@ const testEnvVars = {
INPUT_BOOLEAN_INPUT_FALSE2: 'False',
INPUT_BOOLEAN_INPUT_FALSE3: 'FALSE',
INPUT_WRONG_BOOLEAN_INPUT: 'wrong',
INPUT_WITH_TRAILING_WHITESPACE: ' some val ',
INPUT_MY_INPUT_LIST: 'val1\nval2\nval3',
// Save inputs
STATE_TEST_1: 'state_val',
@@ -165,6 +170,30 @@ describe('@actions/core', () => {
)
})
it('getMultilineInput works', () => {
expect(core.getMultilineInput('my input list')).toEqual([
'val1',
'val2',
'val3'
])
})
it('getInput trims whitespace by default', () => {
expect(core.getInput('with trailing whitespace')).toBe('some val')
})
it('getInput trims whitespace when option is explicitly true', () => {
expect(
core.getInput('with trailing whitespace', {trimWhitespace: true})
).toBe('some val')
})
it('getInput does not trim whitespace when option is false', () => {
expect(
core.getInput('with trailing whitespace', {trimWhitespace: false})
).toBe(' some val ')
})
it('getInput gets non-required boolean input', () => {
expect(core.getBooleanInput('boolean input')).toBe(true)
})
@@ -242,6 +271,21 @@ describe('@actions/core', () => {
assertWriteCalls([`::error::Error: ${message}${os.EOL}`])
})
it('error handles parameters correctly', () => {
const message = 'this is my error message'
core.error(new Error(message), {
title: 'A title',
file: 'root/test.txt',
startColumn: 1,
endColumn: 2,
startLine: 5,
endLine: 5
})
assertWriteCalls([
`::error title=A title,file=root/test.txt,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
])
})
it('warning sets the correct message', () => {
core.warning('Warning')
assertWriteCalls([`::warning::Warning${os.EOL}`])
@@ -258,6 +302,72 @@ describe('@actions/core', () => {
assertWriteCalls([`::warning::Error: ${message}${os.EOL}`])
})
it('warning handles parameters correctly', () => {
const message = 'this is my error message'
core.warning(new Error(message), {
title: 'A title',
file: 'root/test.txt',
startColumn: 1,
endColumn: 2,
startLine: 5,
endLine: 5
})
assertWriteCalls([
`::warning title=A title,file=root/test.txt,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
])
})
it('notice sets the correct message', () => {
core.notice('Notice')
assertWriteCalls([`::notice::Notice${os.EOL}`])
})
it('notice escapes the message', () => {
core.notice('\r\nnotice\n')
assertWriteCalls([`::notice::%0D%0Anotice%0A${os.EOL}`])
})
it('notice handles an error object', () => {
const message = 'this is my error message'
core.notice(new Error(message))
assertWriteCalls([`::notice::Error: ${message}${os.EOL}`])
})
it('notice handles parameters correctly', () => {
const message = 'this is my error message'
core.notice(new Error(message), {
title: 'A title',
file: 'root/test.txt',
startColumn: 1,
endColumn: 2,
startLine: 5,
endLine: 5
})
assertWriteCalls([
`::notice title=A title,file=root/test.txt,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
])
})
it('annotations map field names correctly', () => {
const commandProperties = toCommandProperties({
title: 'A title',
file: 'root/test.txt',
startColumn: 1,
endColumn: 2,
startLine: 5,
endLine: 5
})
expect(commandProperties.title).toBe('A title')
expect(commandProperties.file).toBe('root/test.txt')
expect(commandProperties.col).toBe(1)
expect(commandProperties.endColumn).toBe(2)
expect(commandProperties.line).toBe(5)
expect(commandProperties.endLine).toBe(5)
expect(commandProperties.startColumn).toBeUndefined()
expect(commandProperties.startLine).toBeUndefined()
})
it('startGroup starts a new group', () => {
core.startGroup('my-group')
assertWriteCalls([`::group::my-group${os.EOL}`])
@@ -360,3 +470,20 @@ function verifyFileCommand(command: string, expectedContents: string): void {
fs.unlinkSync(filePath)
}
}
function getTokenEndPoint(): string {
return 'https://vstoken.actions.githubusercontent.com/.well-known/openid-configuration'
}
describe('oidc-client-tests', () => {
it('Get Http Client', async () => {
const http = new HttpClient('actions/oidc-client')
expect(http).toBeDefined()
})
it('HTTP get request to get token endpoint', async () => {
const http = new HttpClient('actions/oidc-client')
const res = await http.get(getTokenEndPoint())
expect(res.message.statusCode).toBe(200)
})
})
+275
View File
@@ -0,0 +1,275 @@
import * as fs from 'fs'
import * as os from 'os'
import path from 'path'
import {summary, SUMMARY_ENV_VAR} from '../src/summary'
const testDirectoryPath = path.join(__dirname, 'test')
const testFilePath = path.join(testDirectoryPath, 'test-summary.md')
async function assertSummary(expected: string): Promise<void> {
const file = await fs.promises.readFile(testFilePath, {encoding: 'utf8'})
expect(file).toEqual(expected)
}
const fixtures = {
text: 'hello world 🌎',
code: `func fork() {
for {
go fork()
}
}`,
list: ['foo', 'bar', 'baz', '💣'],
table: [
[
{
data: 'foo',
header: true
},
{
data: 'bar',
header: true
},
{
data: 'baz',
header: true
},
{
data: 'tall',
rowspan: '3'
}
],
['one', 'two', 'three'],
[
{
data: 'wide',
colspan: '3'
}
]
],
details: {
label: 'open me',
content: '🎉 surprise'
},
img: {
src: 'https://github.com/actions.png',
alt: 'actions logo',
options: {
width: '32',
height: '32'
}
},
quote: {
text: 'Where the world builds software',
cite: 'https://github.com/about'
},
link: {
text: 'GitHub',
href: 'https://github.com/'
}
}
describe('@actions/core/src/summary', () => {
beforeEach(async () => {
process.env[SUMMARY_ENV_VAR] = testFilePath
await fs.promises.mkdir(testDirectoryPath, {recursive: true})
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
summary.emptyBuffer()
})
afterAll(async () => {
await fs.promises.unlink(testFilePath)
})
it('throws if summary env var is undefined', async () => {
process.env[SUMMARY_ENV_VAR] = undefined
const write = summary.addRaw(fixtures.text).write()
await expect(write).rejects.toThrow()
})
it('throws if summary file does not exist', async () => {
await fs.promises.unlink(testFilePath)
const write = summary.addRaw(fixtures.text).write()
await expect(write).rejects.toThrow()
})
it('appends text to summary file', async () => {
await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'})
await summary.addRaw(fixtures.text).write()
await assertSummary(`# ${fixtures.text}`)
})
it('overwrites text to summary file', async () => {
await fs.promises.writeFile(testFilePath, 'overwrite', {encoding: 'utf8'})
await summary.addRaw(fixtures.text).write({overwrite: true})
await assertSummary(fixtures.text)
})
it('appends text with EOL to summary file', async () => {
await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'})
await summary.addRaw(fixtures.text, true).write()
await assertSummary(`# ${fixtures.text}${os.EOL}`)
})
it('chains appends text to summary file', async () => {
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
await summary
.addRaw(fixtures.text)
.addRaw(fixtures.text)
.addRaw(fixtures.text)
.write()
await assertSummary([fixtures.text, fixtures.text, fixtures.text].join(''))
})
it('empties buffer after write', async () => {
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
await summary.addRaw(fixtures.text).write()
await assertSummary(fixtures.text)
expect(summary.isEmptyBuffer()).toBe(true)
})
it('returns summary buffer as string', () => {
summary.addRaw(fixtures.text)
expect(summary.stringify()).toEqual(fixtures.text)
})
it('return correct values for isEmptyBuffer', () => {
summary.addRaw(fixtures.text)
expect(summary.isEmptyBuffer()).toBe(false)
summary.emptyBuffer()
expect(summary.isEmptyBuffer()).toBe(true)
})
it('clears a buffer and summary file', async () => {
await fs.promises.writeFile(testFilePath, 'content', {encoding: 'utf8'})
await summary.clear()
await assertSummary('')
expect(summary.isEmptyBuffer()).toBe(true)
})
it('adds EOL', async () => {
await summary
.addRaw(fixtures.text)
.addEOL()
.write()
await assertSummary(fixtures.text + os.EOL)
})
it('adds a code block without language', async () => {
await summary.addCodeBlock(fixtures.code).write()
const expected = `<pre><code>func fork() {\n for {\n go fork()\n }\n}</code></pre>${os.EOL}`
await assertSummary(expected)
})
it('adds a code block with a language', async () => {
await summary.addCodeBlock(fixtures.code, 'go').write()
const expected = `<pre lang="go"><code>func fork() {\n for {\n go fork()\n }\n}</code></pre>${os.EOL}`
await assertSummary(expected)
})
it('adds an unordered list', async () => {
await summary.addList(fixtures.list).write()
const expected = `<ul><li>foo</li><li>bar</li><li>baz</li><li>💣</li></ul>${os.EOL}`
await assertSummary(expected)
})
it('adds an ordered list', async () => {
await summary.addList(fixtures.list, true).write()
const expected = `<ol><li>foo</li><li>bar</li><li>baz</li><li>💣</li></ol>${os.EOL}`
await assertSummary(expected)
})
it('adds a table', async () => {
await summary.addTable(fixtures.table).write()
const expected = `<table><tr><th>foo</th><th>bar</th><th>baz</th><td rowspan="3">tall</td></tr><tr><td>one</td><td>two</td><td>three</td></tr><tr><td colspan="3">wide</td></tr></table>${os.EOL}`
await assertSummary(expected)
})
it('adds a details element', async () => {
await summary
.addDetails(fixtures.details.label, fixtures.details.content)
.write()
const expected = `<details><summary>open me</summary>🎉 surprise</details>${os.EOL}`
await assertSummary(expected)
})
it('adds an image with alt text', async () => {
await summary.addImage(fixtures.img.src, fixtures.img.alt).write()
const expected = `<img src="https://github.com/actions.png" alt="actions logo">${os.EOL}`
await assertSummary(expected)
})
it('adds an image with custom dimensions', async () => {
await summary
.addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options)
.write()
const expected = `<img src="https://github.com/actions.png" alt="actions logo" width="32" height="32">${os.EOL}`
await assertSummary(expected)
})
it('adds an image with custom dimensions', async () => {
await summary
.addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options)
.write()
const expected = `<img src="https://github.com/actions.png" alt="actions logo" width="32" height="32">${os.EOL}`
await assertSummary(expected)
})
it('adds headings h1...h6', async () => {
for (const i of [1, 2, 3, 4, 5, 6]) {
summary.addHeading('heading', i)
}
await summary.write()
const expected = `<h1>heading</h1>${os.EOL}<h2>heading</h2>${os.EOL}<h3>heading</h3>${os.EOL}<h4>heading</h4>${os.EOL}<h5>heading</h5>${os.EOL}<h6>heading</h6>${os.EOL}`
await assertSummary(expected)
})
it('adds h1 if heading level not specified', async () => {
await summary.addHeading('heading').write()
const expected = `<h1>heading</h1>${os.EOL}`
await assertSummary(expected)
})
it('uses h1 if heading level is garbage or out of range', async () => {
await summary
.addHeading('heading', 'foobar')
.addHeading('heading', 1337)
.addHeading('heading', -1)
.addHeading('heading', Infinity)
.write()
const expected = `<h1>heading</h1>${os.EOL}<h1>heading</h1>${os.EOL}<h1>heading</h1>${os.EOL}<h1>heading</h1>${os.EOL}`
await assertSummary(expected)
})
it('adds a separator', async () => {
await summary.addSeparator().write()
const expected = `<hr>${os.EOL}`
await assertSummary(expected)
})
it('adds a break', async () => {
await summary.addBreak().write()
const expected = `<br>${os.EOL}`
await assertSummary(expected)
})
it('adds a quote', async () => {
await summary.addQuote(fixtures.quote.text).write()
const expected = `<blockquote>Where the world builds software</blockquote>${os.EOL}`
await assertSummary(expected)
})
it('adds a quote with citation', async () => {
await summary.addQuote(fixtures.quote.text, fixtures.quote.cite).write()
const expected = `<blockquote cite="https://github.com/about">Where the world builds software</blockquote>${os.EOL}`
await assertSummary(expected)
})
it('adds a link with href', async () => {
await summary.addLink(fixtures.link.text, fixtures.link.href).write()
const expected = `<a href="https://github.com/">GitHub</a>${os.EOL}`
await assertSummary(expected)
})
})
+50 -2
View File
@@ -1,14 +1,62 @@
{
"name": "@actions/core",
"version": "1.2.7",
"lockfileVersion": 1,
"version": "1.8.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@actions/core",
"version": "1.8.1",
"license": "MIT",
"dependencies": {
"@actions/http-client": "^2.0.1"
},
"devDependencies": {
"@types/node": "^12.0.2"
}
},
"node_modules/@actions/http-client": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
"dependencies": {
"tunnel": "^0.0.6"
}
},
"node_modules/@types/node": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
"dev": true
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
"engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
}
}
},
"dependencies": {
"@actions/http-client": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
"requires": {
"tunnel": "^0.0.6"
}
},
"@types/node": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
"dev": true
},
"tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
}
}
}
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/core",
"version": "1.2.7",
"version": "1.8.2",
"description": "Actions core lib",
"keywords": [
"github",
@@ -35,6 +35,9 @@
"bugs": {
"url": "https://github.com/actions/toolkit/issues"
},
"dependencies": {
"@actions/http-client": "^2.0.1"
},
"devDependencies": {
"@types/node": "^12.0.2"
}
+2 -2
View File
@@ -6,7 +6,7 @@ import {toCommandValue} from './utils'
// We use any as a valid input type
/* eslint-disable @typescript-eslint/no-explicit-any */
interface CommandProperties {
export interface CommandProperties {
[key: string]: any
}
@@ -29,7 +29,7 @@ export function issueCommand(
process.stdout.write(cmd.toString() + os.EOL)
}
export function issue(name: string, message: string = ''): void {
export function issue(name: string, message = ''): void {
issueCommand(name, {}, message)
}
+120 -7
View File
@@ -1,16 +1,21 @@
import {issue, issueCommand} from './command'
import {issueCommand as issueFileCommand} from './file-command'
import {toCommandValue} from './utils'
import {toCommandProperties, toCommandValue} from './utils'
import * as os from 'os'
import * as path from 'path'
import {OidcClient} from './oidc-utils'
/**
* Interface for getInput options
*/
export interface InputOptions {
/** Optional. Whether the input is required. If required and not present, will throw. Defaults to false */
required?: boolean
/** Optional. Whether leading/trailing whitespace will be trimmed for the input. Defaults to true */
trimWhitespace?: boolean
}
/**
@@ -28,6 +33,43 @@ export enum ExitCode {
Failure = 1
}
/**
* Optional properties that can be sent with annotatation commands (notice, error, and warning)
* See: https://docs.github.com/en/rest/reference/checks#create-a-check-run for more information about annotations.
*/
export interface AnnotationProperties {
/**
* A title for the annotation.
*/
title?: string
/**
* The path of the file for which the annotation should be created.
*/
file?: string
/**
* The start line for the annotation.
*/
startLine?: number
/**
* The end line for the annotation. Defaults to `startLine` when `startLine` is provided.
*/
endLine?: number
/**
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
*/
startColumn?: number
/**
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
* Defaults to `startColumn` when `startColumn` is provided.
*/
endColumn?: number
}
//-----------------------------------------------------------------------
// Variables
//-----------------------------------------------------------------------
@@ -75,7 +117,9 @@ export function addPath(inputPath: string): void {
}
/**
* Gets the value of an input. The value is also trimmed.
* Gets the value of an input.
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
* Returns an empty string if the value is not defined.
*
* @param name name of the input to get
* @param options optional. See InputOptions.
@@ -88,9 +132,32 @@ export function getInput(name: string, options?: InputOptions): string {
throw new Error(`Input required and not supplied: ${name}`)
}
if (options && options.trimWhitespace === false) {
return val
}
return val.trim()
}
/**
* Gets the values of an multiline input. Each value is also trimmed.
*
* @param name name of the input to get
* @param options optional. See InputOptions.
* @returns string[]
*
*/
export function getMultilineInput(
name: string,
options?: InputOptions
): string[] {
const inputs: string[] = getInput(name, options)
.split('\n')
.filter(x => x !== '')
return inputs
}
/**
* Gets the input value of the boolean type in the YAML 1.2 "core schema" specification.
* Support boolean input list: `true | True | TRUE | false | False | FALSE` .
@@ -171,17 +238,49 @@ export function debug(message: string): void {
/**
* Adds an error issue
* @param message error issue message. Errors will be converted to string via toString()
* @param properties optional properties to add to the annotation.
*/
export function error(message: string | Error): void {
issue('error', message instanceof Error ? message.toString() : message)
export function error(
message: string | Error,
properties: AnnotationProperties = {}
): void {
issueCommand(
'error',
toCommandProperties(properties),
message instanceof Error ? message.toString() : message
)
}
/**
* Adds an warning issue
* Adds a warning issue
* @param message warning issue message. Errors will be converted to string via toString()
* @param properties optional properties to add to the annotation.
*/
export function warning(message: string | Error): void {
issue('warning', message instanceof Error ? message.toString() : message)
export function warning(
message: string | Error,
properties: AnnotationProperties = {}
): void {
issueCommand(
'warning',
toCommandProperties(properties),
message instanceof Error ? message.toString() : message
)
}
/**
* Adds a notice issue
* @param message notice issue message. Errors will be converted to string via toString()
* @param properties optional properties to add to the annotation.
*/
export function notice(
message: string | Error,
properties: AnnotationProperties = {}
): void {
issueCommand(
'notice',
toCommandProperties(properties),
message instanceof Error ? message.toString() : message
)
}
/**
@@ -256,3 +355,17 @@ export function saveState(name: string, value: any): void {
export function getState(name: string): string {
return process.env[`STATE_${name}`] || ''
}
export async function getIDToken(aud?: string): Promise<string> {
return await OidcClient.getIDToken(aud)
}
/**
* Summary exports
*/
export {summary} from './summary'
/**
* @deprecated use core.summary
*/
export {markdownSummary} from './summary'
+84
View File
@@ -0,0 +1,84 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
import * as actions_http_client from '@actions/http-client'
import {RequestOptions} from '@actions/http-client/lib/interfaces'
import {HttpClient} from '@actions/http-client'
import {BearerCredentialHandler} from '@actions/http-client/lib/auth'
import {debug, setSecret} from './core'
interface TokenResponse {
value?: string
}
export class OidcClient {
private static createHttpClient(
allowRetry = true,
maxRetry = 10
): actions_http_client.HttpClient {
const requestOptions: RequestOptions = {
allowRetries: allowRetry,
maxRetries: maxRetry
}
return new HttpClient(
'actions/oidc-client',
[new BearerCredentialHandler(OidcClient.getRequestToken())],
requestOptions
)
}
private static getRequestToken(): string {
const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']
if (!token) {
throw new Error(
'Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable'
)
}
return token
}
private static getIDTokenUrl(): string {
const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL']
if (!runtimeUrl) {
throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable')
}
return runtimeUrl
}
private static async getCall(id_token_url: string): Promise<string> {
const httpclient = OidcClient.createHttpClient()
const res = await httpclient
.getJson<TokenResponse>(id_token_url)
.catch(error => {
throw new Error(
`Failed to get ID Token. \n
Error Code : ${error.statusCode}\n
Error Message: ${error.result.message}`
)
})
const id_token = res.result?.value
if (!id_token) {
throw new Error('Response json body do not have ID Token field')
}
return id_token
}
static async getIDToken(audience?: string): Promise<string> {
try {
// New ID Token is requested from action service
let id_token_url: string = OidcClient.getIDTokenUrl()
if (audience) {
const encodedAudience = encodeURIComponent(audience)
id_token_url = `${id_token_url}&audience=${encodedAudience}`
}
debug(`ID token url is ${id_token_url}`)
const id_token = await OidcClient.getCall(id_token_url)
setSecret(id_token)
return id_token
} catch (error) {
throw new Error(`Error message: ${error.message}`)
}
}
}
+363
View File
@@ -0,0 +1,363 @@
import {EOL} from 'os'
import {constants, promises} from 'fs'
const {access, appendFile, writeFile} = promises
export const SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'
export const SUMMARY_DOCS_URL =
'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary'
export type SummaryTableRow = (SummaryTableCell | string)[]
export interface SummaryTableCell {
/**
* Cell content
*/
data: string
/**
* Render cell as header
* (optional) default: false
*/
header?: boolean
/**
* Number of columns the cell extends
* (optional) default: '1'
*/
colspan?: string
/**
* Number of rows the cell extends
* (optional) default: '1'
*/
rowspan?: string
}
export interface SummaryImageOptions {
/**
* The width of the image in pixels. Must be an integer without a unit.
* (optional)
*/
width?: string
/**
* The height of the image in pixels. Must be an integer without a unit.
* (optional)
*/
height?: string
}
export interface SummaryWriteOptions {
/**
* Replace all existing content in summary file with buffer contents
* (optional) default: false
*/
overwrite?: boolean
}
class Summary {
private _buffer: string
private _filePath?: string
constructor() {
this._buffer = ''
}
/**
* Finds the summary file path from the environment, rejects if env var is not found or file does not exist
* Also checks r/w permissions.
*
* @returns step summary file path
*/
private async filePath(): Promise<string> {
if (this._filePath) {
return this._filePath
}
const pathFromEnv = process.env[SUMMARY_ENV_VAR]
if (!pathFromEnv) {
throw new Error(
`Unable to find environment variable for $${SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`
)
}
try {
await access(pathFromEnv, constants.R_OK | constants.W_OK)
} catch {
throw new Error(
`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`
)
}
this._filePath = pathFromEnv
return this._filePath
}
/**
* Wraps content in an HTML tag, adding any HTML attributes
*
* @param {string} tag HTML tag to wrap
* @param {string | null} content content within the tag
* @param {[attribute: string]: string} attrs key-value list of HTML attributes to add
*
* @returns {string} content wrapped in HTML element
*/
private wrap(
tag: string,
content: string | null,
attrs: {[attribute: string]: string} = {}
): string {
const htmlAttrs = Object.entries(attrs)
.map(([key, value]) => ` ${key}="${value}"`)
.join('')
if (!content) {
return `<${tag}${htmlAttrs}>`
}
return `<${tag}${htmlAttrs}>${content}</${tag}>`
}
/**
* Writes text in the buffer to the summary buffer file and empties buffer. Will append by default.
*
* @param {SummaryWriteOptions} [options] (optional) options for write operation
*
* @returns {Promise<Summary>} summary instance
*/
async write(options?: SummaryWriteOptions): Promise<Summary> {
const overwrite = !!options?.overwrite
const filePath = await this.filePath()
const writeFunc = overwrite ? writeFile : appendFile
await writeFunc(filePath, this._buffer, {encoding: 'utf8'})
return this.emptyBuffer()
}
/**
* Clears the summary buffer and wipes the summary file
*
* @returns {Summary} summary instance
*/
async clear(): Promise<Summary> {
return this.emptyBuffer().write({overwrite: true})
}
/**
* Returns the current summary buffer as a string
*
* @returns {string} string of summary buffer
*/
stringify(): string {
return this._buffer
}
/**
* If the summary buffer is empty
*
* @returns {boolen} true if the buffer is empty
*/
isEmptyBuffer(): boolean {
return this._buffer.length === 0
}
/**
* Resets the summary buffer without writing to summary file
*
* @returns {Summary} summary instance
*/
emptyBuffer(): Summary {
this._buffer = ''
return this
}
/**
* Adds raw text to the summary buffer
*
* @param {string} text content to add
* @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false)
*
* @returns {Summary} summary instance
*/
addRaw(text: string, addEOL = false): Summary {
this._buffer += text
return addEOL ? this.addEOL() : this
}
/**
* Adds the operating system-specific end-of-line marker to the buffer
*
* @returns {Summary} summary instance
*/
addEOL(): Summary {
return this.addRaw(EOL)
}
/**
* Adds an HTML codeblock to the summary buffer
*
* @param {string} code content to render within fenced code block
* @param {string} lang (optional) language to syntax highlight code
*
* @returns {Summary} summary instance
*/
addCodeBlock(code: string, lang?: string): Summary {
const attrs = {
...(lang && {lang})
}
const element = this.wrap('pre', this.wrap('code', code), attrs)
return this.addRaw(element).addEOL()
}
/**
* Adds an HTML list to the summary buffer
*
* @param {string[]} items list of items to render
* @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false)
*
* @returns {Summary} summary instance
*/
addList(items: string[], ordered = false): Summary {
const tag = ordered ? 'ol' : 'ul'
const listItems = items.map(item => this.wrap('li', item)).join('')
const element = this.wrap(tag, listItems)
return this.addRaw(element).addEOL()
}
/**
* Adds an HTML table to the summary buffer
*
* @param {SummaryTableCell[]} rows table rows
*
* @returns {Summary} summary instance
*/
addTable(rows: SummaryTableRow[]): Summary {
const tableBody = rows
.map(row => {
const cells = row
.map(cell => {
if (typeof cell === 'string') {
return this.wrap('td', cell)
}
const {header, data, colspan, rowspan} = cell
const tag = header ? 'th' : 'td'
const attrs = {
...(colspan && {colspan}),
...(rowspan && {rowspan})
}
return this.wrap(tag, data, attrs)
})
.join('')
return this.wrap('tr', cells)
})
.join('')
const element = this.wrap('table', tableBody)
return this.addRaw(element).addEOL()
}
/**
* Adds a collapsable HTML details element to the summary buffer
*
* @param {string} label text for the closed state
* @param {string} content collapsable content
*
* @returns {Summary} summary instance
*/
addDetails(label: string, content: string): Summary {
const element = this.wrap('details', this.wrap('summary', label) + content)
return this.addRaw(element).addEOL()
}
/**
* Adds an HTML image tag to the summary buffer
*
* @param {string} src path to the image you to embed
* @param {string} alt text description of the image
* @param {SummaryImageOptions} options (optional) addition image attributes
*
* @returns {Summary} summary instance
*/
addImage(src: string, alt: string, options?: SummaryImageOptions): Summary {
const {width, height} = options || {}
const attrs = {
...(width && {width}),
...(height && {height})
}
const element = this.wrap('img', null, {src, alt, ...attrs})
return this.addRaw(element).addEOL()
}
/**
* Adds an HTML section heading element
*
* @param {string} text heading text
* @param {number | string} [level=1] (optional) the heading level, default: 1
*
* @returns {Summary} summary instance
*/
addHeading(text: string, level?: number | string): Summary {
const tag = `h${level}`
const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)
? tag
: 'h1'
const element = this.wrap(allowedTag, text)
return this.addRaw(element).addEOL()
}
/**
* Adds an HTML thematic break (<hr>) to the summary buffer
*
* @returns {Summary} summary instance
*/
addSeparator(): Summary {
const element = this.wrap('hr', null)
return this.addRaw(element).addEOL()
}
/**
* Adds an HTML line break (<br>) to the summary buffer
*
* @returns {Summary} summary instance
*/
addBreak(): Summary {
const element = this.wrap('br', null)
return this.addRaw(element).addEOL()
}
/**
* Adds an HTML blockquote to the summary buffer
*
* @param {string} text quote text
* @param {string} cite (optional) citation url
*
* @returns {Summary} summary instance
*/
addQuote(text: string, cite?: string): Summary {
const attrs = {
...(cite && {cite})
}
const element = this.wrap('blockquote', text, attrs)
return this.addRaw(element).addEOL()
}
/**
* Adds an HTML anchor tag to the summary buffer
*
* @param {string} text link text/content
* @param {string} href hyperlink
*
* @returns {Summary} summary instance
*/
addLink(text: string, href: string): Summary {
const element = this.wrap('a', text, {href})
return this.addRaw(element).addEOL()
}
}
const _summary = new Summary()
/**
* @deprecated use `core.summary`
*/
export const markdownSummary = _summary
export const summary = _summary
+26
View File
@@ -1,6 +1,9 @@
// We use any as a valid input type
/* eslint-disable @typescript-eslint/no-explicit-any */
import {AnnotationProperties} from './core'
import {CommandProperties} from './command'
/**
* Sanitizes an input into a string so it can be passed into issueCommand safely
* @param input input to sanitize into a string
@@ -13,3 +16,26 @@ export function toCommandValue(input: any): string {
}
return JSON.stringify(input)
}
/**
*
* @param annotationProperties
* @returns The command properties to send with the actual annotation command
* See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646
*/
export function toCommandProperties(
annotationProperties: AnnotationProperties
): CommandProperties {
if (!Object.keys(annotationProperties).length) {
return {}
}
return {
title: annotationProperties.title,
file: annotationProperties.file,
line: annotationProperties.startLine,
endLine: annotationProperties.endLine,
col: annotationProperties.startColumn,
endColumn: annotationProperties.endColumn
}
}
+9
View File
@@ -1,5 +1,14 @@
# @actions/exec Releases
### 1.1.1
- Update `lockfileVersion` to `v2` in `package-lock.json [#1024](https://github.com/actions/toolkit/pull/1024)
### 1.1.0
- [Fix stdline dropping large output](https://github.com/actions/toolkit/pull/773)
- [Add getExecOutput function](https://github.com/actions/toolkit/pull/814)
- [Better error for bad cwd](https://github.com/actions/toolkit/pull/793)
### 1.0.3
- [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221)
+197 -6
View File
@@ -1,5 +1,4 @@
import * as exec from '../src/exec'
import * as tr from '../src/toolrunner'
import * as im from '../src/interfaces'
import * as childProcess from 'child_process'
@@ -287,6 +286,31 @@ describe('@actions/exec', () => {
expect(stderrCalled).toBeTruthy()
})
it('Handles large stdline', async () => {
const stdlinePath: string = path.join(
__dirname,
'scripts',
'stdlineoutput.js'
)
const nodePath: string = await io.which('node', true)
const _testExecOptions = getExecOptions()
let largeLine = ''
_testExecOptions.listeners = {
stdline: (line: string) => {
largeLine = line
}
}
const exitCode = await exec.exec(
`"${nodePath}"`,
[stdlinePath],
_testExecOptions
)
expect(exitCode).toBe(0)
expect(Buffer.byteLength(largeLine)).toEqual(2 ** 16 + 1)
})
it('Handles stdin shell', async () => {
let command: string
if (IS_WINDOWS) {
@@ -539,6 +563,22 @@ describe('@actions/exec', () => {
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
})
it('Exec roots throws friendly error when bad cwd is specified', async () => {
const execOptions = getExecOptions()
execOptions.cwd = 'nonexistent/path'
await expect(exec.exec('ls', ['-all'], execOptions)).rejects.toThrowError(
`The cwd: ${execOptions.cwd} does not exist!`
)
})
it('Exec roots does not throw when valid cwd is provided', async () => {
const execOptions = getExecOptions()
execOptions.cwd = './'
await expect(exec.exec('ls', ['-all'], execOptions)).resolves.toBe(0)
})
it('Exec roots relative tool path using rooted options.cwd', async () => {
let command: string
if (IS_WINDOWS) {
@@ -621,12 +661,163 @@ describe('@actions/exec', () => {
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
})
it('tool runner strips INPUT_ params from environment for child process', () => {
const env = {INPUT_TEST: 'input value', SOME_OTHER_ENV: 'some other value'}
const sanitizedEnv = tr.stripInputEnvironmentVariables(env)
it('correctly outputs for getExecOutput', async () => {
const stdErrPath: string = path.join(
__dirname,
'scripts',
'stderroutput.js'
)
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutoutput.js'
)
const nodePath: string = await io.which('node', true)
expect(sanitizedEnv).not.toHaveProperty('INPUT_TEST')
expect(sanitizedEnv).toHaveProperty('SOME_OTHER_ENV')
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
`"${nodePath}"`,
[stdOutPath],
getExecOptions()
)
expect(exitCodeOut).toBe(0)
expect(stdout).toBe('this is output to stdout')
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
`"${nodePath}"`,
[stdErrPath],
getExecOptions()
)
expect(exitCodeErr).toBe(0)
expect(stderr).toBe('this is output to stderr')
})
it('correctly outputs for getExecOutput with additional listeners', async () => {
const stdErrPath: string = path.join(
__dirname,
'scripts',
'stderroutput.js'
)
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutoutput.js'
)
const nodePath: string = await io.which('node', true)
let listenerOut = ''
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
`"${nodePath}"`,
[stdOutPath],
{
...getExecOptions(),
listeners: {
stdout: data => {
listenerOut = data.toString()
}
}
}
)
expect(exitCodeOut).toBe(0)
expect(stdout).toBe('this is output to stdout')
expect(listenerOut).toBe('this is output to stdout')
let listenerErr = ''
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
`"${nodePath}"`,
[stdErrPath],
{
...getExecOptions(),
listeners: {
stderr: data => {
listenerErr = data.toString()
}
}
}
)
expect(exitCodeErr).toBe(0)
expect(stderr).toBe('this is output to stderr')
expect(listenerErr).toBe('this is output to stderr')
})
it('correctly outputs for getExecOutput when total size exceeds buffer size', async () => {
const stdErrPath: string = path.join(
__dirname,
'scripts',
'stderroutput.js'
)
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutoutputlarge.js'
)
const nodePath: string = await io.which('node', true)
let listenerOut = ''
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
`"${nodePath}"`,
[stdOutPath],
{
...getExecOptions(),
listeners: {
stdout: data => {
listenerOut += data.toString()
}
}
}
)
expect(exitCodeOut).toBe(0)
expect(Buffer.byteLength(stdout || '', 'utf8')).toBe(2 ** 25)
expect(Buffer.byteLength(listenerOut, 'utf8')).toBe(2 ** 25)
let listenerErr = ''
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
`"${nodePath}"`,
[stdErrPath],
{
...getExecOptions(),
listeners: {
stderr: data => {
listenerErr = data.toString()
}
}
}
)
expect(exitCodeErr).toBe(0)
expect(stderr).toBe('this is output to stderr')
expect(listenerErr).toBe('this is output to stderr')
})
it('correctly outputs for getExecOutput with multi-byte characters', async () => {
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutputspecial.js'
)
const nodePath: string = await io.which('node', true)
let numStdOutBufferCalls = 0
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
`"${nodePath}"`,
[stdOutPath],
{
...getExecOptions(),
listeners: {
stdout: () => {
numStdOutBufferCalls += 1
}
}
}
)
expect(exitCodeOut).toBe(0)
//one call for each half of the © character, ensuring it was actually split and not sent together
expect(numStdOutBufferCalls).toBe(2)
expect(stdout).toBe('©')
})
if (IS_WINDOWS) {
@@ -0,0 +1,5 @@
//Default highWaterMark for readable stream buffers us 64K (2^16)
//so we go over that to get more than a buffer's worth
const os = require('os')
process.stdout.write('a'.repeat(2**16 + 1) + os.EOL);
@@ -0,0 +1,3 @@
//Default highWaterMark for readable stream buffers us 64K (2^16)
//so we go over that to get more than a buffer's worth
process.stdout.write('a\n'.repeat(2**24));
@@ -0,0 +1,8 @@
//first half of © character
process.stdout.write(Buffer.from([0xC2]), (err) => {
//write in the callback so that the second byte is sent separately
setTimeout(() => {
process.stdout.write(Buffer.from([0xA9])) //second half of © character
}, 5000)
})
+17 -2
View File
@@ -1,8 +1,23 @@
{
"name": "@actions/exec",
"version": "1.0.4",
"lockfileVersion": 1,
"version": "1.1.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@actions/exec",
"version": "1.1.1",
"license": "MIT",
"dependencies": {
"@actions/io": "^1.0.1"
}
},
"node_modules/@actions/io": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.1.tgz",
"integrity": "sha512-rhq+tfZukbtaus7xyUtwKfuiCRXd1hWSfmJNEpFgBQJ4woqPEpsBw04awicjwz9tyG2/MVhAEMfVn664Cri5zA=="
}
},
"dependencies": {
"@actions/io": {
"version": "1.0.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/exec",
"version": "1.0.4",
"version": "1.1.1",
"description": "Actions exec lib",
"keywords": [
"github",
+62 -2
View File
@@ -1,7 +1,8 @@
import {ExecOptions} from './interfaces'
import {StringDecoder} from 'string_decoder'
import {ExecOptions, ExecOutput, ExecListeners} from './interfaces'
import * as tr from './toolrunner'
export {ExecOptions}
export {ExecOptions, ExecOutput, ExecListeners}
/**
* Exec a command.
@@ -28,3 +29,62 @@ export async function exec(
const runner: tr.ToolRunner = new tr.ToolRunner(toolPath, args, options)
return runner.exec()
}
/**
* Exec a command and get the output.
* Output will be streamed to the live console.
* Returns promise with the exit code and collected stdout and stderr
*
* @param commandLine command to execute (can include additional args). Must be correctly escaped.
* @param args optional arguments for tool. Escaping is handled by the lib.
* @param options optional exec options. See ExecOptions
* @returns Promise<ExecOutput> exit code, stdout, and stderr
*/
export async function getExecOutput(
commandLine: string,
args?: string[],
options?: ExecOptions
): Promise<ExecOutput> {
let stdout = ''
let stderr = ''
//Using string decoder covers the case where a mult-byte character is split
const stdoutDecoder = new StringDecoder('utf8')
const stderrDecoder = new StringDecoder('utf8')
const originalStdoutListener = options?.listeners?.stdout
const originalStdErrListener = options?.listeners?.stderr
const stdErrListener = (data: Buffer): void => {
stderr += stderrDecoder.write(data)
if (originalStdErrListener) {
originalStdErrListener(data)
}
}
const stdOutListener = (data: Buffer): void => {
stdout += stdoutDecoder.write(data)
if (originalStdoutListener) {
originalStdoutListener(data)
}
}
const listeners: ExecListeners = {
...options?.listeners,
stdout: stdOutListener,
stderr: stdErrListener
}
const exitCode = await exec(commandLine, args, {...options, listeners})
//flush any remaining characters
stdout += stdoutDecoder.end()
stderr += stderrDecoder.end()
return {
exitCode,
stdout,
stderr
}
}
+36 -12
View File
@@ -6,7 +6,7 @@ export interface ExecOptions {
/** optional working directory. defaults to current */
cwd?: string
/** optional envvar dictionary. defaults to current process's env with `INPUT_*` variables removed */
/** optional envvar dictionary. defaults to current process's env */
env?: {[key: string]: string}
/** optional. defaults to false */
@@ -34,15 +34,39 @@ export interface ExecOptions {
input?: Buffer
/** optional. Listeners for output. Callback functions that will be called on these events */
listeners?: {
stdout?: (data: Buffer) => void
stderr?: (data: Buffer) => void
stdline?: (data: string) => void
errline?: (data: string) => void
debug?: (data: string) => void
}
listeners?: ExecListeners
}
/**
* Interface for the output of getExecOutput()
*/
export interface ExecOutput {
/**The exit code of the process */
exitCode: number
/**The entire stdout of the process as a string */
stdout: string
/**The entire stderr of the process as a string */
stderr: string
}
/**
* The user defined listeners for an exec call
*/
export interface ExecListeners {
/** A call back for each buffer of stdout */
stdout?: (data: Buffer) => void
/** A call back for each buffer of stderr */
stderr?: (data: Buffer) => void
/** A call back for each line of stdout */
stdline?: (data: string) => void
/** A call back for each line of stderr */
errline?: (data: string) => void
/** A call back for each debug log */
debug?: (data: string) => void
}
+34 -34
View File
@@ -84,7 +84,7 @@ export class ToolRunner extends events.EventEmitter {
data: Buffer,
strBuffer: string,
onLine: (line: string) => void
): void {
): string {
try {
let s = strBuffer + data.toString()
let n = s.indexOf(os.EOL)
@@ -98,10 +98,12 @@ export class ToolRunner extends events.EventEmitter {
n = s.indexOf(os.EOL)
}
strBuffer = s
return s
} catch (err) {
// streaming lines to console is best effort. Don't fail a build.
this._debug(`error processing line. Failed with error ${err}`)
return ''
}
}
@@ -377,7 +379,7 @@ export class ToolRunner extends events.EventEmitter {
options = options || <im.ExecOptions>{}
const result = <child.SpawnOptions>{}
result.cwd = options.cwd
result.env = options.env || stripInputEnvironmentVariables(process.env)
result.env = options.env
result['windowsVerbatimArguments'] =
options.windowsVerbatimArguments || this._isCmdFile()
if (options.windowsVerbatimArguments) {
@@ -414,7 +416,7 @@ export class ToolRunner extends events.EventEmitter {
// otherwise verify it exists (add extension on Windows if necessary)
this.toolPath = await io.which(this.toolPath, true)
return new Promise<number>((resolve, reject) => {
return new Promise<number>(async (resolve, reject) => {
this._debug(`exec tool: ${this.toolPath}`)
this._debug('arguments:')
for (const arg of this.args) {
@@ -433,6 +435,10 @@ export class ToolRunner extends events.EventEmitter {
this._debug(message)
})
if (this.options.cwd && !(await ioUtil.exists(this.options.cwd))) {
return reject(new Error(`The cwd: ${this.options.cwd} does not exist!`))
}
const fileName = this._getSpawnFileName()
const cp = child.spawn(
fileName,
@@ -440,7 +446,7 @@ export class ToolRunner extends events.EventEmitter {
this._getSpawnOptions(this.options, fileName)
)
const stdbuffer = ''
let stdbuffer = ''
if (cp.stdout) {
cp.stdout.on('data', (data: Buffer) => {
if (this.options.listeners && this.options.listeners.stdout) {
@@ -451,15 +457,19 @@ export class ToolRunner extends events.EventEmitter {
optionsNonNull.outStream.write(data)
}
this._processLineBuffer(data, stdbuffer, (line: string) => {
if (this.options.listeners && this.options.listeners.stdline) {
this.options.listeners.stdline(line)
stdbuffer = this._processLineBuffer(
data,
stdbuffer,
(line: string) => {
if (this.options.listeners && this.options.listeners.stdline) {
this.options.listeners.stdline(line)
}
}
})
)
})
}
const errbuffer = ''
let errbuffer = ''
if (cp.stderr) {
cp.stderr.on('data', (data: Buffer) => {
state.processStderr = true
@@ -478,11 +488,15 @@ export class ToolRunner extends events.EventEmitter {
s.write(data)
}
this._processLineBuffer(data, errbuffer, (line: string) => {
if (this.options.listeners && this.options.listeners.errline) {
this.options.listeners.errline(line)
errbuffer = this._processLineBuffer(
data,
errbuffer,
(line: string) => {
if (this.options.listeners && this.options.listeners.errline) {
this.options.listeners.errline(line)
}
}
})
)
})
}
@@ -600,20 +614,6 @@ export function argStringToArray(argString: string): string[] {
return args
}
// Strips INPUT_ environment variables to prevent them leaking to child processes
export function stripInputEnvironmentVariables(
env: NodeJS.ProcessEnv
): NodeJS.ProcessEnv {
return Object.entries(env)
.filter(([key]) => {
return !key.startsWith('INPUT_')
})
.reduce((obj: NodeJS.ProcessEnv, [key, value]) => {
obj[key] = value
return obj
}, {})
}
class ExecState extends events.EventEmitter {
constructor(options: im.ExecOptions, toolPath: string) {
super()
@@ -629,13 +629,13 @@ class ExecState extends events.EventEmitter {
}
}
processClosed: boolean = false // tracks whether the process has exited and stdio is closed
processError: string = ''
processExitCode: number = 0
processExited: boolean = false // tracks whether the process has exited
processStderr: boolean = false // tracks whether stderr was written to
processClosed = false // tracks whether the process has exited and stdio is closed
processError = ''
processExitCode = 0
processExited = false // tracks whether the process has exited
processStderr = false // tracks whether stderr was written to
private delay = 10000 // 10 seconds
private done: boolean = false
private done = false
private options: im.ExecOptions
private timeout: NodeJS.Timer | null = null
private toolPath: string
+6 -5
View File
@@ -59,18 +59,19 @@ const newIssue = await octokit.rest.issues.create({
## Webhook payload typescript definitions
The npm module `@octokit/webhooks` provides type definitions for the response payloads. You can cast the payload to these types for better type information.
The npm module `@octokit/webhooks-definitions` provides type definitions for the response payloads. You can cast the payload to these types for better type information.
First, install the npm module `npm install @octokit/webhooks`
First, install the npm module `npm install @octokit/webhooks-definitions`
Then, assert the type based on the eventName
```ts
import * as core from '@actions/core'
import * as github from '@actions/github'
import * as Webhooks from '@octokit/webhooks'
import {PushEvent} from '@octokit/webhooks-definitions/schema'
if (github.context.eventName === 'push') {
const pushPayload = github.context.payload as Webhooks.WebhookPayloadPush
core.info(`The head commit is: ${pushPayload.head}`)
const pushPayload = github.context.payload as PushEvent
core.info(`The head commit is: ${pushPayload.head_commit}`)
}
```
+9
View File
@@ -1,7 +1,16 @@
# @actions/github Releases
### 5.0.3
- - Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
### 5.0.2
- Update to v2.0.0 of `@actions/http-client`
### 5.0.1
- [Update Octokit Dependencies](https://github.com/actions/toolkit/pull/1037)
### 5.0.0
- [Update @actions/github to include latest octokit definitions](https://github.com/actions/toolkit/pull/783)
- [Add urls to context](https://github.com/actions/toolkit/pull/794)
### 4.0.0
- [Add execution state information to context](https://github.com/actions/toolkit/pull/499)
+1
View File
@@ -2,6 +2,7 @@ import * as path from 'path'
import {Context} from '../src/context'
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-var-requires */
describe('@actions/context', () => {
let context: Context
-1
View File
@@ -3,7 +3,6 @@ module.exports = {
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
+490 -4655
View File
File diff suppressed because it is too large Load Diff
+6 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/github",
"version": "5.0.0",
"version": "5.0.3",
"description": "Actions github lib",
"keywords": [
"github",
@@ -38,13 +38,12 @@
"url": "https://github.com/actions/toolkit/issues"
},
"dependencies": {
"@actions/http-client": "^1.0.11",
"@octokit/core": "^3.4.0",
"@octokit/plugin-paginate-rest": "^2.13.3",
"@octokit/plugin-rest-endpoint-methods": "^5.1.1"
"@actions/http-client": "^2.0.1",
"@octokit/core": "^3.6.0",
"@octokit/plugin-paginate-rest": "^2.17.0",
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
},
"devDependencies": {
"jest": "^25.1.0",
"proxy": "^1.0.1"
"proxy": "^1.0.2"
}
}
+7
View File
@@ -18,6 +18,9 @@ export class Context {
job: string
runNumber: number
runId: number
apiUrl: string
serverUrl: string
graphqlUrl: string
/**
* Hydrate the context from the environment
@@ -43,6 +46,10 @@ export class Context {
this.job = process.env.GITHUB_JOB as string
this.runNumber = parseInt(process.env.GITHUB_RUN_NUMBER as string, 10)
this.runId = parseInt(process.env.GITHUB_RUN_ID as string, 10)
this.apiUrl = process.env.GITHUB_API_URL ?? `https://api.github.com`
this.serverUrl = process.env.GITHUB_SERVER_URL ?? `https://github.com`
this.graphqlUrl =
process.env.GITHUB_GRAPHQL_URL ?? `https://api.github.com/graphql`
}
get issue(): {owner: string; repo: string; number: number} {
+17 -3
View File
@@ -1,9 +1,23 @@
# @actions/glob Releases
### 0.3.0
- Added a `verbose` option to HashFiles [#1052](https://github.com/actions/toolkit/pull/1052/files)
### 0.2.1
- Update `lockfileVersion` to `v2` in `package-lock.json [#1023](https://github.com/actions/toolkit/pull/1023)
### 0.2.0
- [Added the hashFiles function to Glob](https://github.com/actions/toolkit/pull/830)
- [Added an option to filter out directories](https://github.com/actions/toolkit/pull/728)
### 0.1.2
- [Fix bug where files were matched incorrectly](https://github.com/actions/toolkit/pull/805)
### 0.1.1
- Update @actions/core version
### 0.1.0
- Initial release
### 0.1.1
- Update @actions/core version
+126
View File
@@ -0,0 +1,126 @@
import * as io from '../../io/src/io'
import * as path from 'path'
import {hashFiles} from '../src/glob'
import {promises as fs} from 'fs'
const IS_WINDOWS = process.platform === 'win32'
/**
* These test focus on the ability of globber to find files
* and not on the pattern matching aspect
*/
describe('globber', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('basic hashfiles test', async () => {
const root = path.join(getTestTemp(), 'basic-hashfiles')
await fs.mkdir(path.join(root), {recursive: true})
await fs.writeFile(path.join(root, 'test.txt'), 'test file content')
const hash = await hashFiles(`${root}/*`)
expect(hash).toEqual(
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
)
})
it('basic hashfiles no match should return empty string', async () => {
const root = path.join(getTestTemp(), 'empty-hashfiles')
const hash = await hashFiles(`${root}/*`)
expect(hash).toEqual('')
})
it('followSymbolicLinks defaults to true', async () => {
const root = path.join(
getTestTemp(),
'defaults-to-follow-symbolic-links-true'
)
await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
await fs.writeFile(
path.join(root, 'realdir', 'file.txt'),
'test file content'
)
await createSymlinkDir(
path.join(root, 'realdir'),
path.join(root, 'symDir')
)
const testPath = path.join(root, `symDir`)
const hash = await hashFiles(testPath)
expect(hash).toEqual(
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
)
})
it('followSymbolicLinks set to true', async () => {
const root = path.join(getTestTemp(), 'set-to-true')
await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
await fs.writeFile(path.join(root, 'realdir', 'file'), 'test file content')
await createSymlinkDir(
path.join(root, 'realdir'),
path.join(root, 'symDir')
)
const testPath = path.join(root, `symDir`)
const hash = await hashFiles(testPath, {followSymbolicLinks: true})
expect(hash).toEqual(
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
)
})
it('followSymbolicLinks set to false', async () => {
// Create the following layout:
// <root>
// <root>/folder-a
// <root>/folder-a/file
// <root>/symDir -> <root>/folder-a
const root = path.join(getTestTemp(), 'set-to-false')
await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
await fs.writeFile(path.join(root, 'realdir', 'file'), 'test file content')
await createSymlinkDir(
path.join(root, 'realdir'),
path.join(root, 'symDir')
)
const testPath = path.join(root, 'symdir')
const hash = await hashFiles(testPath, {followSymbolicLinks: false})
expect(hash).toEqual('')
})
it('multipath test basic', async () => {
// Create the following layout:
// <root>
// <root>/folder-a
// <root>/folder-a/file
// <root>/symDir -> <root>/folder-a
const root = path.join(getTestTemp(), 'set-to-false')
await fs.mkdir(path.join(root, 'dir1'), {recursive: true})
await fs.mkdir(path.join(root, 'dir2'), {recursive: true})
await fs.writeFile(
path.join(root, 'dir1', 'testfile1.txt'),
'test file content'
)
await fs.writeFile(
path.join(root, 'dir2', 'testfile2.txt'),
'test file content'
)
const testPath = `${path.join(root, 'dir1')}\n${path.join(root, 'dir2')}`
const hash = await hashFiles(testPath)
expect(hash).toEqual(
'4e911ea5824830b6a3ec096c7833d5af8381c189ffaa825c3503a5333a73eadc'
)
})
})
function getTestTemp(): string {
return path.join(__dirname, '_temp', 'hash_files')
}
/**
* Creates a symlink directory on OSX/Linux, and a junction point directory on Windows.
* A symlink directory is not created on Windows since it requires an elevated context.
*/
async function createSymlinkDir(real: string, link: string): Promise<void> {
if (IS_WINDOWS) {
await fs.symlink(real, link, 'junction')
} else {
await fs.symlink(real, link)
}
}
@@ -97,6 +97,41 @@ describe('globber', () => {
])
})
it('defaults to matchDirectories=true', async () => {
// Create the following layout:
// <root>
// <root>/folder-a
// <root>/folder-a/file
const root = path.join(getTestTemp(), 'defaults-to-match-directories-true')
await fs.mkdir(path.join(root, 'folder-a'), {recursive: true})
await fs.writeFile(path.join(root, 'folder-a', 'file'), 'test file content')
const itemPaths = await glob(root, {})
expect(itemPaths).toEqual([
root,
path.join(root, 'folder-a'),
path.join(root, 'folder-a', 'file')
])
})
it('does not match file with trailing slash when implicitDescendants=true', async () => {
// Create the following layout:
// <root>
// <root>/file
const root = path.join(
getTestTemp(),
'defaults-to-implicit-descendants-true'
)
const filePath = path.join(root, 'file')
await fs.mkdir(root, {recursive: true})
await fs.writeFile(filePath, 'test file content')
const itemPaths = await glob(`${filePath}/`, {})
expect(itemPaths).toEqual([])
})
it('defaults to omitBrokenSymbolicLinks=true', async () => {
// Create the following layout:
// <root>
@@ -343,6 +378,34 @@ describe('globber', () => {
expect(itemPaths).toEqual([])
})
it('does not return directories when match directories false', async () => {
// Create the following layout:
// <root>/file-1
// <root>/dir-1
// <root>/dir-1/file-2
// <root>/dir-1/dir-2
// <root>/dir-1/dir-2/file-3
const root = path.join(
getTestTemp(),
'does-not-return-directories-when-match-directories-false'
)
await fs.mkdir(path.join(root, 'dir-1', 'dir-2'), {recursive: true})
await fs.writeFile(path.join(root, 'file-1'), '')
await fs.writeFile(path.join(root, 'dir-1', 'file-2'), '')
await fs.writeFile(path.join(root, 'dir-1', 'dir-2', 'file-3'), '')
const pattern = `${root}${path.sep}**`
expect(
await glob(pattern, {
matchDirectories: false
})
).toEqual([
path.join(root, 'dir-1', 'dir-2', 'file-3'),
path.join(root, 'dir-1', 'file-2'),
path.join(root, 'file-1')
])
})
it('does not search paths that are not partial matches', async () => {
// Create the following layout:
// <root>
@@ -26,7 +26,7 @@ describe('pattern', () => {
it('escapes homedir', async () => {
const home = path.join(getTestTemp(), 'home-with-[and]')
await fs.mkdir(home, {recursive: true})
const pattern = new Pattern('~/m*', undefined, home)
const pattern = new Pattern('~/m*', false, undefined, home)
expect(pattern.searchPath).toBe(home)
expect(pattern.match(path.join(home, 'match'))).toBeTruthy()
+119 -38
View File
@@ -1,40 +1,121 @@
{
"name": "@actions/glob",
"version": "0.1.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@actions/core": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "^1.1.7"
}
}
}
"name": "@actions/glob",
"version": "0.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@actions/glob",
"version": "0.3.0",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.2.6",
"minimatch": "^3.0.4"
}
},
"node_modules/@actions/core": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.0.tgz",
"integrity": "sha512-XirM+Zo/PFlA+1h+i4bkfvagujta+LIM2AOSzPbt8JqXbbuxb1HTB+FqIyaKmue9yiCx/JIJY6pXsOl3+T8JGw==",
"dependencies": {
"@actions/http-client": "^1.0.11"
}
},
"node_modules/@actions/http-client": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"dependencies": {
"tunnel": "0.0.6"
}
},
"node_modules/balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
"engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
}
}
},
"dependencies": {
"@actions/core": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.0.tgz",
"integrity": "sha512-XirM+Zo/PFlA+1h+i4bkfvagujta+LIM2AOSzPbt8JqXbbuxb1HTB+FqIyaKmue9yiCx/JIJY6pXsOl3+T8JGw==",
"requires": {
"@actions/http-client": "^1.0.11"
}
},
"@actions/http-client": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"requires": {
"tunnel": "0.0.6"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/glob",
"version": "0.1.1",
"version": "0.3.0",
"preview": true,
"description": "Actions glob lib",
"keywords": [
+21
View File
@@ -1,5 +1,7 @@
import {Globber, DefaultGlobber} from './internal-globber'
import {GlobOptions} from './internal-glob-options'
import {HashFileOptions} from './internal-hash-file-options'
import {hashFiles as _hashFiles} from './internal-hash-files'
export {Globber, GlobOptions}
@@ -15,3 +17,22 @@ export async function create(
): Promise<Globber> {
return await DefaultGlobber.create(patterns, options)
}
/**
* Computes the sha256 hash of a glob
*
* @param patterns Patterns separated by newlines
* @param options Glob options
*/
export async function hashFiles(
patterns: string,
options?: HashFileOptions,
verbose: Boolean = false
): Promise<string> {
let followSymbolicLinks = true
if (options && typeof options.followSymbolicLinks === 'boolean') {
followSymbolicLinks = options.followSymbolicLinks
}
const globber = await create(patterns, {followSymbolicLinks})
return _hashFiles(globber, verbose)
}
@@ -8,6 +8,7 @@ export function getOptions(copy?: GlobOptions): GlobOptions {
const result: GlobOptions = {
followSymbolicLinks: true,
implicitDescendants: true,
matchDirectories: true,
omitBrokenSymbolicLinks: true
}
@@ -22,6 +23,11 @@ export function getOptions(copy?: GlobOptions): GlobOptions {
core.debug(`implicitDescendants '${result.implicitDescendants}'`)
}
if (typeof copy.matchDirectories === 'boolean') {
result.matchDirectories = copy.matchDirectories
core.debug(`matchDirectories '${result.matchDirectories}'`)
}
if (typeof copy.omitBrokenSymbolicLinks === 'boolean') {
result.omitBrokenSymbolicLinks = copy.omitBrokenSymbolicLinks
core.debug(`omitBrokenSymbolicLinks '${result.omitBrokenSymbolicLinks}'`)
@@ -21,6 +21,14 @@ export interface GlobOptions {
*/
implicitDescendants?: boolean
/**
* Indicates whether matching directories should be included in the
* result set.
*
* @default true
*/
matchDirectories?: boolean
/**
* Indicates whether broken symbolic should be ignored and omitted from the
* result set. Otherwise an error will be thrown.
+4 -3
View File
@@ -66,7 +66,6 @@ export class DefaultGlobber implements Globber {
async *globGenerator(): AsyncGenerator<string, void> {
// Fill in defaults options
const options = globOptionsHelper.getOptions(this.options)
// Implicit descendants?
const patterns: Pattern[] = []
for (const pattern of this.patterns) {
@@ -77,12 +76,13 @@ export class DefaultGlobber implements Globber {
pattern.segments[pattern.segments.length - 1] !== '**')
) {
patterns.push(
new Pattern(pattern.negate, pattern.segments.concat('**'))
new Pattern(pattern.negate, true, pattern.segments.concat('**'))
)
}
}
// Push the search paths
const stack: SearchState[] = []
for (const searchPath of patternHelper.getSearchPaths(patterns)) {
core.debug(`Search path '${searchPath}'`)
@@ -131,7 +131,7 @@ export class DefaultGlobber implements Globber {
// Directory
if (stats.isDirectory()) {
// Matched
if (match & MatchKind.Directory) {
if (match & MatchKind.Directory && options.matchDirectories) {
yield item.path
}
// Descend?
@@ -180,6 +180,7 @@ export class DefaultGlobber implements Globber {
}
result.searchPaths.push(...patternHelper.getSearchPaths(result.patterns))
return result
}
@@ -0,0 +1,12 @@
/**
* Options to control globbing behavior
*/
export interface HashFileOptions {
/**
* Indicates whether to follow symbolic links. Generally should set to false
* when deleting files.
*
* @default true
*/
followSymbolicLinks?: boolean
}
+46
View File
@@ -0,0 +1,46 @@
import * as crypto from 'crypto'
import * as core from '@actions/core'
import * as fs from 'fs'
import * as stream from 'stream'
import * as util from 'util'
import * as path from 'path'
import {Globber} from './glob'
export async function hashFiles(
globber: Globber,
verbose: Boolean = false
): Promise<string> {
const writeDelegate = verbose ? core.info : core.debug
let hasMatch = false
const githubWorkspace = process.env['GITHUB_WORKSPACE'] ?? process.cwd()
const result = crypto.createHash('sha256')
let count = 0
for await (const file of globber.globGenerator()) {
writeDelegate(file)
if (!file.startsWith(`${githubWorkspace}${path.sep}`)) {
writeDelegate(`Ignore '${file}' since it is not under GITHUB_WORKSPACE.`)
continue
}
if (fs.statSync(file).isDirectory()) {
writeDelegate(`Skip directory '${file}'.`)
continue
}
const hash = crypto.createHash('sha256')
const pipeline = util.promisify(stream.pipeline)
await pipeline(fs.createReadStream(file), hash)
result.write(hash.digest())
count++
if (!hasMatch) {
hasMatch = true
}
}
result.end()
if (hasMatch) {
writeDelegate(`Found ${count} files to hash.`)
return result.digest('hex')
} else {
writeDelegate(`No matches found for glob`)
return ''
}
}
+20 -6
View File
@@ -43,15 +43,27 @@ export class Pattern {
*/
private readonly rootRegExp: RegExp
/* eslint-disable no-dupe-class-members */
// Disable no-dupe-class-members due to false positive for method overload
// https://github.com/typescript-eslint/typescript-eslint/issues/291
/**
* Indicates that the pattern is implicitly added as opposed to user specified.
*/
private readonly isImplicitPattern: boolean
constructor(pattern: string)
constructor(pattern: string, segments: undefined, homedir: string)
constructor(negate: boolean, segments: string[])
constructor(
pattern: string,
isImplicitPattern: boolean,
segments: undefined,
homedir: string
)
constructor(
negate: boolean,
isImplicitPattern: boolean,
segments: string[],
homedir?: string
)
constructor(
patternOrNegate: string | boolean,
isImplicitPattern = false,
segments?: string[],
homedir?: string
) {
@@ -107,6 +119,8 @@ export class Pattern {
IS_WINDOWS ? 'i' : ''
)
this.isImplicitPattern = isImplicitPattern
// Create minimatch
const minimatchOptions: IMinimatchOptions = {
dot: true,
@@ -132,7 +146,7 @@ export class Pattern {
// Append a trailing slash. Otherwise Minimatch will not match the directory immediately
// preceding the globstar. For example, given the pattern `/foo/**`, Minimatch returns
// false for `/foo` but returns true for `/foo/`. Append a trailing slash to handle that quirk.
if (!itemPath.endsWith(path.sep)) {
if (!itemPath.endsWith(path.sep) && this.isImplicitPattern === false) {
// Note, this is safe because the constructor ensures the pattern has an absolute root.
// For example, formats like C: and C:foo on Windows are resolved to an absolute root.
itemPath = `${itemPath}${path.sep}`
+2
View File
@@ -0,0 +1,2 @@
testoutput.txt
npm-debug.log
+21
View File
@@ -0,0 +1,21 @@
Actions Http Client for Node.js
Copyright (c) GitHub, Inc.
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+73
View File
@@ -0,0 +1,73 @@
# `@actions/http-client`
A lightweight HTTP client optimized for building actions.
## Features
- HTTP client with TypeScript generics and async/await/Promises
- Typings included!
- [Proxy support](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-self-hosted-runners#using-a-proxy-server-with-self-hosted-runners) just works with actions and the runner
- Targets ES2019 (runner runs actions with node 12+). Only supported on node 12+.
- Basic, Bearer and PAT Support out of the box. Extensible handlers for others.
- Redirects supported
Features and releases [here](./RELEASES.md)
## Install
```
npm install @actions/http-client --save
```
## Samples
See the [tests](./__tests__) for detailed examples.
## Errors
### HTTP
The HTTP client does not throw unless truly exceptional.
* A request that successfully executes resulting in a 404, 500 etc... will return a response object with a status code and a body.
* Redirects (3xx) will be followed by default.
See the [tests](./__tests__) for detailed examples.
## Debugging
To enable detailed console logging of all HTTP requests and responses, set the NODE_DEBUG environment varible:
```shell
export NODE_DEBUG=http
```
## Node support
The http-client is built using the latest LTS version of Node 12. It may work on previous node LTS versions but it's tested and officially supported on Node12+.
## Support and Versioning
We follow semver and will hold compatibility between major versions and increment the minor version with new features and capabilities (while holding compat).
## Contributing
We welcome PRs. Please create an issue and if applicable, a design before proceeding with code.
once:
```
npm install
```
To build:
```
npm run build
```
To run all tests:
```
npm test
```
+39
View File
@@ -0,0 +1,39 @@
## Releases
## 2.0.1
- Fix an issue with missing `tunnel` dependency [#1085](https://github.com/actions/toolkit/pull/1085)
## 2.0.0
- The package is now compiled with TypeScript's [`strict` compiler setting](https://www.typescriptlang.org/tsconfig#strict). To comply with stricter rules:
- Some exported types now include `| null` or `| undefined`, matching their actual behavior.
- Types implementing the method `RequestHandler.handleAuthentication()` now throw an `Error` rather than returning `null` if they do not support handling an HTTP 401 response. Callers can still use `canHandleAuthentication()` to determine if this handling is supported or not.
- Types using `any` have been scoped to more specific types.
- Following TypeScript's naming conventions, exported interfaces no longer begin with the prefix `I-`.
- Delete the `IHttpClientResponse` interface in favor of the `HttpClientResponse` class.
- Delete the `IHeaders` interface in favor of `http.OutgoingHttpHeaders`.
- The source code of the package was moved to build with [actions/toolkit](https://github.com/actions/toolkit).
## 1.0.11
Contains a bug fix where proxy is defined without a user and password. see [PR here](https://github.com/actions/http-client/pull/42)
## 1.0.9
Throw HttpClientError instead of a generic Error from the \<verb>Json() helper methods when the server responds with a non-successful status code.
## 1.0.8
Fixed security issue where a redirect (e.g. 302) to another domain would pass headers. The fix was to strip the authorization header if the hostname was different. More [details in PR #27](https://github.com/actions/http-client/pull/27)
## 1.0.7
Update NPM dependencies and add 429 to the list of HttpCodes
## 1.0.6
Automatically sends Content-Type and Accept application/json headers for \<verb>Json() helper methods if not set in the client or parameters.
## 1.0.5
Adds \<verb>Json() helper methods for json over http scenarios.
## 1.0.4
Started to add \<verb>Json() helper methods. Do not use this release for that. Use >= 1.0.5 since there was an issue with types.
## 1.0.1 to 1.0.3
Adds proxy support.
@@ -0,0 +1,73 @@
import * as httpm from '../lib'
import * as am from '../lib/auth'
describe('auth', () => {
beforeEach(() => {})
afterEach(() => {})
it('does basic http get request with basic auth', async () => {
const bh: am.BasicCredentialHandler = new am.BasicCredentialHandler(
'johndoe',
'password'
)
const http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [
bh
])
const res: httpm.HttpClientResponse = await http.get(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
const auth: string = obj.headers.Authorization
const creds: string = Buffer.from(
auth.substring('Basic '.length),
'base64'
).toString()
expect(creds).toBe('johndoe:password')
expect(obj.url).toBe('http://httpbin.org/get')
})
it('does basic http get request with pat token auth', async () => {
const token = 'scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs'
const ph: am.PersonalAccessTokenCredentialHandler = new am.PersonalAccessTokenCredentialHandler(
token
)
const http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [
ph
])
const res: httpm.HttpClientResponse = await http.get(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
const auth: string = obj.headers.Authorization
const creds: string = Buffer.from(
auth.substring('Basic '.length),
'base64'
).toString()
expect(creds).toBe(`PAT:${token}`)
expect(obj.url).toBe('http://httpbin.org/get')
})
it('does basic http get request with pat token auth', async () => {
const token = 'scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs'
const ph: am.BearerCredentialHandler = new am.BearerCredentialHandler(token)
const http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [
ph
])
const res: httpm.HttpClientResponse = await http.get(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
const auth: string = obj.headers.Authorization
expect(auth).toBe(`Bearer ${token}`)
expect(obj.url).toBe('http://httpbin.org/get')
})
})
@@ -0,0 +1,374 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as httpm from '..'
import * as path from 'path'
import * as fs from 'fs'
const sampleFilePath: string = path.join(__dirname, 'testoutput.txt')
interface HttpBinData {
url: string
data: any
json: any
headers: any
args?: any
}
describe('basics', () => {
let _http: httpm.HttpClient
beforeEach(() => {
_http = new httpm.HttpClient('http-client-tests')
})
afterEach(() => {})
it('constructs', () => {
const http: httpm.HttpClient = new httpm.HttpClient('thttp-client-tests')
expect(http).toBeDefined()
})
// responses from httpbin return something like:
// {
// "args": {},
// "headers": {
// "Connection": "close",
// "Host": "httpbin.org",
// "User-Agent": "typed-test-client-tests"
// },
// "origin": "173.95.152.44",
// "url": "https://httpbin.org/get"
// }
it('does basic http get request', async () => {
const res: httpm.HttpClientResponse = await _http.get(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
expect(obj.url).toBe('http://httpbin.org/get')
expect(obj.headers['User-Agent']).toBeTruthy()
})
it('does basic http get request with no user agent', async () => {
const http: httpm.HttpClient = new httpm.HttpClient()
const res: httpm.HttpClientResponse = await http.get(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
expect(obj.url).toBe('http://httpbin.org/get')
expect(obj.headers['User-Agent']).toBeFalsy()
})
it('does basic https get request', async () => {
const res: httpm.HttpClientResponse = await _http.get(
'https://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
expect(obj.url).toBe('https://httpbin.org/get')
})
it('does basic http get request with default headers', async () => {
const http: httpm.HttpClient = new httpm.HttpClient(
'http-client-tests',
[],
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
}
)
const res: httpm.HttpClientResponse = await http.get(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
expect(obj.headers.Accept).toBe('application/json')
expect(obj.headers['Content-Type']).toBe('application/json')
expect(obj.url).toBe('http://httpbin.org/get')
})
it('does basic http get request with merged headers', async () => {
const http: httpm.HttpClient = new httpm.HttpClient(
'http-client-tests',
[],
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
}
)
const res: httpm.HttpClientResponse = await http.get(
'http://httpbin.org/get',
{
'content-type': 'application/x-www-form-urlencoded'
}
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
expect(obj.headers.Accept).toBe('application/json')
expect(obj.headers['Content-Type']).toBe(
'application/x-www-form-urlencoded'
)
expect(obj.url).toBe('http://httpbin.org/get')
})
it('pipes a get request', async () => {
return new Promise<void>(async resolve => {
const file = fs.createWriteStream(sampleFilePath)
;(await _http.get('https://httpbin.org/get')).message
.pipe(file)
.on('close', () => {
const body: string = fs.readFileSync(sampleFilePath).toString()
const obj = JSON.parse(body)
expect(obj.url).toBe('https://httpbin.org/get')
resolve()
})
})
})
it('does basic get request with redirects', async () => {
const res: httpm.HttpClientResponse = await _http.get(
`https://httpbin.org/redirect-to?url=${encodeURIComponent(
'https://httpbin.org/get'
)}`
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
expect(obj.url).toBe('https://httpbin.org/get')
})
it('does basic get request with redirects (303)', async () => {
const res: httpm.HttpClientResponse = await _http.get(
`https://httpbin.org/redirect-to?url=${encodeURIComponent(
'https://httpbin.org/get'
)}&status_code=303`
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
expect(obj.url).toBe('https://httpbin.org/get')
})
it('returns 404 for not found get request on redirect', async () => {
const res: httpm.HttpClientResponse = await _http.get(
`https://httpbin.org/redirect-to?url=${encodeURIComponent(
'https://httpbin.org/status/404'
)}&status_code=303`
)
expect(res.message.statusCode).toBe(404)
await res.readBody()
})
it('does not follow redirects if disabled', async () => {
const http: httpm.HttpClient = new httpm.HttpClient(
'typed-test-client-tests',
undefined,
{allowRedirects: false}
)
const res: httpm.HttpClientResponse = await http.get(
`https://httpbin.org/redirect-to?url=${encodeURIComponent(
'https://httpbin.org/get'
)}`
)
expect(res.message.statusCode).toBe(302)
await res.readBody()
})
it('does not pass auth with diff hostname redirects', async () => {
const headers = {
accept: 'application/json',
authorization: 'shhh'
}
const res: httpm.HttpClientResponse = await _http.get(
`https://httpbin.org/redirect-to?url=${encodeURIComponent(
'https://www.httpbin.org/get'
)}`,
headers
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
// httpbin "fixes" the casing
expect(obj.headers['Accept']).toBe('application/json')
expect(obj.headers['Authorization']).toBeUndefined()
expect(obj.headers['authorization']).toBeUndefined()
expect(obj.url).toBe('https://www.httpbin.org/get')
})
it('does not pass Auth with diff hostname redirects', async () => {
const headers = {
Accept: 'application/json',
Authorization: 'shhh'
}
const res: httpm.HttpClientResponse = await _http.get(
`https://httpbin.org/redirect-to?url=${encodeURIComponent(
'https://www.httpbin.org/get'
)}`,
headers
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
// httpbin "fixes" the casing
expect(obj.headers['Accept']).toBe('application/json')
expect(obj.headers['Authorization']).toBeUndefined()
expect(obj.headers['authorization']).toBeUndefined()
expect(obj.url).toBe('https://www.httpbin.org/get')
})
it('does basic head request', async () => {
const res: httpm.HttpClientResponse = await _http.head(
'http://httpbin.org/get'
)
expect(res.message.statusCode).toBe(200)
})
it('does basic http delete request', async () => {
const res: httpm.HttpClientResponse = await _http.del(
'http://httpbin.org/delete'
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
JSON.parse(body)
})
it('does basic http post request', async () => {
const b = 'Hello World!'
const res: httpm.HttpClientResponse = await _http.post(
'http://httpbin.org/post',
b
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
expect(obj.data).toBe(b)
expect(obj.url).toBe('http://httpbin.org/post')
})
it('does basic http patch request', async () => {
const b = 'Hello World!'
const res: httpm.HttpClientResponse = await _http.patch(
'http://httpbin.org/patch',
b
)
expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody()
const obj = JSON.parse(body)
expect(obj.data).toBe(b)
expect(obj.url).toBe('http://httpbin.org/patch')
})
it('does basic http options request', async () => {
const res: httpm.HttpClientResponse = await _http.options(
'http://httpbin.org'
)
expect(res.message.statusCode).toBe(200)
await res.readBody()
})
it('returns 404 for not found get request', async () => {
const res: httpm.HttpClientResponse = await _http.get(
'http://httpbin.org/status/404'
)
expect(res.message.statusCode).toBe(404)
await res.readBody()
})
it('gets a json object', async () => {
const jsonObj = await _http.getJson<HttpBinData>('https://httpbin.org/get')
expect(jsonObj.statusCode).toBe(200)
expect(jsonObj.result).toBeDefined()
expect(jsonObj.result?.url).toBe('https://httpbin.org/get')
expect(jsonObj.result?.headers['Accept']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
it('getting a non existent json object returns null', async () => {
const jsonObj = await _http.getJson<HttpBinData>(
'https://httpbin.org/status/404'
)
expect(jsonObj.statusCode).toBe(404)
expect(jsonObj.result).toBeNull()
})
it('posts a json object', async () => {
const res = {name: 'foo'}
const restRes = await _http.postJson<HttpBinData>(
'https://httpbin.org/post',
res
)
expect(restRes.statusCode).toBe(200)
expect(restRes.result).toBeDefined()
expect(restRes.result?.url).toBe('https://httpbin.org/post')
expect(restRes.result?.json.name).toBe('foo')
expect(restRes.result?.headers['Accept']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(restRes.result?.headers['Content-Type']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(restRes.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
it('puts a json object', async () => {
const res = {name: 'foo'}
const restRes = await _http.putJson<HttpBinData>(
'https://httpbin.org/put',
res
)
expect(restRes.statusCode).toBe(200)
expect(restRes.result).toBeDefined()
expect(restRes.result?.url).toBe('https://httpbin.org/put')
expect(restRes.result?.json.name).toBe('foo')
expect(restRes.result?.headers['Accept']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(restRes.result?.headers['Content-Type']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(restRes.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
it('patch a json object', async () => {
const res = {name: 'foo'}
const restRes = await _http.patchJson<HttpBinData>(
'https://httpbin.org/patch',
res
)
expect(restRes.statusCode).toBe(200)
expect(restRes.result).toBeDefined()
expect(restRes.result?.url).toBe('https://httpbin.org/patch')
expect(restRes.result?.json.name).toBe('foo')
expect(restRes.result?.headers['Accept']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(restRes.result?.headers['Content-Type']).toBe(
httpm.MediaTypes.ApplicationJson
)
expect(restRes.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
})
@@ -0,0 +1,116 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as httpm from '..'
describe('headers', () => {
let _http: httpm.HttpClient
beforeEach(() => {
_http = new httpm.HttpClient('http-client-tests')
})
it('preserves existing headers on getJson', async () => {
const additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj = await _http.getJson<any>(
'https://httpbin.org/get',
additionalHeaders
)
expect(jsonObj.result.headers['Accept']).toBe('foo')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
const httpWithHeaders = new httpm.HttpClient()
httpWithHeaders.requestOptions = {
headers: {
[httpm.Headers.Accept]: 'baz'
}
}
jsonObj = await httpWithHeaders.getJson<any>('https://httpbin.org/get')
expect(jsonObj.result.headers['Accept']).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
it('preserves existing headers on postJson', async () => {
const additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj = await _http.postJson<any>(
'https://httpbin.org/post',
{},
additionalHeaders
)
expect(jsonObj.result.headers['Accept']).toBe('foo')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
const httpWithHeaders = new httpm.HttpClient()
httpWithHeaders.requestOptions = {
headers: {
[httpm.Headers.Accept]: 'baz'
}
}
jsonObj = await httpWithHeaders.postJson<any>(
'https://httpbin.org/post',
{}
)
expect(jsonObj.result.headers['Accept']).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
it('preserves existing headers on putJson', async () => {
const additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj = await _http.putJson<any>(
'https://httpbin.org/put',
{},
additionalHeaders
)
expect(jsonObj.result.headers['Accept']).toBe('foo')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
const httpWithHeaders = new httpm.HttpClient()
httpWithHeaders.requestOptions = {
headers: {
[httpm.Headers.Accept]: 'baz'
}
}
jsonObj = await httpWithHeaders.putJson<any>('https://httpbin.org/put', {})
expect(jsonObj.result.headers['Accept']).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
it('preserves existing headers on patchJson', async () => {
const additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj = await _http.patchJson<any>(
'https://httpbin.org/patch',
{},
additionalHeaders
)
expect(jsonObj.result.headers['Accept']).toBe('foo')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
const httpWithHeaders = new httpm.HttpClient()
httpWithHeaders.requestOptions = {
headers: {
[httpm.Headers.Accept]: 'baz'
}
}
jsonObj = await httpWithHeaders.patchJson<any>(
'https://httpbin.org/patch',
{}
)
expect(jsonObj.result.headers['Accept']).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson
)
})
})

Some files were not shown because too many files have changed in this diff Show More