From c6ec99779f695aadd80d788ed675765b75862272 Mon Sep 17 00:00:00 2001 From: Daniel Kennedy Date: Wed, 25 Feb 2026 13:30:34 -0500 Subject: [PATCH 01/10] Try fixing licenced issues --- .licenses/npm/minimatch-10.1.1.dep.yml | 66 ++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .licenses/npm/minimatch-10.1.1.dep.yml diff --git a/.licenses/npm/minimatch-10.1.1.dep.yml b/.licenses/npm/minimatch-10.1.1.dep.yml new file mode 100644 index 0000000..b3a5b56 --- /dev/null +++ b/.licenses/npm/minimatch-10.1.1.dep.yml @@ -0,0 +1,66 @@ +--- +name: minimatch +version: 10.1.1 +type: npm +summary: a glob matcher in javascript +homepage: +license: blueoak-1.0.0 +licenses: +- sources: LICENSE.md + text: | + # Blue Oak Model License + + Version 1.0.0 + + ## Purpose + + This license gives everyone as much permission to work with + this software as possible, while protecting contributors + from liability. + + ## Acceptance + + In order to receive this license, you must agree to its + rules. The rules of this license are both obligations + under that agreement and conditions to your license. + You must not do anything with this software that triggers + a rule that you cannot or will not follow. + + ## Copyright + + Each contributor licenses you to do everything with this + software that would otherwise infringe that contributor's + copyright in it. + + ## Notices + + You must ensure that everyone who gets a copy of + any part of this software from you, with or without + changes, also gets the text of this license or a link to + . + + ## Excuse + + If anyone notifies you in writing that you have not + complied with [Notices](#notices), you can keep your + license by taking all practical steps to comply within 30 + days after the notice. If you do not do so, your license + ends immediately. + + ## Patent + + Each contributor licenses you to do everything with this + software that would otherwise infringe any patent claims + they can license or become able to license. + + ## Reliability + + No contributor can revoke this license. + + ## No Liability + + **_As far as the law allows, this software comes as is, + without any warranty or condition, and no contributor + will be liable to anyone for any damages related to this + software or this license, under any kind of legal claim._** +notices: [] \ No newline at end of file From 4177a106a58b32e5a7742e47cea3ceb16435dd9b Mon Sep 17 00:00:00 2001 From: Daniel Kennedy Date: Wed, 25 Feb 2026 13:44:14 -0500 Subject: [PATCH 02/10] More licensed fixes --- .licenses/npm/balanced-match-4.0.4.dep.yml | 34 ---------- .licenses/npm/brace-expansion-5.0.3.dep.yml | 34 ---------- .licenses/npm/fast-content-type-parse.dep.yml | 37 ----------- .licenses/npm/json-with-bigint.dep.yml | 33 ---------- .licenses/npm/minimatch-10.1.1.dep.yml | 66 ------------------- .licenses/npm/minimatch-9.0.8.dep.yml | 66 ------------------- .licenses/npm/minimatch.dep.yml | 8 +-- 7 files changed, 4 insertions(+), 274 deletions(-) delete mode 100644 .licenses/npm/balanced-match-4.0.4.dep.yml delete mode 100644 .licenses/npm/brace-expansion-5.0.3.dep.yml delete mode 100644 .licenses/npm/fast-content-type-parse.dep.yml delete mode 100644 .licenses/npm/json-with-bigint.dep.yml delete mode 100644 .licenses/npm/minimatch-10.1.1.dep.yml delete mode 100644 .licenses/npm/minimatch-9.0.8.dep.yml diff --git a/.licenses/npm/balanced-match-4.0.4.dep.yml b/.licenses/npm/balanced-match-4.0.4.dep.yml deleted file mode 100644 index 101c2c3..0000000 --- a/.licenses/npm/balanced-match-4.0.4.dep.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: balanced-match -version: 4.0.4 -type: npm -summary: Match balanced character pairs, like "{" and "}" -homepage: -license: mit -licenses: -- sources: LICENSE.md - text: | - (MIT) - - Original code Copyright Julian Gruber - - Port to TypeScript Copyright Isaac Z. Schlueter - - 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. -notices: [] diff --git a/.licenses/npm/brace-expansion-5.0.3.dep.yml b/.licenses/npm/brace-expansion-5.0.3.dep.yml deleted file mode 100644 index 07359b8..0000000 --- a/.licenses/npm/brace-expansion-5.0.3.dep.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: brace-expansion -version: 5.0.3 -type: npm -summary: Brace expansion as known from sh/bash -homepage: -license: mit -licenses: -- sources: LICENSE - text: | - MIT License - - Copyright Julian Gruber - - TypeScript port Copyright Isaac Z. Schlueter - - 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. -notices: [] diff --git a/.licenses/npm/fast-content-type-parse.dep.yml b/.licenses/npm/fast-content-type-parse.dep.yml deleted file mode 100644 index e411b38..0000000 --- a/.licenses/npm/fast-content-type-parse.dep.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: fast-content-type-parse -version: 3.0.0 -type: npm -summary: Parse HTTP Content-Type header according to RFC 7231 -homepage: https://github.com/fastify/fast-content-type-parse#readme -license: other -licenses: -- sources: LICENSE - text: |- - MIT License - - Copyright (c) 2023 The Fastify Team - - The Fastify team members are listed at https://github.com/fastify/fastify#team - and in the README file. - - 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. -- sources: README.md - text: Licensed under [MIT](./LICENSE). -notices: [] diff --git a/.licenses/npm/json-with-bigint.dep.yml b/.licenses/npm/json-with-bigint.dep.yml deleted file mode 100644 index d496868..0000000 --- a/.licenses/npm/json-with-bigint.dep.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: json-with-bigint -version: 3.5.3 -type: npm -summary: JS library that allows you to easily serialize and deserialize data with - BigInt values -homepage: -license: mit -licenses: -- sources: LICENSE - text: | - MIT License - - Copyright (c) 2023 Ivan Korolenko - - 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. -notices: [] diff --git a/.licenses/npm/minimatch-10.1.1.dep.yml b/.licenses/npm/minimatch-10.1.1.dep.yml deleted file mode 100644 index b3a5b56..0000000 --- a/.licenses/npm/minimatch-10.1.1.dep.yml +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: minimatch -version: 10.1.1 -type: npm -summary: a glob matcher in javascript -homepage: -license: blueoak-1.0.0 -licenses: -- sources: LICENSE.md - text: | - # Blue Oak Model License - - Version 1.0.0 - - ## Purpose - - This license gives everyone as much permission to work with - this software as possible, while protecting contributors - from liability. - - ## Acceptance - - In order to receive this license, you must agree to its - rules. The rules of this license are both obligations - under that agreement and conditions to your license. - You must not do anything with this software that triggers - a rule that you cannot or will not follow. - - ## Copyright - - Each contributor licenses you to do everything with this - software that would otherwise infringe that contributor's - copyright in it. - - ## Notices - - You must ensure that everyone who gets a copy of - any part of this software from you, with or without - changes, also gets the text of this license or a link to - . - - ## Excuse - - If anyone notifies you in writing that you have not - complied with [Notices](#notices), you can keep your - license by taking all practical steps to comply within 30 - days after the notice. If you do not do so, your license - ends immediately. - - ## Patent - - Each contributor licenses you to do everything with this - software that would otherwise infringe any patent claims - they can license or become able to license. - - ## Reliability - - No contributor can revoke this license. - - ## No Liability - - **_As far as the law allows, this software comes as is, - without any warranty or condition, and no contributor - will be liable to anyone for any damages related to this - software or this license, under any kind of legal claim._** -notices: [] \ No newline at end of file diff --git a/.licenses/npm/minimatch-9.0.8.dep.yml b/.licenses/npm/minimatch-9.0.8.dep.yml deleted file mode 100644 index e8d3203..0000000 --- a/.licenses/npm/minimatch-9.0.8.dep.yml +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: minimatch -version: 9.0.8 -type: npm -summary: -homepage: -license: blueoak-1.0.0 -licenses: -- sources: LICENSE.md - text: | - # Blue Oak Model License - - Version 1.0.0 - - ## Purpose - - This license gives everyone as much permission to work with - this software as possible, while protecting contributors - from liability. - - ## Acceptance - - In order to receive this license, you must agree to its - rules. The rules of this license are both obligations - under that agreement and conditions to your license. - You must not do anything with this software that triggers - a rule that you cannot or will not follow. - - ## Copyright - - Each contributor licenses you to do everything with this - software that would otherwise infringe that contributor's - copyright in it. - - ## Notices - - You must ensure that everyone who gets a copy of - any part of this software from you, with or without - changes, also gets the text of this license or a link to - . - - ## Excuse - - If anyone notifies you in writing that you have not - complied with [Notices](#notices), you can keep your - license by taking all practical steps to comply within 30 - days after the notice. If you do not do so, your license - ends immediately. - - ## Patent - - Each contributor licenses you to do everything with this - software that would otherwise infringe any patent claims - they can license or become able to license. - - ## Reliability - - No contributor can revoke this license. - - ## No Liability - - **_As far as the law allows, this software comes as is, - without any warranty or condition, and no contributor - will be liable to anyone for any damages related to this - software or this license, under any kind of legal claim._** -notices: [] diff --git a/.licenses/npm/minimatch.dep.yml b/.licenses/npm/minimatch.dep.yml index b3a5b56..8ed580a 100644 --- a/.licenses/npm/minimatch.dep.yml +++ b/.licenses/npm/minimatch.dep.yml @@ -1,9 +1,9 @@ --- name: minimatch -version: 10.1.1 +version: 10.2.4 type: npm -summary: a glob matcher in javascript -homepage: +summary: +homepage: license: blueoak-1.0.0 licenses: - sources: LICENSE.md @@ -63,4 +63,4 @@ licenses: without any warranty or condition, and no contributor will be liable to anyone for any damages related to this software or this license, under any kind of legal claim._** -notices: [] \ No newline at end of file +notices: [] From 6b68470975f00706eebefb1dd4cc04ee70160b7a Mon Sep 17 00:00:00 2001 From: Daniel Kennedy Date: Wed, 25 Feb 2026 13:02:50 -0500 Subject: [PATCH 03/10] Support direct file uploads --- __tests__/upload.test.ts | 1 + action.yml | 10 ++++++++-- dist/upload/index.js | 13 ++++++++++++- package-lock.json | 2 +- package.json | 2 +- src/upload/constants.ts | 3 ++- src/upload/input-helper.ts | 4 +++- src/upload/upload-artifact.ts | 12 ++++++++++++ src/upload/upload-inputs.ts | 6 ++++++ 9 files changed, 46 insertions(+), 7 deletions(-) diff --git a/__tests__/upload.test.ts b/__tests__/upload.test.ts index 6e0c26a..ffdc9a5 100644 --- a/__tests__/upload.test.ts +++ b/__tests__/upload.test.ts @@ -72,6 +72,7 @@ const mockInputs = ( [Inputs.RetentionDays]: 0, [Inputs.CompressionLevel]: 6, [Inputs.Overwrite]: false, + [Inputs.Archive]: true, ...overrides } diff --git a/action.yml b/action.yml index 28f04cc..89dda2a 100644 --- a/action.yml +++ b/action.yml @@ -3,10 +3,10 @@ description: 'Upload a build artifact that can be used by subsequent workflow st author: 'GitHub' inputs: name: - description: 'Artifact name' + description: 'Artifact name. If the "archive" input is `false`, the name of the file uploaded will be the artifact name.' default: 'artifact' path: - description: 'A file, directory or wildcard pattern that describes what to upload' + description: 'A file, directory or wildcard pattern that describes what to upload.' required: true if-no-files-found: description: > @@ -45,6 +45,12 @@ inputs: If true, hidden files will be included in the artifact. If false, hidden files will be excluded from the artifact. default: 'false' + archive: + description: > + If true, the artifact will be archived (zipped) before uploading. + If false, the artifact will be uploaded as-is without archiving. + When archive is false, only a single file can be uploaded. The name of the file will be used as the artifact name. + default: 'true' outputs: artifact-id: diff --git a/dist/upload/index.js b/dist/upload/index.js index f08f27c..a4ca651 100644 --- a/dist/upload/index.js +++ b/dist/upload/index.js @@ -130457,6 +130457,7 @@ var Inputs; Inputs["CompressionLevel"] = "compression-level"; Inputs["Overwrite"] = "overwrite"; Inputs["IncludeHiddenFiles"] = "include-hidden-files"; + Inputs["Archive"] = "archive"; })(Inputs || (Inputs = {})); var NoFileOptions; (function (NoFileOptions) { @@ -130485,6 +130486,7 @@ function getInputs() { const path = getInput(Inputs.Path, { required: true }); const overwrite = getBooleanInput(Inputs.Overwrite); const includeHiddenFiles = getBooleanInput(Inputs.IncludeHiddenFiles); + const archive = getBooleanInput(Inputs.Archive); const ifNoFilesFound = getInput(Inputs.IfNoFilesFound); const noFileBehavior = NoFileOptions[ifNoFilesFound]; if (!noFileBehavior) { @@ -130495,7 +130497,8 @@ function getInputs() { searchPath: path, ifNoFilesFound: noFileBehavior, overwrite: overwrite, - includeHiddenFiles: includeHiddenFiles + includeHiddenFiles: includeHiddenFiles, + archive: archive }; const retentionDaysStr = getInput(Inputs.RetentionDays); if (retentionDaysStr) { @@ -130576,6 +130579,11 @@ async function run() { const s = searchResult.filesToUpload.length === 1 ? '' : 's'; info(`With the provided path, there will be ${searchResult.filesToUpload.length} file${s} uploaded`); core_debug(`Root artifact directory is ${searchResult.rootDirectory}`); + // Validate that only a single file is uploaded when archive is false + if (!inputs.archive && searchResult.filesToUpload.length > 1) { + setFailed(`When 'archive' is set to false, only a single file can be uploaded. Found ${searchResult.filesToUpload.length} files to upload.`); + return; + } if (inputs.overwrite) { await deleteArtifactIfExists(inputs.artifactName); } @@ -130586,6 +130594,9 @@ async function run() { if (typeof inputs.compressionLevel !== 'undefined') { options.compressionLevel = inputs.compressionLevel; } + if (!inputs.archive) { + options.skipArchive = true; + } await upload_artifact_uploadArtifact(inputs.artifactName, searchResult.filesToUpload, searchResult.rootDirectory, options); } } diff --git a/package-lock.json b/package-lock.json index a0c89f3..5739b1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "7.0.0", "license": "MIT", "dependencies": { - "@actions/artifact": "^6.1.0", + "@actions/artifact": "^6.2.0", "@actions/core": "^3.0.0", "@actions/github": "^9.0.0", "@actions/glob": "^0.6.1", diff --git a/package.json b/package.json index c668d43..b71533f 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "node": ">=24" }, "dependencies": { - "@actions/artifact": "^6.1.0", + "@actions/artifact": "^6.2.0", "@actions/core": "^3.0.0", "@actions/github": "^9.0.0", "@actions/glob": "^0.6.1", diff --git a/src/upload/constants.ts b/src/upload/constants.ts index 8407bc1..61ec717 100644 --- a/src/upload/constants.ts +++ b/src/upload/constants.ts @@ -6,7 +6,8 @@ export enum Inputs { RetentionDays = 'retention-days', CompressionLevel = 'compression-level', Overwrite = 'overwrite', - IncludeHiddenFiles = 'include-hidden-files' + IncludeHiddenFiles = 'include-hidden-files', + Archive = 'archive' } export enum NoFileOptions { diff --git a/src/upload/input-helper.ts b/src/upload/input-helper.ts index 527ce6c..29605c6 100644 --- a/src/upload/input-helper.ts +++ b/src/upload/input-helper.ts @@ -10,6 +10,7 @@ export function getInputs(): UploadInputs { const path = core.getInput(Inputs.Path, {required: true}) const overwrite = core.getBooleanInput(Inputs.Overwrite) const includeHiddenFiles = core.getBooleanInput(Inputs.IncludeHiddenFiles) + const archive = core.getBooleanInput(Inputs.Archive) const ifNoFilesFound = core.getInput(Inputs.IfNoFilesFound) const noFileBehavior: NoFileOptions = NoFileOptions[ifNoFilesFound] @@ -29,7 +30,8 @@ export function getInputs(): UploadInputs { searchPath: path, ifNoFilesFound: noFileBehavior, overwrite: overwrite, - includeHiddenFiles: includeHiddenFiles + includeHiddenFiles: includeHiddenFiles, + archive: archive } as UploadInputs const retentionDaysStr = core.getInput(Inputs.RetentionDays) diff --git a/src/upload/upload-artifact.ts b/src/upload/upload-artifact.ts index 1cffa3d..432ec11 100644 --- a/src/upload/upload-artifact.ts +++ b/src/upload/upload-artifact.ts @@ -57,6 +57,14 @@ export async function run(): Promise { ) core.debug(`Root artifact directory is ${searchResult.rootDirectory}`) + // Validate that only a single file is uploaded when archive is false + if (!inputs.archive && searchResult.filesToUpload.length > 1) { + core.setFailed( + `When 'archive' is set to false, only a single file can be uploaded. Found ${searchResult.filesToUpload.length} files to upload.` + ) + return + } + if (inputs.overwrite) { await deleteArtifactIfExists(inputs.artifactName) } @@ -70,6 +78,10 @@ export async function run(): Promise { options.compressionLevel = inputs.compressionLevel } + if (!inputs.archive) { + options.skipArchive = true + } + await uploadArtifact( inputs.artifactName, searchResult.filesToUpload, diff --git a/src/upload/upload-inputs.ts b/src/upload/upload-inputs.ts index b3fa72e..61f221d 100644 --- a/src/upload/upload-inputs.ts +++ b/src/upload/upload-inputs.ts @@ -35,4 +35,10 @@ export interface UploadInputs { * Whether or not to include hidden files in the artifact */ includeHiddenFiles: boolean + + /** + * Whether or not to archive (zip) the artifact before uploading. + * When false, only a single file can be uploaded. + */ + archive: boolean } From ffeab76ee20a70e2c3525e8591d4fecbe4cf2920 Mon Sep 17 00:00:00 2001 From: Daniel Kennedy Date: Wed, 25 Feb 2026 14:18:40 -0500 Subject: [PATCH 04/10] Add CI tests for direct uploads --- .github/workflows/test.yml | 95 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8171bd..8e3fad6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -213,6 +213,101 @@ jobs: Write-Error "File contents of downloaded artifact are incorrect" } shell: pwsh + + # Upload a single file without archiving (direct file upload) + - name: 'Create direct upload file' + run: echo -n 'direct file upload content' > direct-upload-${{ matrix.runs-on }}.txt + shell: bash + + - name: 'Upload direct file artifact' + uses: ./ + with: + name: 'Direct-File-${{ matrix.runs-on }}' + path: direct-upload-${{ matrix.runs-on }}.txt + archive: false + + - name: 'Download direct file artifact' + uses: actions/download-artifact@v4 + with: + name: direct-upload-${{ matrix.runs-on }}.txt + path: direct-download + + - name: 'Verify direct file artifact' + run: | + $file = "direct-download/direct-upload-${{ matrix.runs-on }}.txt" + if(!(Test-Path -path $file)) + { + Write-Error "Expected file does not exist" + } + if(!((Get-Content $file -Raw).TrimEnd() -ceq "direct file upload content")) + { + Write-Error "File contents of downloaded artifact are incorrect" + } + shell: pwsh + + upload-html-report: + name: Upload HTML Report + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node 24 + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Compile + run: npm run build + + - name: Create HTML report + run: | + cat > report.html << 'EOF' + + + + + + Artifact Upload Test Report + + + +

Artifact Upload Test Report

+
+ This HTML file was uploaded as a single un-zipped artifact. + If you can see this in the browser, the feature is working correctly! +
+ + + + + +
PropertyValue
Upload methodarchive: false
Content-Typetext/html
Filereport.html
+

✔ Single file upload is working!

+ + + EOF + + - name: Upload HTML report (no archive) + uses: ./ + with: + name: 'test-report' + path: report.html + archive: false + merge: name: Merge needs: build From 15af3237b6be10bdab185ba95fbb14a8e0fbba39 Mon Sep 17 00:00:00 2001 From: Daniel Kennedy Date: Wed, 25 Feb 2026 14:27:06 -0500 Subject: [PATCH 05/10] Use download-artifact@main temporarily --- .github/workflows/test.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8e3fad6..7ff1ed0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -94,7 +94,7 @@ jobs: # Download Artifact #1 and verify the correctness of the content - name: 'Download artifact #1' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@main with: name: 'Artifact-A-${{ matrix.runs-on }}' path: some/new/path @@ -114,7 +114,7 @@ jobs: # Download Artifact #2 and verify the correctness of the content - name: 'Download artifact #2' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@main with: name: 'Artifact-Wildcard-${{ matrix.runs-on }}' path: some/other/path @@ -135,7 +135,7 @@ jobs: # Download Artifact #4 and verify the correctness of the content - name: 'Download artifact #4' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@main with: name: 'Multi-Path-Artifact-${{ matrix.runs-on }}' path: multi/artifact @@ -155,7 +155,7 @@ jobs: shell: pwsh - name: 'Download symlinked artifact' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@main with: name: 'Symlinked-Artifact-${{ matrix.runs-on }}' path: from/symlink @@ -196,7 +196,7 @@ jobs: # Download replaced Artifact #1 and verify the correctness of the content - name: 'Download artifact #1 again' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@main with: name: 'Artifact-A-${{ matrix.runs-on }}' path: overwrite/some/new/path @@ -227,7 +227,7 @@ jobs: archive: false - name: 'Download direct file artifact' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@main with: name: direct-upload-${{ matrix.runs-on }}.txt path: direct-download @@ -325,7 +325,7 @@ jobs: # easier to identify each of the merged artifacts separate-directories: true - name: 'Download merged artifacts' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@main with: name: merged-artifacts path: all-merged-artifacts @@ -361,7 +361,7 @@ jobs: # Download merged artifacts and verify the correctness of the content - name: 'Download merged artifacts' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@main with: name: Merged-Artifact-As path: merged-artifact-a From 428074b62c1577578835dcdb1f3ba0ca9c665c6a Mon Sep 17 00:00:00 2001 From: Daniel Kennedy Date: Wed, 25 Feb 2026 14:32:24 -0500 Subject: [PATCH 06/10] CI: clean up artifacts on successful runs --- .github/workflows/test.yml | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ff1ed0..94316ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -385,3 +385,44 @@ jobs: } shell: pwsh + cleanup: + name: Cleanup Artifacts + needs: [build, merge] + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node 24 + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Delete test artifacts + uses: actions/github-script@v7 + with: + script: | + const artifactClient = require('@actions/artifact'); + const artifact = artifactClient.default || artifactClient; + + const {artifacts} = await artifact.listArtifacts({latest: true}); + const keep = ['report.html']; + + for (const a of artifacts) { + if (keep.includes(a.name)) { + console.log(`Keeping artifact '${a.name}'`); + continue; + } + try { + await artifact.deleteArtifact(a.name); + console.log(`Deleted artifact '${a.name}'`); + } catch (err) { + console.log(`Could not delete artifact '${a.name}': ${err.message}`); + } + } + From bdabba390d8031c64e093719fae5faff2432f5ae Mon Sep 17 00:00:00 2001 From: Daniel Kennedy Date: Wed, 25 Feb 2026 15:19:28 -0500 Subject: [PATCH 07/10] Use script v8 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94316ef..b0fa590 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -404,7 +404,7 @@ jobs: run: npm ci - name: Delete test artifacts - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const artifactClient = require('@actions/artifact'); From 3a2b6d4c857c8051f7ad8e432e3a09769e9a78f2 Mon Sep 17 00:00:00 2001 From: Daniel Kennedy Date: Wed, 25 Feb 2026 15:25:26 -0500 Subject: [PATCH 08/10] Fix some issues with the cleanup --- .github/workflows/test.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b0fa590..e0fa2ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,10 @@ on: paths-ignore: - '**.md' +permissions: + contents: read + actions: write + jobs: build: name: Build @@ -391,27 +395,20 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node 24 - uses: actions/setup-node@v4 - with: - node-version: 24.x - cache: 'npm' - - - name: Install dependencies - run: npm ci - - name: Delete test artifacts uses: actions/github-script@v8 with: script: | - const artifactClient = require('@actions/artifact'); - const artifact = artifactClient.default || artifactClient; - - const {artifacts} = await artifact.listArtifacts({latest: true}); const keep = ['report.html']; + const owner = context.repo.owner; + const repo = context.repo.repo; + const runId = context.runId; + + const {data: {artifacts}} = await github.rest.actions.listWorkflowRunArtifacts({ + owner, + repo, + run_id: runId + }); for (const a of artifacts) { if (keep.includes(a.name)) { @@ -419,10 +416,13 @@ jobs: continue; } try { - await artifact.deleteArtifact(a.name); + await github.rest.actions.deleteArtifact({ + owner, + repo, + artifact_id: a.id + }); console.log(`Deleted artifact '${a.name}'`); } catch (err) { console.log(`Could not delete artifact '${a.name}': ${err.message}`); } } - From 518025b7539e0b579f3f7c5f5e4c4990b54bd64d Mon Sep 17 00:00:00 2001 From: Daniel Kennedy Date: Wed, 25 Feb 2026 15:32:46 -0500 Subject: [PATCH 09/10] Add unit tests --- __tests__/upload.test.ts | 53 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/__tests__/upload.test.ts b/__tests__/upload.test.ts index ffdc9a5..de81a1c 100644 --- a/__tests__/upload.test.ts +++ b/__tests__/upload.test.ts @@ -274,4 +274,57 @@ describe('upload', () => { `Skipping deletion of '${fixtures.artifactName}', it does not exist` ) }) + + test('passes skipArchive when archive is false', async () => { + mockInputs({ + [Inputs.Archive]: false + }) + + mockFindFilesToUpload.mockResolvedValue({ + filesToUpload: [fixtures.filesToUpload[0]], + rootDirectory: fixtures.rootDirectory + }) + + await run() + + expect(artifact.default.uploadArtifact).toHaveBeenCalledWith( + fixtures.artifactName, + [fixtures.filesToUpload[0]], + fixtures.rootDirectory, + {compressionLevel: 6, skipArchive: true} + ) + }) + + test('does not pass skipArchive when archive is true', async () => { + mockInputs({ + [Inputs.Archive]: true + }) + + mockFindFilesToUpload.mockResolvedValue({ + filesToUpload: [fixtures.filesToUpload[0]], + rootDirectory: fixtures.rootDirectory + }) + + await run() + + expect(artifact.default.uploadArtifact).toHaveBeenCalledWith( + fixtures.artifactName, + [fixtures.filesToUpload[0]], + fixtures.rootDirectory, + {compressionLevel: 6} + ) + }) + + test('fails when archive is false and multiple files are provided', async () => { + mockInputs({ + [Inputs.Archive]: false + }) + + await run() + + expect(core.setFailed).toHaveBeenCalledWith( + `When 'archive' is set to false, only a single file can be uploaded. Found ${fixtures.filesToUpload.length} files to upload.` + ) + expect(artifact.default.uploadArtifact).not.toHaveBeenCalled() + }) }) From d5c7beebb0eee5c0b1cdec43bf2bc827ad7edab2 Mon Sep 17 00:00:00 2001 From: Daniel Kennedy Date: Wed, 25 Feb 2026 15:34:02 -0500 Subject: [PATCH 10/10] Clarify naming --- action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index 89dda2a..7cb4d1e 100644 --- a/action.yml +++ b/action.yml @@ -3,7 +3,7 @@ description: 'Upload a build artifact that can be used by subsequent workflow st author: 'GitHub' inputs: name: - description: 'Artifact name. If the "archive" input is `false`, the name of the file uploaded will be the artifact name.' + description: 'Artifact name. If the `archive` input is `false`, the name of the file uploaded will be the artifact name.' default: 'artifact' path: description: 'A file, directory or wildcard pattern that describes what to upload.' @@ -49,7 +49,7 @@ inputs: description: > If true, the artifact will be archived (zipped) before uploading. If false, the artifact will be uploaded as-is without archiving. - When archive is false, only a single file can be uploaded. The name of the file will be used as the artifact name. + When `archive` is `false`, only a single file can be uploaded. The name of the file will be used as the artifact name (ignoring the `name` parameter). default: 'true' outputs: