Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 143 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@

# Download Artifact #1 and verify the correctness of the content
- name: 'Download artifact #1'
uses: actions/download-artifact@v4
uses: actions/download-artifact@main

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Genuinely curious - why isn't this action pinned to its commit SHA?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two actions are intertwined quite a bit. Keeping this test's reference relative helps smoke test
somewhat.

with:
name: 'Artifact-A-${{ matrix.runs-on }}'
path: some/new/path
Expand All @@ -114,7 +114,7 @@

# 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
Expand All @@ -135,7 +135,7 @@

# 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
Expand All @@ -155,7 +155,7 @@
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
Expand Down Expand Up @@ -196,7 +196,7 @@

# 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
Expand All @@ -213,7 +213,102 @@
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@main
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'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Artifact Upload Test Report</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; color: #24292f; }
h1 { border-bottom: 1px solid #d0d7de; padding-bottom: 8px; }
.success { color: #1a7f37; }
.info { background: #ddf4ff; border: 1px solid #54aeff; border-radius: 6px; padding: 12px 16px; margin: 16px 0; }
table { border-collapse: collapse; width: 100%; margin: 16px 0; }
th, td { border: 1px solid #d0d7de; padding: 8px 12px; text-align: left; }
th { background: #f6f8fa; }
</style>
</head>
<body>
<h1>Artifact Upload Test Report</h1>
<div class="info">
<strong>This HTML file was uploaded as a single un-zipped artifact.</strong>
If you can see this in the browser, the feature is working correctly!
</div>
<table>
<tr><th>Property</th><th>Value</th></tr>
<tr><td>Upload method</td><td><code>archive: false</code></td></tr>
<tr><td>Content-Type</td><td><code>text/html</code></td></tr>
<tr><td>File</td><td><code>report.html</code></td></tr>
</table>
<p class="success">&#10004; Single file upload is working!</p>
</body>
</html>
EOF

- name: Upload HTML report (no archive)
uses: ./
with:
name: 'test-report'
path: report.html
archive: false

merge:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium test

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
name: Merge
needs: build
runs-on: ubuntu-latest
Expand All @@ -230,7 +325,7 @@
# 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
Expand Down Expand Up @@ -266,7 +361,7 @@

# 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
Expand All @@ -290,3 +385,44 @@
}
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}`);
}
}

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium test

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 4 months ago

In general, the fix is to add an explicit permissions: block that grants only the minimum scopes needed to run this workflow. This can be done at the workflow (top) level to apply to all jobs, or specifically on the cleanup job if different jobs need different scopes. Since the highlighted issue is on the cleanup job, and we want the smallest change without affecting other jobs’ current behavior, we will add a permissions: block only to the cleanup job.

The cleanup job reads and deletes artifacts via the @actions/artifact client. Artifact operations are governed by the actions permission, not contents. There is no need for contents: write, issues, pull-requests, etc. A minimal and appropriate configuration is:

    permissions:
      actions: write
      contents: read

actions: write allows managing artifacts created by workflows; contents: read is a safe baseline and recommended as a default read-only scope. We will insert this directly under runs-on: ubuntu-latest in the cleanup job, around line 392, in .github/workflows/test.yml. No imports or additional methods are required because this is purely a YAML configuration change.

Suggested changeset 1
.github/workflows/test.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -389,6 +389,9 @@
     name: Cleanup Artifacts
     needs: [build, merge]
     runs-on: ubuntu-latest
+    permissions:
+      actions: write
+      contents: read
 
     steps:
     - name: Checkout
EOF
@@ -389,6 +389,9 @@
name: Cleanup Artifacts
needs: [build, merge]
runs-on: ubuntu-latest
permissions:
actions: write
contents: read

steps:
- name: Checkout
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set the permissions block.


1 change: 1 addition & 0 deletions __tests__/upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const mockInputs = (
[Inputs.RetentionDays]: 0,
[Inputs.CompressionLevel]: 6,
[Inputs.Overwrite]: false,
[Inputs.Archive]: true,
...overrides
Comment thread
danwkennedy marked this conversation as resolved.
}

Expand Down
10 changes: 8 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >
Expand Down Expand Up @@ -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.
Comment thread
danwkennedy marked this conversation as resolved.
Outdated
default: 'true'

outputs:
artifact-id:
Expand Down
13 changes: 12 additions & 1 deletion dist/upload/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
}
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/upload/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion src/upload/input-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions src/upload/upload-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ export async function run(): Promise<void> {
)
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)
}
Expand All @@ -70,6 +78,10 @@ export async function run(): Promise<void> {
options.compressionLevel = inputs.compressionLevel
}

if (!inputs.archive) {
options.skipArchive = true
}

await uploadArtifact(
inputs.artifactName,
searchResult.filesToUpload,
Expand Down
6 changes: 6 additions & 0 deletions src/upload/upload-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading