Compare commits

..

45 Commits

Author SHA1 Message Date
Ryan Ghadimi 1959adefa3 Add additional optional param to core.error 2025-03-27 17:03:39 +00:00
Salman Chishti 930c890727 Merge pull request #1995 from actions/salmanmkc/2-new-cache-artifacts-release
Prepare Cache v4.0.3 & Artifact v2.3.2 releases
2025-03-17 21:22:10 +00:00
Salman Chishti a410c4a9cf remove extra brace 2025-03-17 17:14:25 +00:00
Salman Chishti 10277d48ca Add update to release doc, as will include it in this release 2025-03-17 17:12:32 +00:00
JoannaaKL 857c61a9df Merge pull request #1994 from gitulisca-enterprise-cloud-testing/gitulisca/log-restore-request-version
Log cache version requested on debugging message
2025-03-17 17:58:16 +01:00
Salman Chishti c40bccc9c3 Use patch instead of minor 2025-03-17 14:08:42 +00:00
Salman Chishti ff4d4afef8 shared instead of secure 2025-03-17 12:48:56 +00:00
Salman Chishti 4d4bbebd6a update package-lock.json 2025-03-17 12:47:54 +00:00
Salman Chishti 261fcae498 change it to minor version instead of patch 2025-03-17 12:44:51 +00:00
Salman Chishti 4059d2af66 update versions for cache and artifact 2025-03-17 12:09:16 +00:00
Salman Chishti 2559a2ac8a Merge pull request #1982 from actions/salmanmkc/obfuscate-sas
Remove logging of any SAS tokens in Actions/Cache and Actions/Artifact
2025-03-17 11:47:29 +00:00
Art Leo 514314311c Log cache version requested 2025-03-15 10:13:43 +11:00
Salman Chishti 957d42e6c5 add encoding back with extra tests 2025-03-14 06:38:57 -07:00
Salman Chishti 39419dd8c3 don't need to url encode or set var 2025-03-14 06:21:41 -07:00
Salman Chishti d13e6311f1 fix tests 2025-03-14 04:28:22 -07:00
Salman Chishti 6876e2a664 update ts docs 2025-03-13 04:47:49 -07:00
Salman Chishti fc482662af PR feedback, back to simplified approach, no export on client as well 2025-03-13 04:23:45 -07:00
Salman Chishti abd9054c61 Log debug error when failing to decode 2025-03-12 08:14:01 -07:00
Ryan Ghadimi 253e837c4d Merge pull request #1991 from actions/ghadimir/hash_to_digest_upload
Change hash to digest for consistent terminology across runner logs
2025-03-12 12:26:25 +00:00
Salman Chishti 3ac34ffcb7 Mask different situations, malformed URL, encoded, decoded, raw signatures, nested parameters, and moved to a utility file 2025-03-12 03:17:35 -07:00
Ryan Ghadimi 56c5a39afb Update blob-upload.ts 2025-03-12 07:59:00 +00:00
Ryan Ghadimi 7ae578ddd1 Merge pull request #1987 from actions/ghadimir/digest_typo
Bump release version
2025-03-11 11:07:20 +00:00
Ryan Ghadimi b2d2270685 Bump package.json 2025-03-11 11:02:42 +00:00
Ryan Ghadimi 0d1d5c7687 Bump release version 2025-03-11 10:58:38 +00:00
Ryan Ghadimi 769bb0fea1 Merge pull request #1986 from actions/ghadimir/digest_typo
Fix comment on expectedHash
2025-03-11 10:57:05 +00:00
Ryan Ghadimi d7ddca4309 Fix comment on expectedHash 2025-03-11 10:52:19 +00:00
Ryan Ghadimi 8780507298 Merge pull request #1985 from actions/ghadimir/dropdown_releases
Dropdown for package when releasing
2025-03-10 15:42:45 +00:00
Ryan Ghadimi 790c56665a Update releases.yml 2025-03-10 15:33:38 +00:00
Ryan Ghadimi 9d8017eadb Merge pull request #1976 from actions/ghadimir/prep_artifact_release
Prepare for Artifact v2.3.0 release
2025-03-10 15:23:55 +00:00
Ryan Ghadimi 20fee3ea63 Update @actions/artifact version to 2.3.0 2025-03-10 15:12:36 +00:00
Ryan Ghadimi 7501423b6f Update RELEASES.md for version 2.3.0 2025-03-10 15:11:43 +00:00
Ryan Ghadimi d0cc3418ea Bump version to 2.3.0
Better semver
2025-03-10 15:11:18 +00:00
Salman Chishti 5007821c77 Remove clean script 2025-03-10 06:51:30 -07:00
Salman Chishti 47c4fa85df masks the whole URL, update tests 2025-03-10 06:47:52 -07:00
Salman Chishti 1cd2f8a538 Instead of using utility method in core lib, use method in both twirp clients 2025-03-07 06:01:25 -08:00
Ryan Ghadimi b85d4e6b38 Prepare for Artifact v2.2.3 release 2025-03-07 10:14:36 +00:00
Ryan Ghadimi dc22dc7cad Merge pull request #1975 from actions/ghadimir/update_call_to_list_artifacts
Compare Artifact Digests
2025-03-07 09:51:05 +00:00
Ryan Ghadimi 8c05dc87d8 Change info logs to debug logs 2025-03-07 09:38:33 +00:00
Salman Chishti 884aa17886 remove these changes 2025-03-06 14:31:21 -08:00
Salman Chishti 944e6b78db Add secret and signature masking for cache and artifact packages 2025-03-06 14:25:32 -08:00
JoannaaKL d70fb49aaa Merge pull request #1974 from actions/list-artifacts-fix
Dont skip pages
2025-03-06 09:35:57 +01:00
Ryan Ghadimi 3726c11433 Please the linter 2025-03-05 14:44:58 +00:00
Ryan Ghadimi 71b40f7024 nicer wording 2025-03-05 14:35:01 +00:00
Ryan Ghadimi 83e5e2517b Change some debug -> info for artifacts hash logging 2025-03-05 14:30:51 +00:00
Ryan Ghadimi d5c8a0fa27 Update proto artifact interface, retrieve artifact digests, return indicator of mismatch failure 2025-03-05 11:29:44 +00:00
31 changed files with 1301 additions and 85 deletions
+14 -1
View File
@@ -6,8 +6,21 @@ on:
workflow_dispatch:
inputs:
package:
type: choice
required: true
description: 'core, artifact, cache, exec, github, glob, http-client, io, tool-cache, attest'
description: 'Which package to release'
options:
- artifact
- attest
- cache
- core
- exec
- github
- glob
- http-client
- io
- tool-cache
jobs:
test:
+243 -9
View File
@@ -5,6 +5,10 @@
"packages": {
"": {
"name": "root",
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.0"
},
"devDependencies": {
"@types/jest": "^29.5.4",
"@types/node": "^20.5.7",
@@ -33,6 +37,211 @@
"node": ">=0.10.0"
}
},
"node_modules/@actions/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
"license": "MIT",
"dependencies": {
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1"
}
},
"node_modules/@actions/exec": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
"license": "MIT",
"dependencies": {
"@actions/io": "^1.0.1"
}
},
"node_modules/@actions/github": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz",
"integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==",
"license": "MIT",
"dependencies": {
"@actions/http-client": "^2.2.0",
"@octokit/core": "^5.0.1",
"@octokit/plugin-paginate-rest": "^9.0.0",
"@octokit/plugin-rest-endpoint-methods": "^10.0.0"
}
},
"node_modules/@actions/github/node_modules/@octokit/auth-token": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
"integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==",
"license": "MIT",
"engines": {
"node": ">= 18"
}
},
"node_modules/@actions/github/node_modules/@octokit/core": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz",
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
"@octokit/request": "^8.4.1",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@actions/github/node_modules/@octokit/endpoint": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz",
"integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@actions/github/node_modules/@octokit/graphql": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz",
"integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^8.4.1",
"@octokit/types": "^13.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@actions/github/node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT"
},
"node_modules/@actions/github/node_modules/@octokit/plugin-paginate-rest": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz",
"integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^12.6.0"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "5"
}
},
"node_modules/@actions/github/node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": {
"version": "20.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
"integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
"license": "MIT"
},
"node_modules/@actions/github/node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
"integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^20.0.0"
}
},
"node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz",
"integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^12.6.0"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "5"
}
},
"node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": {
"version": "20.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
"integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
"license": "MIT"
},
"node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
"integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^20.0.0"
}
},
"node_modules/@actions/github/node_modules/@octokit/request": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz",
"integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^9.0.6",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@actions/github/node_modules/@octokit/request-error": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz",
"integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.1.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@actions/github/node_modules/@octokit/types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@actions/http-client": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
"integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^5.25.4"
}
},
"node_modules/@actions/io": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
"license": "MIT"
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
@@ -769,6 +978,15 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -5090,8 +5308,7 @@
"node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
"dev": true
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
},
"node_modules/big-integer": {
"version": "1.6.51",
@@ -6283,8 +6500,7 @@
"node_modules/deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"dev": true
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
},
"node_modules/dequal": {
"version": "2.0.3",
@@ -11781,7 +11997,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
@@ -13905,6 +14120,15 @@
"typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
}
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
"license": "MIT",
"engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -14059,6 +14283,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/undici": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
"engines": {
"node": ">=14.0"
}
},
"node_modules/unique-filename": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz",
@@ -14086,8 +14322,7 @@
"node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"dev": true
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="
},
"node_modules/universalify": {
"version": "2.0.0",
@@ -14344,8 +14579,7 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/write-file-atomic": {
"version": "4.0.2",
+5 -1
View File
@@ -32,5 +32,9 @@
"prettier": "^3.0.0",
"ts-jest": "^29.1.1",
"typescript": "^5.2.2"
},
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.0"
}
}
}
+1
View File
@@ -41,3 +41,4 @@ Any easy way to test changes for the official upload/download actions is to fork
1. In the locally cloned fork, link to your local toolkit changes: `npm link @actions/artifact`
2. Then, compile your changes with: `npm run release`. The local `dist/index.js` should be updated with your changes.
3. Commit and push to your fork, you can then test with a `uses:` in your workflow pointed at your fork.
4. The format for the above is `<username>/<repository-name>/@<ref>`, i.e. `me/myrepo/@HEAD`
+13
View File
@@ -1,5 +1,18 @@
# @actions/artifact Releases
### 2.3.2
- Added masking for Shared Access Signature (SAS) artifact URLs [#1982](https://github.com/actions/toolkit/pull/1982)
- Change hash to digest for consistent terminology across runner logs [#1991](https://github.com/actions/toolkit/pull/1991)
### 2.3.1
- Fix comment typo on expectedHash. [#1986](https://github.com/actions/toolkit/pull/1986)
### 2.3.0
- Allow ArtifactClient to perform digest comparisons, if supplied. [#1975](https://github.com/actions/toolkit/pull/1975)
### 2.2.2
- Default concurrency to 5 for uploading artifacts [#1962](https://github.com/actions/toolkit/pull/1962
@@ -319,14 +319,6 @@ describe('download-artifact', () => {
const mockGet = jest.fn(async () => {
return new Promise((resolve, reject) => {
// Resolve with a 200 status code immediately
resolve({
message: msg,
readBody: async () => {
return Promise.resolve(`{"ok": true}`)
}
})
// Reject with an error after 31 seconds
setTimeout(() => {
reject(new Error('Request timeout'))
@@ -1,5 +1,4 @@
import * as github from '@actions/github'
import type {RestEndpointMethods} from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/method-types'
import type {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/parameters-and-response-types'
import {
listArtifactsInternal,
@@ -10,13 +9,13 @@ import {ArtifactServiceClientJSON, Timestamp} from '../src/generated'
import * as util from '../src/internal/shared/util'
import {noopLogs} from './common'
import {Artifact} from '../src/internal/shared/interfaces'
import {RequestInterface} from '@octokit/types'
type MockedListWorkflowRunArtifacts = jest.MockedFunction<
RestEndpointMethods['actions']['listWorkflowRunArtifacts']
>
type MockedRequest = jest.MockedFunction<RequestInterface<object>>
jest.mock('@actions/github', () => ({
getOctokit: jest.fn().mockReturnValue({
request: jest.fn(),
rest: {
actions: {
listWorkflowRunArtifacts: jest.fn()
@@ -81,10 +80,10 @@ describe('list-artifact', () => {
describe('public', () => {
it('should return a list of artifacts', async () => {
const mockListArtifacts = github.getOctokit(fixtures.token).rest.actions
.listWorkflowRunArtifacts as MockedListWorkflowRunArtifacts
const mockRequest = github.getOctokit(fixtures.token)
.request as MockedRequest
mockListArtifacts.mockResolvedValueOnce({
mockRequest.mockResolvedValueOnce({
status: 200,
headers: {},
url: '',
@@ -105,10 +104,10 @@ describe('list-artifact', () => {
})
it('should return the latest artifact when latest is specified', async () => {
const mockListArtifacts = github.getOctokit(fixtures.token).rest.actions
.listWorkflowRunArtifacts as MockedListWorkflowRunArtifacts
const mockRequest = github.getOctokit(fixtures.token)
.request as MockedRequest
mockListArtifacts.mockResolvedValueOnce({
mockRequest.mockResolvedValueOnce({
status: 200,
headers: {},
url: '',
@@ -129,10 +128,10 @@ describe('list-artifact', () => {
})
it('can return empty artifacts', async () => {
const mockListArtifacts = github.getOctokit(fixtures.token).rest.actions
.listWorkflowRunArtifacts as MockedListWorkflowRunArtifacts
const mockRequest = github.getOctokit(fixtures.token)
.request as MockedRequest
mockListArtifacts.mockResolvedValueOnce({
mockRequest.mockResolvedValueOnce({
status: 200,
headers: {},
url: '',
@@ -156,10 +155,10 @@ describe('list-artifact', () => {
})
it('should fail if non-200 response', async () => {
const mockListArtifacts = github.getOctokit(fixtures.token).rest.actions
.listWorkflowRunArtifacts as MockedListWorkflowRunArtifacts
const mockRequest = github.getOctokit(fixtures.token)
.request as MockedRequest
mockListArtifacts.mockRejectedValue(new Error('boom'))
mockRequest.mockRejectedValueOnce(new Error('boom'))
await expect(
listArtifactsPublic(
+158
View File
@@ -1,5 +1,7 @@
import * as config from '../src/internal/shared/config'
import * as util from '../src/internal/shared/util'
import {maskSigUrl, maskSecretUrls} from '../src/internal/shared/util'
import {setSecret, debug} from '@actions/core'
export const testRuntimeToken =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2NwIjoiQWN0aW9ucy5FeGFtcGxlIEFjdGlvbnMuQW5vdGhlckV4YW1wbGU6dGVzdCBBY3Rpb25zLlJlc3VsdHM6Y2U3ZjU0YzctNjFjNy00YWFlLTg4N2YtMzBkYTQ3NWY1ZjFhOmNhMzk1MDg1LTA0MGEtNTI2Yi0yY2U4LWJkYzg1ZjY5Mjc3NCIsImlhdCI6MTUxNjIzOTAyMn0.XYnI_wHPBlUi1mqYveJnnkJhp4dlFjqxzRmISPsqfw8'
@@ -59,3 +61,159 @@ describe('get-backend-ids-from-token', () => {
)
})
})
jest.mock('@actions/core')
describe('maskSigUrl', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('does nothing if no sig parameter is present', () => {
const url = 'https://example.com'
maskSigUrl(url)
expect(setSecret).not.toHaveBeenCalled()
})
it('masks the sig parameter in the middle of the URL and sets it as a secret', () => {
const url = 'https://example.com/?param1=value1&sig=12345&param2=value2'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('12345')
expect(setSecret).toHaveBeenCalledWith(encodeURIComponent('12345'))
})
it('does nothing if the URL is empty', () => {
const url = ''
maskSigUrl(url)
expect(setSecret).not.toHaveBeenCalled()
})
it('handles URLs with fragments', () => {
const url = 'https://example.com?sig=12345#fragment'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('12345')
expect(setSecret).toHaveBeenCalledWith(encodeURIComponent('12345'))
})
})
describe('maskSigUrl handles special characters in signatures', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('handles signatures with slashes', () => {
const url = 'https://example.com/?sig=abc/123'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('abc/123')
expect(setSecret).toHaveBeenCalledWith('abc%2F123')
})
it('handles signatures with plus signs', () => {
const url = 'https://example.com/?sig=abc+123'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('abc 123')
expect(setSecret).toHaveBeenCalledWith('abc%20123')
})
it('handles signatures with equals signs', () => {
const url = 'https://example.com/?sig=abc=123'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('abc=123')
expect(setSecret).toHaveBeenCalledWith('abc%3D123')
})
it('handles already percent-encoded signatures', () => {
const url = 'https://example.com/?sig=abc%2F123%3D'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('abc/123=')
expect(setSecret).toHaveBeenCalledWith('abc%2F123%3D')
})
it('handles complex Azure SAS signatures', () => {
const url =
'https://example.com/container/file.txt?sig=nXyQIUj%2F%2F06Cxt80pBRYiiJlYqtPYg5sz%2FvEh5iHAhw%3D&se=2023-12-31'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith(
'nXyQIUj//06Cxt80pBRYiiJlYqtPYg5sz/vEh5iHAhw='
)
expect(setSecret).toHaveBeenCalledWith(
'nXyQIUj%2F%2F06Cxt80pBRYiiJlYqtPYg5sz%2FvEh5iHAhw%3D'
)
})
it('handles signatures with multiple special characters', () => {
const url = 'https://example.com/?sig=a/b+c=d&e=f'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('a/b c=d')
expect(setSecret).toHaveBeenCalledWith('a%2Fb%20c%3Dd')
})
})
describe('maskSecretUrls', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('masks sig parameters in signed_upload_url and signed_url', () => {
const body = {
signed_upload_url: 'https://upload.com?sig=upload123',
signed_url: 'https://download.com?sig=download123'
}
maskSecretUrls(body)
expect(setSecret).toHaveBeenCalledWith('upload123')
expect(setSecret).toHaveBeenCalledWith(encodeURIComponent('upload123'))
expect(setSecret).toHaveBeenCalledWith('download123')
expect(setSecret).toHaveBeenCalledWith(encodeURIComponent('download123'))
})
it('handles case where only upload_url is present', () => {
const body = {
signed_upload_url: 'https://upload.com?sig=upload123'
}
maskSecretUrls(body)
expect(setSecret).toHaveBeenCalledWith('upload123')
expect(setSecret).toHaveBeenCalledWith(encodeURIComponent('upload123'))
})
it('handles case where only download_url is present', () => {
const body = {
signed_url: 'https://download.com?sig=download123'
}
maskSecretUrls(body)
expect(setSecret).toHaveBeenCalledWith('download123')
expect(setSecret).toHaveBeenCalledWith(encodeURIComponent('download123'))
})
it('handles case where URLs do not contain sig parameters', () => {
const body = {
signed_upload_url: 'https://upload.com?token=abc',
signed_url: 'https://download.com?token=xyz'
}
maskSecretUrls(body)
expect(setSecret).not.toHaveBeenCalled()
})
it('handles empty string URLs', () => {
const body = {
signed_upload_url: '',
signed_url: ''
}
maskSecretUrls(body)
expect(setSecret).not.toHaveBeenCalled()
})
it('does nothing if body is not an object or is null', () => {
maskSecretUrls(null)
expect(debug).toHaveBeenCalledWith('body is not an object or is null')
expect(setSecret).not.toHaveBeenCalled()
})
it('does nothing if signed_upload_url and signed_url are not strings', () => {
const body = {
signed_upload_url: 123,
signed_url: 456
}
maskSecretUrls(body)
expect(setSecret).not.toHaveBeenCalled()
})
})
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@actions/artifact",
"version": "2.2.2",
"version": "2.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@actions/artifact",
"version": "2.2.2",
"version": "2.3.2",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/artifact",
"version": "2.2.2",
"version": "2.3.2",
"preview": true,
"description": "Actions artifact lib",
"keywords": [
@@ -15,6 +15,66 @@ import { MessageType } from "@protobuf-ts/runtime";
import { Int64Value } from "../../../google/protobuf/wrappers";
import { StringValue } from "../../../google/protobuf/wrappers";
import { Timestamp } from "../../../google/protobuf/timestamp";
/**
* @generated from protobuf message github.actions.results.api.v1.MigrateArtifactRequest
*/
export interface MigrateArtifactRequest {
/**
* @generated from protobuf field: string workflow_run_backend_id = 1;
*/
workflowRunBackendId: string;
/**
* @generated from protobuf field: string name = 2;
*/
name: string;
/**
* @generated from protobuf field: google.protobuf.Timestamp expires_at = 3;
*/
expiresAt?: Timestamp;
}
/**
* @generated from protobuf message github.actions.results.api.v1.MigrateArtifactResponse
*/
export interface MigrateArtifactResponse {
/**
* @generated from protobuf field: bool ok = 1;
*/
ok: boolean;
/**
* @generated from protobuf field: string signed_upload_url = 2;
*/
signedUploadUrl: string;
}
/**
* @generated from protobuf message github.actions.results.api.v1.FinalizeMigratedArtifactRequest
*/
export interface FinalizeMigratedArtifactRequest {
/**
* @generated from protobuf field: string workflow_run_backend_id = 1;
*/
workflowRunBackendId: string;
/**
* @generated from protobuf field: string name = 2;
*/
name: string;
/**
* @generated from protobuf field: int64 size = 3;
*/
size: string;
}
/**
* @generated from protobuf message github.actions.results.api.v1.FinalizeMigratedArtifactResponse
*/
export interface FinalizeMigratedArtifactResponse {
/**
* @generated from protobuf field: bool ok = 1;
*/
ok: boolean;
/**
* @generated from protobuf field: int64 artifact_id = 2;
*/
artifactId: string;
}
/**
* @generated from protobuf message github.actions.results.api.v1.CreateArtifactRequest
*/
@@ -169,6 +229,12 @@ export interface ListArtifactsResponse_MonolithArtifact {
* @generated from protobuf field: google.protobuf.Timestamp created_at = 6;
*/
createdAt?: Timestamp;
/**
* The SHA-256 digest of the artifact, calculated on upload for upload-artifact v4 & newer
*
* @generated from protobuf field: google.protobuf.StringValue digest = 7;
*/
digest?: StringValue;
}
/**
* @generated from protobuf message github.actions.results.api.v1.GetSignedArtifactURLRequest
@@ -227,6 +293,236 @@ export interface DeleteArtifactResponse {
artifactId: string;
}
// @generated message type with reflection information, may provide speed optimized methods
class MigrateArtifactRequest$Type extends MessageType<MigrateArtifactRequest> {
constructor() {
super("github.actions.results.api.v1.MigrateArtifactRequest", [
{ no: 1, name: "workflow_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "expires_at", kind: "message", T: () => Timestamp }
]);
}
create(value?: PartialMessage<MigrateArtifactRequest>): MigrateArtifactRequest {
const message = { workflowRunBackendId: "", name: "" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<MigrateArtifactRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: MigrateArtifactRequest): MigrateArtifactRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string workflow_run_backend_id */ 1:
message.workflowRunBackendId = reader.string();
break;
case /* string name */ 2:
message.name = reader.string();
break;
case /* google.protobuf.Timestamp expires_at */ 3:
message.expiresAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.expiresAt);
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: MigrateArtifactRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string workflow_run_backend_id = 1; */
if (message.workflowRunBackendId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.workflowRunBackendId);
/* string name = 2; */
if (message.name !== "")
writer.tag(2, WireType.LengthDelimited).string(message.name);
/* google.protobuf.Timestamp expires_at = 3; */
if (message.expiresAt)
Timestamp.internalBinaryWrite(message.expiresAt, writer.tag(3, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message github.actions.results.api.v1.MigrateArtifactRequest
*/
export const MigrateArtifactRequest = new MigrateArtifactRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class MigrateArtifactResponse$Type extends MessageType<MigrateArtifactResponse> {
constructor() {
super("github.actions.results.api.v1.MigrateArtifactResponse", [
{ no: 1, name: "ok", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 2, name: "signed_upload_url", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<MigrateArtifactResponse>): MigrateArtifactResponse {
const message = { ok: false, signedUploadUrl: "" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<MigrateArtifactResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: MigrateArtifactResponse): MigrateArtifactResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bool ok */ 1:
message.ok = reader.bool();
break;
case /* string signed_upload_url */ 2:
message.signedUploadUrl = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: MigrateArtifactResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bool ok = 1; */
if (message.ok !== false)
writer.tag(1, WireType.Varint).bool(message.ok);
/* string signed_upload_url = 2; */
if (message.signedUploadUrl !== "")
writer.tag(2, WireType.LengthDelimited).string(message.signedUploadUrl);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message github.actions.results.api.v1.MigrateArtifactResponse
*/
export const MigrateArtifactResponse = new MigrateArtifactResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class FinalizeMigratedArtifactRequest$Type extends MessageType<FinalizeMigratedArtifactRequest> {
constructor() {
super("github.actions.results.api.v1.FinalizeMigratedArtifactRequest", [
{ no: 1, name: "workflow_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "size", kind: "scalar", T: 3 /*ScalarType.INT64*/ }
]);
}
create(value?: PartialMessage<FinalizeMigratedArtifactRequest>): FinalizeMigratedArtifactRequest {
const message = { workflowRunBackendId: "", name: "", size: "0" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<FinalizeMigratedArtifactRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FinalizeMigratedArtifactRequest): FinalizeMigratedArtifactRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string workflow_run_backend_id */ 1:
message.workflowRunBackendId = reader.string();
break;
case /* string name */ 2:
message.name = reader.string();
break;
case /* int64 size */ 3:
message.size = reader.int64().toString();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: FinalizeMigratedArtifactRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string workflow_run_backend_id = 1; */
if (message.workflowRunBackendId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.workflowRunBackendId);
/* string name = 2; */
if (message.name !== "")
writer.tag(2, WireType.LengthDelimited).string(message.name);
/* int64 size = 3; */
if (message.size !== "0")
writer.tag(3, WireType.Varint).int64(message.size);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message github.actions.results.api.v1.FinalizeMigratedArtifactRequest
*/
export const FinalizeMigratedArtifactRequest = new FinalizeMigratedArtifactRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class FinalizeMigratedArtifactResponse$Type extends MessageType<FinalizeMigratedArtifactResponse> {
constructor() {
super("github.actions.results.api.v1.FinalizeMigratedArtifactResponse", [
{ no: 1, name: "ok", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 2, name: "artifact_id", kind: "scalar", T: 3 /*ScalarType.INT64*/ }
]);
}
create(value?: PartialMessage<FinalizeMigratedArtifactResponse>): FinalizeMigratedArtifactResponse {
const message = { ok: false, artifactId: "0" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<FinalizeMigratedArtifactResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FinalizeMigratedArtifactResponse): FinalizeMigratedArtifactResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bool ok */ 1:
message.ok = reader.bool();
break;
case /* int64 artifact_id */ 2:
message.artifactId = reader.int64().toString();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: FinalizeMigratedArtifactResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bool ok = 1; */
if (message.ok !== false)
writer.tag(1, WireType.Varint).bool(message.ok);
/* int64 artifact_id = 2; */
if (message.artifactId !== "0")
writer.tag(2, WireType.Varint).int64(message.artifactId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message github.actions.results.api.v1.FinalizeMigratedArtifactResponse
*/
export const FinalizeMigratedArtifactResponse = new FinalizeMigratedArtifactResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class CreateArtifactRequest$Type extends MessageType<CreateArtifactRequest> {
constructor() {
super("github.actions.results.api.v1.CreateArtifactRequest", [
@@ -608,7 +904,8 @@ class ListArtifactsResponse_MonolithArtifact$Type extends MessageType<ListArtifa
{ no: 3, name: "database_id", kind: "scalar", T: 3 /*ScalarType.INT64*/ },
{ no: 4, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 5, name: "size", kind: "scalar", T: 3 /*ScalarType.INT64*/ },
{ no: 6, name: "created_at", kind: "message", T: () => Timestamp }
{ no: 6, name: "created_at", kind: "message", T: () => Timestamp },
{ no: 7, name: "digest", kind: "message", T: () => StringValue }
]);
}
create(value?: PartialMessage<ListArtifactsResponse_MonolithArtifact>): ListArtifactsResponse_MonolithArtifact {
@@ -641,6 +938,9 @@ class ListArtifactsResponse_MonolithArtifact$Type extends MessageType<ListArtifa
case /* google.protobuf.Timestamp created_at */ 6:
message.createdAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.createdAt);
break;
case /* google.protobuf.StringValue digest */ 7:
message.digest = StringValue.internalBinaryRead(reader, reader.uint32(), options, message.digest);
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -671,6 +971,9 @@ class ListArtifactsResponse_MonolithArtifact$Type extends MessageType<ListArtifa
/* google.protobuf.Timestamp created_at = 6; */
if (message.createdAt)
Timestamp.internalBinaryWrite(message.createdAt, writer.tag(6, WireType.LengthDelimited).fork(), options).join();
/* google.protobuf.StringValue digest = 7; */
if (message.digest)
StringValue.internalBinaryWrite(message.digest, writer.tag(7, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -912,5 +1215,7 @@ export const ArtifactService = new ServiceType("github.actions.results.api.v1.Ar
{ name: "FinalizeArtifact", options: {}, I: FinalizeArtifactRequest, O: FinalizeArtifactResponse },
{ name: "ListArtifacts", options: {}, I: ListArtifactsRequest, O: ListArtifactsResponse },
{ name: "GetSignedArtifactURL", options: {}, I: GetSignedArtifactURLRequest, O: GetSignedArtifactURLResponse },
{ name: "DeleteArtifact", options: {}, I: DeleteArtifactRequest, O: DeleteArtifactResponse }
]);
{ name: "DeleteArtifact", options: {}, I: DeleteArtifactRequest, O: DeleteArtifactResponse },
{ name: "MigrateArtifact", options: {}, I: MigrateArtifactRequest, O: MigrateArtifactResponse },
{ name: "FinalizeMigratedArtifact", options: {}, I: FinalizeMigratedArtifactRequest, O: FinalizeMigratedArtifactResponse }
]);
@@ -1,11 +1,15 @@
import fs from 'fs/promises'
import * as crypto from 'crypto'
import * as stream from 'stream'
import * as github from '@actions/github'
import * as core from '@actions/core'
import * as httpClient from '@actions/http-client'
import unzip from 'unzip-stream'
import {
DownloadArtifactOptions,
DownloadArtifactResponse
DownloadArtifactResponse,
StreamExtractResponse
} from '../shared/interfaces'
import {getUserAgentString} from '../shared/user-agent'
import {getGitHubWorkspaceDir} from '../shared/config'
@@ -37,12 +41,14 @@ async function exists(path: string): Promise<boolean> {
}
}
async function streamExtract(url: string, directory: string): Promise<void> {
async function streamExtract(
url: string,
directory: string
): Promise<StreamExtractResponse> {
let retryCount = 0
while (retryCount < 5) {
try {
await streamExtractExternal(url, directory)
return
return await streamExtractExternal(url, directory)
} catch (error) {
retryCount++
core.debug(
@@ -59,7 +65,7 @@ async function streamExtract(url: string, directory: string): Promise<void> {
export async function streamExtractExternal(
url: string,
directory: string
): Promise<void> {
): Promise<StreamExtractResponse> {
const client = new httpClient.HttpClient(getUserAgentString())
const response = await client.get(url)
if (response.message.statusCode !== 200) {
@@ -69,6 +75,7 @@ export async function streamExtractExternal(
}
const timeout = 30 * 1000 // 30 seconds
let sha256Digest: string | undefined = undefined
return new Promise((resolve, reject) => {
const timerFn = (): void => {
@@ -78,7 +85,14 @@ export async function streamExtractExternal(
}
const timer = setTimeout(timerFn, timeout)
response.message
const hashStream = crypto.createHash('sha256').setEncoding('hex')
const passThrough = new stream.PassThrough()
response.message.pipe(passThrough)
passThrough.pipe(hashStream)
const extractStream = passThrough
extractStream
.on('data', () => {
timer.refresh()
})
@@ -92,7 +106,12 @@ export async function streamExtractExternal(
.pipe(unzip.Extract({path: directory}))
.on('close', () => {
clearTimeout(timer)
resolve()
if (hashStream) {
hashStream.end()
sha256Digest = hashStream.read() as string
core.info(`SHA256 digest of downloaded artifact is ${sha256Digest}`)
}
resolve({sha256Digest: `sha256:${sha256Digest}`})
})
.on('error', (error: Error) => {
reject(error)
@@ -111,6 +130,8 @@ export async function downloadArtifactPublic(
const api = github.getOctokit(token)
let digestMismatch = false
core.info(
`Downloading artifact '${artifactId}' from '${repositoryOwner}/${repositoryName}'`
)
@@ -140,13 +161,20 @@ export async function downloadArtifactPublic(
try {
core.info(`Starting download of artifact to: ${downloadPath}`)
await streamExtract(location, downloadPath)
const extractResponse = await streamExtract(location, downloadPath)
core.info(`Artifact download completed successfully.`)
if (options?.expectedHash) {
if (options?.expectedHash !== extractResponse.sha256Digest) {
digestMismatch = true
core.debug(`Computed digest: ${extractResponse.sha256Digest}`)
core.debug(`Expected digest: ${options.expectedHash}`)
}
}
} catch (error) {
throw new Error(`Unable to download and extract artifact: ${error.message}`)
}
return {downloadPath}
return {downloadPath, digestMismatch}
}
export async function downloadArtifactInternal(
@@ -157,6 +185,8 @@ export async function downloadArtifactInternal(
const artifactClient = internalArtifactTwirpClient()
let digestMismatch = false
const {workflowRunBackendId, workflowJobRunBackendId} =
getBackendIdsFromToken()
@@ -192,13 +222,20 @@ export async function downloadArtifactInternal(
try {
core.info(`Starting download of artifact to: ${downloadPath}`)
await streamExtract(signedUrl, downloadPath)
const extractResponse = await streamExtract(signedUrl, downloadPath)
core.info(`Artifact download completed successfully.`)
if (options?.expectedHash) {
if (options?.expectedHash !== extractResponse.sha256Digest) {
digestMismatch = true
core.debug(`Computed digest: ${extractResponse.sha256Digest}`)
core.debug(`Expected digest: ${options.expectedHash}`)
}
}
} catch (error) {
throw new Error(`Unable to download and extract artifact: ${error.message}`)
}
return {downloadPath}
return {downloadPath, digestMismatch}
}
async function resolveOrCreateDirectory(
@@ -68,7 +68,10 @@ export async function getArtifactPublic(
name: artifact.name,
id: artifact.id,
size: artifact.size_in_bytes,
createdAt: artifact.created_at ? new Date(artifact.created_at) : undefined
createdAt: artifact.created_at
? new Date(artifact.created_at)
: undefined,
digest: artifact.digest
}
}
}
@@ -115,7 +118,8 @@ export async function getArtifactInternal(
size: Number(artifact.size),
createdAt: artifact.createdAt
? Timestamp.toDate(artifact.createdAt)
: undefined
: undefined,
digest: artifact.digest?.value
}
}
}
@@ -41,14 +41,17 @@ export async function listArtifactsPublic(
const github = getOctokit(token, opts, retry, requestLog)
let currentPageNumber = 1
const {data: listArtifactResponse} =
await github.rest.actions.listWorkflowRunArtifacts({
const {data: listArtifactResponse} = await github.request(
'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts',
{
owner: repositoryOwner,
repo: repositoryName,
run_id: workflowRunId,
per_page: paginationCount,
page: currentPageNumber
})
}
)
let numberOfPages = Math.ceil(
listArtifactResponse.total_count / paginationCount
@@ -67,7 +70,10 @@ export async function listArtifactsPublic(
name: artifact.name,
id: artifact.id,
size: artifact.size_in_bytes,
createdAt: artifact.created_at ? new Date(artifact.created_at) : undefined
createdAt: artifact.created_at
? new Date(artifact.created_at)
: undefined,
digest: (artifact as ArtifactResponse).digest
})
}
// Move to the next page
@@ -80,14 +86,16 @@ export async function listArtifactsPublic(
) {
debug(`Fetching page ${currentPageNumber} of artifact list`)
const {data: listArtifactResponse} =
await github.rest.actions.listWorkflowRunArtifacts({
const {data: listArtifactResponse} = await github.request(
'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts',
{
owner: repositoryOwner,
repo: repositoryName,
run_id: workflowRunId,
per_page: paginationCount,
page: currentPageNumber
})
}
)
for (const artifact of listArtifactResponse.artifacts) {
artifacts.push({
@@ -96,7 +104,8 @@ export async function listArtifactsPublic(
size: artifact.size_in_bytes,
createdAt: artifact.created_at
? new Date(artifact.created_at)
: undefined
: undefined,
digest: (artifact as ArtifactResponse).digest
})
}
}
@@ -132,7 +141,8 @@ export async function listArtifactsInternal(
size: Number(artifact.size),
createdAt: artifact.createdAt
? Timestamp.toDate(artifact.createdAt)
: undefined
: undefined,
digest: artifact.digest?.value
}))
if (latest) {
@@ -146,6 +156,18 @@ export async function listArtifactsInternal(
}
}
/**
* This exists so that we don't have to use 'any' when receiving the artifact list from the GitHub API.
* The digest field is not present in OpenAPI/types at time of writing, which necessitates this change.
*/
interface ArtifactResponse {
name: string
id: number
size_in_bytes: number
created_at?: string
digest?: string
}
/**
* Filters a list of artifacts to only include the latest artifact for each name
* @param artifacts The artifacts to filter
@@ -5,6 +5,7 @@ import {ArtifactServiceClientJSON} from '../../generated'
import {getResultsServiceUrl, getRuntimeToken} from './config'
import {getUserAgentString} from './user-agent'
import {NetworkError, UsageError} from './errors'
import {maskSecretUrls} from './util'
// The twirp http client must implement this interface
interface Rpc {
@@ -86,6 +87,7 @@ class ArtifactHttpClient implements Rpc {
debug(`[Response] - ${response.message.statusCode}`)
debug(`Headers: ${JSON.stringify(response.message.headers, null, 2)}`)
const body = JSON.parse(rawBody)
maskSecretUrls(body)
debug(`Body: ${JSON.stringify(body, null, 2)}`)
if (this.isSuccessStatusCode(statusCode)) {
return {response, body}
@@ -91,6 +91,11 @@ export interface DownloadArtifactResponse {
* The path where the artifact was downloaded to
*/
downloadPath?: string
/**
* Returns true if the digest of the downloaded artifact does not match the expected hash
*/
digestMismatch?: boolean
}
/**
@@ -101,6 +106,20 @@ export interface DownloadArtifactOptions {
* Denotes where the artifact will be downloaded to. If not specified then the artifact is download to GITHUB_WORKSPACE
*/
path?: string
/**
* The hash that was computed for the artifact during upload. If provided, the outcome of the download
* will provide a digestMismatch property indicating whether the hash of the downloaded artifact
* matches the expected hash.
*/
expectedHash?: string
}
export interface StreamExtractResponse {
/**
* The SHA256 hash of the downloaded file
*/
sha256Digest?: string
}
/**
@@ -126,6 +145,11 @@ export interface Artifact {
* The time when the artifact was created
*/
createdAt?: Date
/**
* The digest of the artifact, computed at time of upload.
*/
digest?: string
}
// FindOptions are for fetching Artifact(s) out of the scope of the current run.
@@ -1,6 +1,7 @@
import * as core from '@actions/core'
import {getRuntimeToken} from './config'
import jwt_decode from 'jwt-decode'
import {debug, setSecret} from '@actions/core'
export interface BackendIds {
workflowRunBackendId: string
@@ -69,3 +70,76 @@ export function getBackendIdsFromToken(): BackendIds {
throw InvalidJwtError
}
/**
* Masks the `sig` parameter in a URL and sets it as a secret.
*
* @param url - The URL containing the signature parameter to mask
* @remarks
* This function attempts to parse the provided URL and identify the 'sig' query parameter.
* If found, it registers both the raw and URL-encoded signature values as secrets using
* the Actions `setSecret` API, which prevents them from being displayed in logs.
*
* The function handles errors gracefully if URL parsing fails, logging them as debug messages.
*
* @example
* ```typescript
* // Mask a signature in an Azure SAS token URL
* maskSigUrl('https://example.blob.core.windows.net/container/file.txt?sig=abc123&se=2023-01-01');
* ```
*/
export function maskSigUrl(url: string): void {
if (!url) return
try {
const parsedUrl = new URL(url)
const signature = parsedUrl.searchParams.get('sig')
if (signature) {
setSecret(signature)
setSecret(encodeURIComponent(signature))
}
} catch (error) {
debug(
`Failed to parse URL: ${url} ${
error instanceof Error ? error.message : String(error)
}`
)
}
}
/**
* Masks sensitive information in URLs containing signature parameters.
* Currently supports masking 'sig' parameters in the 'signed_upload_url'
* and 'signed_download_url' properties of the provided object.
*
* @param body - The object should contain a signature
* @remarks
* This function extracts URLs from the object properties and calls maskSigUrl
* on each one to redact sensitive signature information. The function doesn't
* modify the original object; it only marks the signatures as secrets for
* logging purposes.
*
* @example
* ```typescript
* const responseBody = {
* signed_upload_url: 'https://example.com?sig=abc123',
* signed_download_url: 'https://example.com?sig=def456'
* };
* maskSecretUrls(responseBody);
* ```
*/
export function maskSecretUrls(body: Record<string, unknown> | null): void {
if (typeof body !== 'object' || body === null) {
debug('body is not an object or is null')
return
}
if (
'signed_upload_url' in body &&
typeof body.signed_upload_url === 'string'
) {
maskSigUrl(body.signed_upload_url)
}
if ('signed_url' in body && typeof body.signed_url === 'string') {
maskSigUrl(body.signed_url)
}
}
@@ -98,7 +98,7 @@ export async function uploadZipToBlobStorage(
hashStream.end()
sha256Hash = hashStream.read() as string
core.info(`SHA256 hash of uploaded artifact zip is ${sha256Hash}`)
core.info(`SHA256 digest of uploaded artifact zip is ${sha256Hash}`)
if (uploadByteCount === 0) {
core.warning(
+5
View File
@@ -1,5 +1,10 @@
# @actions/cache Releases
### 4.0.3
- Added masking for Shared Access Signature (SAS) cache entry URLs [#1982](https://github.com/actions/toolkit/pull/1982)
- Improved debugging by logging both the cache version alongside the keys requested when a cache restore fails [#1994](https://github.com/actions/toolkit/pull/1994)
### 4.0.2
- Wrap create failures in ReserveCacheError [#1966](https://github.com/actions/toolkit/pull/1966)
+8 -1
View File
@@ -115,6 +115,10 @@ test('restore with restore keys and no cache found', async () => {
const paths = ['node_modules']
const key = 'node-test'
const restoreKeys = ['node-']
const cacheVersion =
'd90f107aaeb22920dba0c637a23c37b5bc497b4dfa3b07fe3f79bf88a273c11b'
const getCacheVersionMock = jest.spyOn(cacheUtils, 'getCacheVersion')
getCacheVersionMock.mockReturnValue(cacheVersion)
jest
.spyOn(CacheServiceClientJSON.prototype, 'GetCacheEntryDownloadURL')
@@ -130,7 +134,10 @@ test('restore with restore keys and no cache found', async () => {
expect(cacheKey).toBe(undefined)
expect(logDebugMock).toHaveBeenCalledWith(
`Cache not found for keys: ${[key, ...restoreKeys].join(', ')}`
`Cache not found for version ${cacheVersion} of keys: ${[
key,
...restoreKeys
].join(', ')}`
)
})
+158
View File
@@ -0,0 +1,158 @@
import {maskSigUrl, maskSecretUrls} from '../src/internal/shared/util'
import {setSecret, debug} from '@actions/core'
jest.mock('@actions/core')
describe('maskSigUrl', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('does nothing if no sig parameter is present', () => {
const url = 'https://example.com'
maskSigUrl(url)
expect(setSecret).not.toHaveBeenCalled()
})
it('masks the sig parameter in the middle of the URL and sets it as a secret', () => {
const url = 'https://example.com/?param1=value1&sig=12345&param2=value2'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('12345')
expect(setSecret).toHaveBeenCalledWith(encodeURIComponent('12345'))
})
it('does nothing if the URL is empty', () => {
const url = ''
maskSigUrl(url)
expect(setSecret).not.toHaveBeenCalled()
})
it('handles URLs with fragments', () => {
const url = 'https://example.com?sig=12345#fragment'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('12345')
expect(setSecret).toHaveBeenCalledWith(encodeURIComponent('12345'))
})
})
describe('maskSigUrl handles special characters in signatures', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('handles signatures with slashes', () => {
const url = 'https://example.com/?sig=abc/123'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('abc/123')
expect(setSecret).toHaveBeenCalledWith('abc%2F123')
})
it('handles signatures with plus signs', () => {
const url = 'https://example.com/?sig=abc+123'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('abc 123')
expect(setSecret).toHaveBeenCalledWith('abc%20123')
})
it('handles signatures with equals signs', () => {
const url = 'https://example.com/?sig=abc=123'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('abc=123')
expect(setSecret).toHaveBeenCalledWith('abc%3D123')
})
it('handles already percent-encoded signatures', () => {
const url = 'https://example.com/?sig=abc%2F123%3D'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('abc/123=')
expect(setSecret).toHaveBeenCalledWith('abc%2F123%3D')
})
it('handles complex Azure SAS signatures', () => {
const url =
'https://example.com/container/file.txt?sig=nXyQIUj%2F%2F06Cxt80pBRYiiJlYqtPYg5sz%2FvEh5iHAhw%3D&se=2023-12-31'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith(
'nXyQIUj//06Cxt80pBRYiiJlYqtPYg5sz/vEh5iHAhw='
)
expect(setSecret).toHaveBeenCalledWith(
'nXyQIUj%2F%2F06Cxt80pBRYiiJlYqtPYg5sz%2FvEh5iHAhw%3D'
)
})
it('handles signatures with multiple special characters', () => {
const url = 'https://example.com/?sig=a/b+c=d&e=f'
maskSigUrl(url)
expect(setSecret).toHaveBeenCalledWith('a/b c=d')
expect(setSecret).toHaveBeenCalledWith('a%2Fb%20c%3Dd')
})
})
describe('maskSecretUrls', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('masks sig parameters in signed_upload_url and signed_download_url', () => {
const body = {
signed_upload_url: 'https://upload.com?sig=upload123',
signed_download_url: 'https://download.com?sig=download123'
}
maskSecretUrls(body)
expect(setSecret).toHaveBeenCalledWith('upload123')
expect(setSecret).toHaveBeenCalledWith(encodeURIComponent('upload123'))
expect(setSecret).toHaveBeenCalledWith('download123')
expect(setSecret).toHaveBeenCalledWith(encodeURIComponent('download123'))
})
it('handles case where only upload_url is present', () => {
const body = {
signed_upload_url: 'https://upload.com?sig=upload123'
}
maskSecretUrls(body)
expect(setSecret).toHaveBeenCalledWith('upload123')
expect(setSecret).toHaveBeenCalledWith(encodeURIComponent('upload123'))
})
it('handles case where only download_url is present', () => {
const body = {
signed_download_url: 'https://download.com?sig=download123'
}
maskSecretUrls(body)
expect(setSecret).toHaveBeenCalledWith('download123')
expect(setSecret).toHaveBeenCalledWith(encodeURIComponent('download123'))
})
it('handles case where URLs do not contain sig parameters', () => {
const body = {
signed_upload_url: 'https://upload.com?token=abc',
signed_download_url: 'https://download.com?token=xyz'
}
maskSecretUrls(body)
expect(setSecret).not.toHaveBeenCalled()
})
it('handles empty string URLs', () => {
const body = {
signed_upload_url: '',
signed_download_url: ''
}
maskSecretUrls(body)
expect(setSecret).not.toHaveBeenCalled()
})
it('does nothing if body is not an object or is null', () => {
maskSecretUrls(null)
expect(debug).toHaveBeenCalledWith('body is not an object or is null')
expect(setSecret).not.toHaveBeenCalled()
})
it('does nothing if signed_upload_url and signed_download_url are not strings', () => {
const body = {
signed_upload_url: 123,
signed_download_url: 456
}
maskSecretUrls(body)
expect(setSecret).not.toHaveBeenCalled()
})
})
+27 -8
View File
@@ -1,12 +1,12 @@
{
"name": "@actions/cache",
"version": "4.0.2",
"version": "4.0.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@actions/cache",
"version": "4.0.2",
"version": "4.0.3",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.11.1",
@@ -21,6 +21,7 @@
"semver": "^6.3.1"
},
"devDependencies": {
"@types/node": "^22.13.9",
"@types/semver": "^6.0.0",
"typescript": "^5.2.2"
}
@@ -324,9 +325,13 @@
}
},
"node_modules/@types/node": {
"version": "20.4.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.6.tgz",
"integrity": "sha512-q0RkvNgMweWWIvSMDiXhflGUKMdIxBo2M2tYM/0kEGDueQByFzK4KZAgu5YHGFNxziTlppNpTIBcqHQAxlfHdA=="
"version": "22.13.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.4",
@@ -548,6 +553,12 @@
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"license": "MIT"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -824,9 +835,12 @@
}
},
"@types/node": {
"version": "20.4.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.6.tgz",
"integrity": "sha512-q0RkvNgMweWWIvSMDiXhflGUKMdIxBo2M2tYM/0kEGDueQByFzK4KZAgu5YHGFNxziTlppNpTIBcqHQAxlfHdA=="
"version": "22.13.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
"requires": {
"undici-types": "~6.20.0"
}
},
"@types/node-fetch": {
"version": "2.6.4",
@@ -993,6 +1007,11 @@
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true
},
"undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/cache",
"version": "4.0.2",
"version": "4.0.3",
"preview": true,
"description": "Actions cache lib",
"keywords": [
@@ -49,6 +49,7 @@
"semver": "^6.3.1"
},
"devDependencies": {
"@types/node": "^22.13.9",
"@types/semver": "^6.0.0",
"typescript": "^5.2.2"
}
+5 -1
View File
@@ -256,7 +256,11 @@ async function restoreCacheV2(
const response = await twirpClient.GetCacheEntryDownloadURL(request)
if (!response.ok) {
core.debug(`Cache not found for keys: ${keys.join(', ')}`)
core.debug(
`Cache not found for version ${request.version} of keys: ${keys.join(
', '
)}`
)
return undefined
}
@@ -6,6 +6,7 @@ import {getRuntimeToken} from '../cacheUtils'
import {BearerCredentialHandler} from '@actions/http-client/lib/auth'
import {HttpClient, HttpClientResponse, HttpCodes} from '@actions/http-client'
import {CacheServiceClientJSON} from '../../generated/results/api/v1/cache.twirp-client'
import {maskSecretUrls} from './util'
// The twirp http client must implement this interface
interface Rpc {
@@ -94,6 +95,7 @@ class CacheServiceClient implements Rpc {
debug(`[Response] - ${response.message.statusCode}`)
debug(`Headers: ${JSON.stringify(response.message.headers, null, 2)}`)
const body = JSON.parse(rawBody)
maskSecretUrls(body)
debug(`Body: ${JSON.stringify(body, null, 2)}`)
if (this.isSuccessStatusCode(statusCode)) {
return {response, body}
+76
View File
@@ -0,0 +1,76 @@
import {debug, setSecret} from '@actions/core'
/**
* Masks the `sig` parameter in a URL and sets it as a secret.
*
* @param url - The URL containing the signature parameter to mask
* @remarks
* This function attempts to parse the provided URL and identify the 'sig' query parameter.
* If found, it registers both the raw and URL-encoded signature values as secrets using
* the Actions `setSecret` API, which prevents them from being displayed in logs.
*
* The function handles errors gracefully if URL parsing fails, logging them as debug messages.
*
* @example
* ```typescript
* // Mask a signature in an Azure SAS token URL
* maskSigUrl('https://example.blob.core.windows.net/container/file.txt?sig=abc123&se=2023-01-01');
* ```
*/
export function maskSigUrl(url: string): void {
if (!url) return
try {
const parsedUrl = new URL(url)
const signature = parsedUrl.searchParams.get('sig')
if (signature) {
setSecret(signature)
setSecret(encodeURIComponent(signature))
}
} catch (error) {
debug(
`Failed to parse URL: ${url} ${
error instanceof Error ? error.message : String(error)
}`
)
}
}
/**
* Masks sensitive information in URLs containing signature parameters.
* Currently supports masking 'sig' parameters in the 'signed_upload_url'
* and 'signed_download_url' properties of the provided object.
*
* @param body - The object should contain a signature
* @remarks
* This function extracts URLs from the object properties and calls maskSigUrl
* on each one to redact sensitive signature information. The function doesn't
* modify the original object; it only marks the signatures as secrets for
* logging purposes.
*
* @example
* ```typescript
* const responseBody = {
* signed_upload_url: 'https://blob.core.windows.net/?sig=abc123',
* signed_download_url: 'https://blob.core/windows.net/?sig=def456'
* };
* maskSecretUrls(responseBody);
* ```
*/
export function maskSecretUrls(body: Record<string, unknown> | null): void {
if (typeof body !== 'object' || body === null) {
debug('body is not an object or is null')
return
}
if (
'signed_upload_url' in body &&
typeof body.signed_upload_url === 'string'
) {
maskSigUrl(body.signed_upload_url)
}
if (
'signed_download_url' in body &&
typeof body.signed_download_url === 'string'
) {
maskSigUrl(body.signed_download_url)
}
}
+3
View File
@@ -1,5 +1,8 @@
# @actions/core Releases
### 1.12.0
- Adds optional functionality to `core.error` to allow for the logging of stack traces.
### 1.11.1
- Fix uses of `crypto.randomUUID` on Node 18 and earlier [#1842](https://github.com/actions/toolkit/pull/1842)
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@actions/core",
"version": "1.11.1",
"version": "1.12.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@actions/core",
"version": "1.11.1",
"version": "1.12.0",
"license": "MIT",
"dependencies": {
"@actions/exec": "^1.1.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/core",
"version": "1.11.1",
"version": "1.12.0",
"description": "Actions core lib",
"keywords": [
"github",
+27 -4
View File
@@ -11,14 +11,37 @@ export interface CommandProperties {
}
/**
* Commands
* Issues a command to the GitHub Actions runner
*
* @param command - The command name to issue
* @param properties - Additional properties for the command (key-value pairs)
* @param message - The message to include with the command
* @remarks
* This function outputs a specially formatted string to stdout that the Actions
* runner interprets as a command. These commands can control workflow behavior,
* set outputs, create annotations, mask values, and more.
*
* Command Format:
* ::name key=value,key=value::message
*
* Examples:
* ::warning::This is the message
* ::set-env name=MY_VAR::some value
* @example
* ```typescript
* // Issue a warning annotation
* issueCommand('warning', {}, 'This is a warning message');
* // Output: ::warning::This is a warning message
*
* // Set an environment variable
* issueCommand('set-env', { name: 'MY_VAR' }, 'some value');
* // Output: ::set-env name=MY_VAR::some value
*
* // Add a secret mask
* issueCommand('add-mask', {}, 'secretValue123');
* // Output: ::add-mask::secretValue123
* ```
*
* @internal
* This is an internal utility function that powers the public API functions
* such as setSecret, warning, error, and exportVariable.
*/
export function issueCommand(
command: string,
+39 -3
View File
@@ -70,6 +70,13 @@ export interface AnnotationProperties {
endColumn?: number
}
export interface ErrorMessageProperties {
/**
* Whether to include the stack trace in the error message. Defaults to false.
*/
withStackTrace?: boolean
}
//-----------------------------------------------------------------------
// Variables
//-----------------------------------------------------------------------
@@ -94,7 +101,32 @@ export function exportVariable(name: string, val: any): void {
/**
* Registers a secret which will get masked from logs
* @param secret value of the secret
*
* @param secret - Value of the secret to be masked
* @remarks
* This function instructs the Actions runner to mask the specified value in any
* logs produced during the workflow run. Once registered, the secret value will
* be replaced with asterisks (***) whenever it appears in console output, logs,
* or error messages.
*
* This is useful for protecting sensitive information such as:
* - API keys
* - Access tokens
* - Authentication credentials
* - URL parameters containing signatures (SAS tokens)
*
* Note that masking only affects future logs; any previous appearances of the
* secret in logs before calling this function will remain unmasked.
*
* @example
* ```typescript
* // Register an API token as a secret
* const apiToken = "abc123xyz456";
* setSecret(apiToken);
*
* // Now any logs containing this value will show *** instead
* console.log(`Using token: ${apiToken}`); // Outputs: "Using token: ***"
* ```
*/
export function setSecret(secret: string): void {
issueCommand('add-mask', {}, secret)
@@ -249,12 +281,16 @@ export function debug(message: string): void {
*/
export function error(
message: string | Error,
properties: AnnotationProperties = {}
properties: AnnotationProperties & ErrorMessageProperties = {}
): void {
if (typeof message === 'string') {
issueCommand('error', toCommandProperties(properties), message)
return
}
issueCommand(
'error',
toCommandProperties(properties),
message instanceof Error ? message.toString() : message
properties.withStackTrace ? message.stack : message.toString()
)
}