X Tutup
Skip to content

Commit bbbca2d

Browse files
authored
Support direct file uploads (#764)
* Cache licenses * Bump minimatch to 10.1.1 * Try fixing licenced issues * More licensed fixes * Support direct file uploads * Add CI tests for direct uploads * Use download-artifact@main temporarily * CI: clean up artifacts on successful runs * Use script v8 * Fix some issues with the cleanup * Add unit tests * Clarify naming
1 parent 589182c commit bbbca2d

File tree

10 files changed

+242
-14
lines changed

10 files changed

+242
-14
lines changed

.github/workflows/test.yml

Lines changed: 143 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ on:
1010
paths-ignore:
1111
- '**.md'
1212

13+
permissions:
14+
contents: read
15+
actions: write
16+
1317
jobs:
1418
build:
1519
name: Build
@@ -94,7 +98,7 @@ jobs:
9498
9599
# Download Artifact #1 and verify the correctness of the content
96100
- name: 'Download artifact #1'
97-
uses: actions/download-artifact@v4
101+
uses: actions/download-artifact@main
98102
with:
99103
name: 'Artifact-A-${{ matrix.runs-on }}'
100104
path: some/new/path
@@ -114,7 +118,7 @@ jobs:
114118

115119
# Download Artifact #2 and verify the correctness of the content
116120
- name: 'Download artifact #2'
117-
uses: actions/download-artifact@v4
121+
uses: actions/download-artifact@main
118122
with:
119123
name: 'Artifact-Wildcard-${{ matrix.runs-on }}'
120124
path: some/other/path
@@ -135,7 +139,7 @@ jobs:
135139

136140
# Download Artifact #4 and verify the correctness of the content
137141
- name: 'Download artifact #4'
138-
uses: actions/download-artifact@v4
142+
uses: actions/download-artifact@main
139143
with:
140144
name: 'Multi-Path-Artifact-${{ matrix.runs-on }}'
141145
path: multi/artifact
@@ -155,7 +159,7 @@ jobs:
155159
shell: pwsh
156160

157161
- name: 'Download symlinked artifact'
158-
uses: actions/download-artifact@v4
162+
uses: actions/download-artifact@main
159163
with:
160164
name: 'Symlinked-Artifact-${{ matrix.runs-on }}'
161165
path: from/symlink
@@ -196,7 +200,7 @@ jobs:
196200

197201
# Download replaced Artifact #1 and verify the correctness of the content
198202
- name: 'Download artifact #1 again'
199-
uses: actions/download-artifact@v4
203+
uses: actions/download-artifact@main
200204
with:
201205
name: 'Artifact-A-${{ matrix.runs-on }}'
202206
path: overwrite/some/new/path
@@ -213,6 +217,101 @@ jobs:
213217
Write-Error "File contents of downloaded artifact are incorrect"
214218
}
215219
shell: pwsh
220+
221+
# Upload a single file without archiving (direct file upload)
222+
- name: 'Create direct upload file'
223+
run: echo -n 'direct file upload content' > direct-upload-${{ matrix.runs-on }}.txt
224+
shell: bash
225+
226+
- name: 'Upload direct file artifact'
227+
uses: ./
228+
with:
229+
name: 'Direct-File-${{ matrix.runs-on }}'
230+
path: direct-upload-${{ matrix.runs-on }}.txt
231+
archive: false
232+
233+
- name: 'Download direct file artifact'
234+
uses: actions/download-artifact@main
235+
with:
236+
name: direct-upload-${{ matrix.runs-on }}.txt
237+
path: direct-download
238+
239+
- name: 'Verify direct file artifact'
240+
run: |
241+
$file = "direct-download/direct-upload-${{ matrix.runs-on }}.txt"
242+
if(!(Test-Path -path $file))
243+
{
244+
Write-Error "Expected file does not exist"
245+
}
246+
if(!((Get-Content $file -Raw).TrimEnd() -ceq "direct file upload content"))
247+
{
248+
Write-Error "File contents of downloaded artifact are incorrect"
249+
}
250+
shell: pwsh
251+
252+
upload-html-report:
253+
name: Upload HTML Report
254+
runs-on: ubuntu-latest
255+
256+
steps:
257+
- name: Checkout
258+
uses: actions/checkout@v4
259+
260+
- name: Setup Node 24
261+
uses: actions/setup-node@v4
262+
with:
263+
node-version: 24.x
264+
cache: 'npm'
265+
266+
- name: Install dependencies
267+
run: npm ci
268+
269+
- name: Compile
270+
run: npm run build
271+
272+
- name: Create HTML report
273+
run: |
274+
cat > report.html << 'EOF'
275+
<!DOCTYPE html>
276+
<html lang="en">
277+
<head>
278+
<meta charset="UTF-8">
279+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
280+
<title>Artifact Upload Test Report</title>
281+
<style>
282+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; color: #24292f; }
283+
h1 { border-bottom: 1px solid #d0d7de; padding-bottom: 8px; }
284+
.success { color: #1a7f37; }
285+
.info { background: #ddf4ff; border: 1px solid #54aeff; border-radius: 6px; padding: 12px 16px; margin: 16px 0; }
286+
table { border-collapse: collapse; width: 100%; margin: 16px 0; }
287+
th, td { border: 1px solid #d0d7de; padding: 8px 12px; text-align: left; }
288+
th { background: #f6f8fa; }
289+
</style>
290+
</head>
291+
<body>
292+
<h1>Artifact Upload Test Report</h1>
293+
<div class="info">
294+
<strong>This HTML file was uploaded as a single un-zipped artifact.</strong>
295+
If you can see this in the browser, the feature is working correctly!
296+
</div>
297+
<table>
298+
<tr><th>Property</th><th>Value</th></tr>
299+
<tr><td>Upload method</td><td><code>archive: false</code></td></tr>
300+
<tr><td>Content-Type</td><td><code>text/html</code></td></tr>
301+
<tr><td>File</td><td><code>report.html</code></td></tr>
302+
</table>
303+
<p class="success">&#10004; Single file upload is working!</p>
304+
</body>
305+
</html>
306+
EOF
307+
308+
- name: Upload HTML report (no archive)
309+
uses: ./
310+
with:
311+
name: 'test-report'
312+
path: report.html
313+
archive: false
314+
216315
merge:
217316
name: Merge
218317
needs: build
@@ -230,7 +329,7 @@ jobs:
230329
# easier to identify each of the merged artifacts
231330
separate-directories: true
232331
- name: 'Download merged artifacts'
233-
uses: actions/download-artifact@v4
332+
uses: actions/download-artifact@main
234333
with:
235334
name: merged-artifacts
236335
path: all-merged-artifacts
@@ -266,7 +365,7 @@ jobs:
266365

267366
# Download merged artifacts and verify the correctness of the content
268367
- name: 'Download merged artifacts'
269-
uses: actions/download-artifact@v4
368+
uses: actions/download-artifact@main
270369
with:
271370
name: Merged-Artifact-As
272371
path: merged-artifact-a
@@ -290,3 +389,40 @@ jobs:
290389
}
291390
shell: pwsh
292391

392+
cleanup:
393+
name: Cleanup Artifacts
394+
needs: [build, merge]
395+
runs-on: ubuntu-latest
396+
397+
steps:
398+
- name: Delete test artifacts
399+
uses: actions/github-script@v8
400+
with:
401+
script: |
402+
const keep = ['report.html'];
403+
const owner = context.repo.owner;
404+
const repo = context.repo.repo;
405+
const runId = context.runId;
406+
407+
const {data: {artifacts}} = await github.rest.actions.listWorkflowRunArtifacts({
408+
owner,
409+
repo,
410+
run_id: runId
411+
});
412+
413+
for (const a of artifacts) {
414+
if (keep.includes(a.name)) {
415+
console.log(`Keeping artifact '${a.name}'`);
416+
continue;
417+
}
418+
try {
419+
await github.rest.actions.deleteArtifact({
420+
owner,
421+
repo,
422+
artifact_id: a.id
423+
});
424+
console.log(`Deleted artifact '${a.name}'`);
425+
} catch (err) {
426+
console.log(`Could not delete artifact '${a.name}': ${err.message}`);
427+
}
428+
}

__tests__/upload.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const mockInputs = (
7272
[Inputs.RetentionDays]: 0,
7373
[Inputs.CompressionLevel]: 6,
7474
[Inputs.Overwrite]: false,
75+
[Inputs.Archive]: true,
7576
...overrides
7677
}
7778

@@ -273,4 +274,57 @@ describe('upload', () => {
273274
`Skipping deletion of '${fixtures.artifactName}', it does not exist`
274275
)
275276
})
277+
278+
test('passes skipArchive when archive is false', async () => {
279+
mockInputs({
280+
[Inputs.Archive]: false
281+
})
282+
283+
mockFindFilesToUpload.mockResolvedValue({
284+
filesToUpload: [fixtures.filesToUpload[0]],
285+
rootDirectory: fixtures.rootDirectory
286+
})
287+
288+
await run()
289+
290+
expect(artifact.default.uploadArtifact).toHaveBeenCalledWith(
291+
fixtures.artifactName,
292+
[fixtures.filesToUpload[0]],
293+
fixtures.rootDirectory,
294+
{compressionLevel: 6, skipArchive: true}
295+
)
296+
})
297+
298+
test('does not pass skipArchive when archive is true', async () => {
299+
mockInputs({
300+
[Inputs.Archive]: true
301+
})
302+
303+
mockFindFilesToUpload.mockResolvedValue({
304+
filesToUpload: [fixtures.filesToUpload[0]],
305+
rootDirectory: fixtures.rootDirectory
306+
})
307+
308+
await run()
309+
310+
expect(artifact.default.uploadArtifact).toHaveBeenCalledWith(
311+
fixtures.artifactName,
312+
[fixtures.filesToUpload[0]],
313+
fixtures.rootDirectory,
314+
{compressionLevel: 6}
315+
)
316+
})
317+
318+
test('fails when archive is false and multiple files are provided', async () => {
319+
mockInputs({
320+
[Inputs.Archive]: false
321+
})
322+
323+
await run()
324+
325+
expect(core.setFailed).toHaveBeenCalledWith(
326+
`When 'archive' is set to false, only a single file can be uploaded. Found ${fixtures.filesToUpload.length} files to upload.`
327+
)
328+
expect(artifact.default.uploadArtifact).not.toHaveBeenCalled()
329+
})
276330
})

action.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ description: 'Upload a build artifact that can be used by subsequent workflow st
33
author: 'GitHub'
44
inputs:
55
name:
6-
description: 'Artifact name'
6+
description: 'Artifact name. If the `archive` input is `false`, the name of the file uploaded will be the artifact name.'
77
default: 'artifact'
88
path:
9-
description: 'A file, directory or wildcard pattern that describes what to upload'
9+
description: 'A file, directory or wildcard pattern that describes what to upload.'
1010
required: true
1111
if-no-files-found:
1212
description: >
@@ -45,6 +45,12 @@ inputs:
4545
If true, hidden files will be included in the artifact.
4646
If false, hidden files will be excluded from the artifact.
4747
default: 'false'
48+
archive:
49+
description: >
50+
If true, the artifact will be archived (zipped) before uploading.
51+
If false, the artifact will be uploaded as-is without archiving.
52+
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).
53+
default: 'true'
4854

4955
outputs:
5056
artifact-id:

dist/upload/index.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130457,6 +130457,7 @@ var Inputs;
130457130457
Inputs["CompressionLevel"] = "compression-level";
130458130458
Inputs["Overwrite"] = "overwrite";
130459130459
Inputs["IncludeHiddenFiles"] = "include-hidden-files";
130460+
Inputs["Archive"] = "archive";
130460130461
})(Inputs || (Inputs = {}));
130461130462
var NoFileOptions;
130462130463
(function (NoFileOptions) {
@@ -130485,6 +130486,7 @@ function getInputs() {
130485130486
const path = getInput(Inputs.Path, { required: true });
130486130487
const overwrite = getBooleanInput(Inputs.Overwrite);
130487130488
const includeHiddenFiles = getBooleanInput(Inputs.IncludeHiddenFiles);
130489+
const archive = getBooleanInput(Inputs.Archive);
130488130490
const ifNoFilesFound = getInput(Inputs.IfNoFilesFound);
130489130491
const noFileBehavior = NoFileOptions[ifNoFilesFound];
130490130492
if (!noFileBehavior) {
@@ -130495,7 +130497,8 @@ function getInputs() {
130495130497
searchPath: path,
130496130498
ifNoFilesFound: noFileBehavior,
130497130499
overwrite: overwrite,
130498-
includeHiddenFiles: includeHiddenFiles
130500+
includeHiddenFiles: includeHiddenFiles,
130501+
archive: archive
130499130502
};
130500130503
const retentionDaysStr = getInput(Inputs.RetentionDays);
130501130504
if (retentionDaysStr) {
@@ -130576,6 +130579,11 @@ async function run() {
130576130579
const s = searchResult.filesToUpload.length === 1 ? '' : 's';
130577130580
info(`With the provided path, there will be ${searchResult.filesToUpload.length} file${s} uploaded`);
130578130581
core_debug(`Root artifact directory is ${searchResult.rootDirectory}`);
130582+
// Validate that only a single file is uploaded when archive is false
130583+
if (!inputs.archive && searchResult.filesToUpload.length > 1) {
130584+
setFailed(`When 'archive' is set to false, only a single file can be uploaded. Found ${searchResult.filesToUpload.length} files to upload.`);
130585+
return;
130586+
}
130579130587
if (inputs.overwrite) {
130580130588
await deleteArtifactIfExists(inputs.artifactName);
130581130589
}
@@ -130586,6 +130594,9 @@ async function run() {
130586130594
if (typeof inputs.compressionLevel !== 'undefined') {
130587130595
options.compressionLevel = inputs.compressionLevel;
130588130596
}
130597+
if (!inputs.archive) {
130598+
options.skipArchive = true;
130599+
}
130589130600
await upload_artifact_uploadArtifact(inputs.artifactName, searchResult.filesToUpload, searchResult.rootDirectory, options);
130590130601
}
130591130602
}

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"node": ">=24"
3434
},
3535
"dependencies": {
36-
"@actions/artifact": "^6.1.0",
36+
"@actions/artifact": "^6.2.0",
3737
"@actions/core": "^3.0.0",
3838
"@actions/github": "^9.0.0",
3939
"@actions/glob": "^0.6.1",

src/upload/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export enum Inputs {
66
RetentionDays = 'retention-days',
77
CompressionLevel = 'compression-level',
88
Overwrite = 'overwrite',
9-
IncludeHiddenFiles = 'include-hidden-files'
9+
IncludeHiddenFiles = 'include-hidden-files',
10+
Archive = 'archive'
1011
}
1112

1213
export enum NoFileOptions {

0 commit comments

Comments
 (0)
X Tutup