mirror of
				https://git.mirrors.martin98.com/https://github.com/actions/upload-artifact
				synced 2025-10-30 23:21:08 +08:00 
			
		
		
		
	reorganize upload code in prep for merge logic, add more tests
This commit is contained in:
		
							parent
							
								
									694cdabd8b
								
							
						
					
					
						commit
						8d531b15a6
					
				
							
								
								
									
										2
									
								
								.github/workflows/check-dist.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/check-dist.yml
									
									
									
									
										vendored
									
									
								
							| @ -34,7 +34,7 @@ jobs: | ||||
|         run: npm ci | ||||
| 
 | ||||
|       - name: Rebuild the dist/ directory | ||||
|         run: npm run build | ||||
|         run: npm run release | ||||
| 
 | ||||
|       - name: Compare the expected and actual dist/ directories | ||||
|         run: | | ||||
|  | ||||
							
								
								
									
										231
									
								
								__tests__/upload.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								__tests__/upload.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,231 @@ | ||||
| import * as core from '@actions/core' | ||||
| import * as github from '@actions/github' | ||||
| import artifact, {ArtifactNotFoundError} from '@actions/artifact' | ||||
| import {run} from '../src/upload/upload-artifact' | ||||
| import {Inputs} from '../src/upload/constants' | ||||
| import * as search from '../src/shared/search' | ||||
| 
 | ||||
| const fixtures = { | ||||
|   artifactName: 'artifact-name', | ||||
|   rootDirectory: '/some/artifact/path', | ||||
|   filesToUpload: [ | ||||
|     '/some/artifact/path/file1.txt', | ||||
|     '/some/artifact/path/file2.txt' | ||||
|   ] | ||||
| } | ||||
| 
 | ||||
| jest.mock('@actions/github', () => ({ | ||||
|   context: { | ||||
|     repo: { | ||||
|       owner: 'actions', | ||||
|       repo: 'toolkit' | ||||
|     }, | ||||
|     runId: 123, | ||||
|     serverUrl: 'https://github.com' | ||||
|   } | ||||
| })) | ||||
| 
 | ||||
| jest.mock('@actions/core') | ||||
| 
 | ||||
| /* eslint-disable no-unused-vars */ | ||||
| const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => { | ||||
|   const inputs = { | ||||
|     [Inputs.Name]: 'artifact-name', | ||||
|     [Inputs.Path]: '/some/artifact/path', | ||||
|     [Inputs.IfNoFilesFound]: 'warn', | ||||
|     [Inputs.RetentionDays]: 0, | ||||
|     [Inputs.CompressionLevel]: 6, | ||||
|     [Inputs.Overwrite]: false, | ||||
|     ...overrides | ||||
|   } | ||||
| 
 | ||||
|   ;(core.getInput as jest.Mock).mockImplementation((name: string) => { | ||||
|     return inputs[name] | ||||
|   }) | ||||
|   ;(core.getBooleanInput as jest.Mock).mockImplementation((name: string) => { | ||||
|     return inputs[name] | ||||
|   }) | ||||
| 
 | ||||
|   return inputs | ||||
| } | ||||
| 
 | ||||
| describe('upload', () => { | ||||
|   beforeEach(async () => { | ||||
|     mockInputs() | ||||
| 
 | ||||
|     jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({ | ||||
|       filesToUpload: fixtures.filesToUpload, | ||||
|       rootDirectory: fixtures.rootDirectory | ||||
|     }) | ||||
| 
 | ||||
|     jest.spyOn(artifact, 'uploadArtifact').mockResolvedValue({ | ||||
|       size: 123, | ||||
|       id: 1337 | ||||
|     }) | ||||
|   }) | ||||
| 
 | ||||
|   it('uploads a single file', async () => { | ||||
|     jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({ | ||||
|       filesToUpload: [fixtures.filesToUpload[0]], | ||||
|       rootDirectory: fixtures.rootDirectory | ||||
|     }) | ||||
| 
 | ||||
|     await run() | ||||
| 
 | ||||
|     expect(artifact.uploadArtifact).toHaveBeenCalledWith( | ||||
|       fixtures.artifactName, | ||||
|       [fixtures.filesToUpload[0]], | ||||
|       fixtures.rootDirectory, | ||||
|       {compressionLevel: 6} | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
|   it('uploads multiple files', async () => { | ||||
|     await run() | ||||
| 
 | ||||
|     expect(artifact.uploadArtifact).toHaveBeenCalledWith( | ||||
|       fixtures.artifactName, | ||||
|       fixtures.filesToUpload, | ||||
|       fixtures.rootDirectory, | ||||
|       {compressionLevel: 6} | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
|   it('sets outputs', async () => { | ||||
|     await run() | ||||
| 
 | ||||
|     expect(core.setOutput).toHaveBeenCalledWith('artifact-id', 1337) | ||||
|     expect(core.setOutput).toHaveBeenCalledWith( | ||||
|       'artifact-url', | ||||
|       `${github.context.serverUrl}/${github.context.repo.owner}/${ | ||||
|         github.context.repo.repo | ||||
|       }/actions/runs/${github.context.runId}/artifacts/${1337}` | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
|   it('supports custom compression level', async () => { | ||||
|     mockInputs({ | ||||
|       [Inputs.CompressionLevel]: 2 | ||||
|     }) | ||||
| 
 | ||||
|     await run() | ||||
| 
 | ||||
|     expect(artifact.uploadArtifact).toHaveBeenCalledWith( | ||||
|       fixtures.artifactName, | ||||
|       fixtures.filesToUpload, | ||||
|       fixtures.rootDirectory, | ||||
|       {compressionLevel: 2} | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
|   it('supports custom retention days', async () => { | ||||
|     mockInputs({ | ||||
|       [Inputs.RetentionDays]: 7 | ||||
|     }) | ||||
| 
 | ||||
|     await run() | ||||
| 
 | ||||
|     expect(artifact.uploadArtifact).toHaveBeenCalledWith( | ||||
|       fixtures.artifactName, | ||||
|       fixtures.filesToUpload, | ||||
|       fixtures.rootDirectory, | ||||
|       {retentionDays: 7, compressionLevel: 6} | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
|   it('supports warn if-no-files-found', async () => { | ||||
|     mockInputs({ | ||||
|       [Inputs.IfNoFilesFound]: 'warn' | ||||
|     }) | ||||
| 
 | ||||
|     jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({ | ||||
|       filesToUpload: [], | ||||
|       rootDirectory: fixtures.rootDirectory | ||||
|     }) | ||||
| 
 | ||||
|     await run() | ||||
| 
 | ||||
|     expect(core.warning).toHaveBeenCalledWith( | ||||
|       `No files were found with the provided path: ${fixtures.rootDirectory}. No artifacts will be uploaded.` | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
|   it('supports error if-no-files-found', async () => { | ||||
|     mockInputs({ | ||||
|       [Inputs.IfNoFilesFound]: 'error' | ||||
|     }) | ||||
| 
 | ||||
|     jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({ | ||||
|       filesToUpload: [], | ||||
|       rootDirectory: fixtures.rootDirectory | ||||
|     }) | ||||
| 
 | ||||
|     await run() | ||||
| 
 | ||||
|     expect(core.setFailed).toHaveBeenCalledWith( | ||||
|       `No files were found with the provided path: ${fixtures.rootDirectory}. No artifacts will be uploaded.` | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
|   it('supports ignore if-no-files-found', async () => { | ||||
|     mockInputs({ | ||||
|       [Inputs.IfNoFilesFound]: 'ignore' | ||||
|     }) | ||||
| 
 | ||||
|     jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({ | ||||
|       filesToUpload: [], | ||||
|       rootDirectory: fixtures.rootDirectory | ||||
|     }) | ||||
| 
 | ||||
|     await run() | ||||
| 
 | ||||
|     expect(core.info).toHaveBeenCalledWith( | ||||
|       `No files were found with the provided path: ${fixtures.rootDirectory}. No artifacts will be uploaded.` | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
|   it('supports overwrite', async () => { | ||||
|     mockInputs({ | ||||
|       [Inputs.Overwrite]: true | ||||
|     }) | ||||
| 
 | ||||
|     jest.spyOn(artifact, 'deleteArtifact').mockResolvedValue({ | ||||
|       id: 1337 | ||||
|     }) | ||||
| 
 | ||||
|     await run() | ||||
| 
 | ||||
|     expect(artifact.uploadArtifact).toHaveBeenCalledWith( | ||||
|       fixtures.artifactName, | ||||
|       fixtures.filesToUpload, | ||||
|       fixtures.rootDirectory, | ||||
|       {compressionLevel: 6} | ||||
|     ) | ||||
| 
 | ||||
|     expect(artifact.deleteArtifact).toHaveBeenCalledWith(fixtures.artifactName) | ||||
|   }) | ||||
| 
 | ||||
|   it('supports overwrite and continues if not found', async () => { | ||||
|     mockInputs({ | ||||
|       [Inputs.Overwrite]: true | ||||
|     }) | ||||
| 
 | ||||
|     jest | ||||
|       .spyOn(artifact, 'deleteArtifact') | ||||
|       .mockRejectedValue(new ArtifactNotFoundError('not found')) | ||||
| 
 | ||||
|     await run() | ||||
| 
 | ||||
|     expect(artifact.uploadArtifact).toHaveBeenCalledWith( | ||||
|       fixtures.artifactName, | ||||
|       fixtures.filesToUpload, | ||||
|       fixtures.rootDirectory, | ||||
|       {compressionLevel: 6} | ||||
|     ) | ||||
| 
 | ||||
|     expect(artifact.deleteArtifact).toHaveBeenCalledWith(fixtures.artifactName) | ||||
|     expect(core.debug).toHaveBeenCalledWith( | ||||
|       `Skipping deletion of '${fixtures.artifactName}', it does not exist` | ||||
|     ) | ||||
|   }) | ||||
| }) | ||||
| @ -58,4 +58,4 @@ outputs: | ||||
|       Common uses cases for such a download URL can be adding download links to artifacts in descriptions or comments on pull requests or issues. | ||||
| runs: | ||||
|   using: 'node20' | ||||
|   main: 'dist/index.js' | ||||
|   main: 'dist/upload/index.js' | ||||
|  | ||||
							
								
								
									
										5037
									
								
								dist/index.js → dist/upload/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5037
									
								
								dist/index.js → dist/upload/index.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -2,10 +2,10 @@ | ||||
|   "name": "upload-artifact", | ||||
|   "version": "4.2.0", | ||||
|   "description": "Upload an Actions Artifact in a workflow run", | ||||
|   "main": "dist/index.js", | ||||
|   "main": "dist/upload/index.js", | ||||
|   "scripts": { | ||||
|     "build": "tsc", | ||||
|     "release": "ncc build src/upload-artifact.ts && git add -f dist/index.js", | ||||
|     "release": "ncc build src/upload/index.ts -o dist/upload", | ||||
|     "check-all": "concurrently \"npm:format-check\" \"npm:lint\" \"npm:test\" \"npm:build\"", | ||||
|     "format": "prettier --write **/*.ts", | ||||
|     "format-check": "prettier --check **/*.ts", | ||||
|  | ||||
							
								
								
									
										28
									
								
								src/shared/upload-artifact.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/shared/upload-artifact.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| import * as core from '@actions/core' | ||||
| import * as github from '@actions/github' | ||||
| import artifact, {UploadArtifactOptions} from '@actions/artifact' | ||||
| 
 | ||||
| export async function uploadArtifact( | ||||
|   artifactName: string, | ||||
|   filesToUpload: string[], | ||||
|   rootDirectory: string, | ||||
|   options: UploadArtifactOptions | ||||
| ) { | ||||
|   const uploadResponse = await artifact.uploadArtifact( | ||||
|     artifactName, | ||||
|     filesToUpload, | ||||
|     rootDirectory, | ||||
|     options | ||||
|   ) | ||||
| 
 | ||||
|   core.info( | ||||
|     `Artifact ${artifactName} has been successfully uploaded! Final size is ${uploadResponse.size} bytes. Artifact ID is ${uploadResponse.id}` | ||||
|   ) | ||||
|   core.setOutput('artifact-id', uploadResponse.id) | ||||
| 
 | ||||
|   const repository = github.context.repo | ||||
|   const artifactURL = `${github.context.serverUrl}/${repository.owner}/${repository.repo}/actions/runs/${github.context.runId}/artifacts/${uploadResponse.id}` | ||||
| 
 | ||||
|   core.info(`Artifact download URL: ${artifactURL}`) | ||||
|   core.setOutput('artifact-url', artifactURL) | ||||
| } | ||||
| @ -1,94 +0,0 @@ | ||||
| import * as core from '@actions/core' | ||||
| import * as github from '@actions/github' | ||||
| import artifact, { | ||||
|   UploadArtifactOptions, | ||||
|   ArtifactNotFoundError | ||||
| } from '@actions/artifact' | ||||
| import {findFilesToUpload} from './search' | ||||
| import {getInputs} from './input-helper' | ||||
| import {NoFileOptions} from './constants' | ||||
| 
 | ||||
| async function deleteArtifactIfExists(artifactName: string): Promise<void> { | ||||
|   try { | ||||
|     await artifact.deleteArtifact(artifactName) | ||||
|   } catch (error) { | ||||
|     if (error instanceof ArtifactNotFoundError) { | ||||
|       core.debug(`Skipping deletion of '${artifactName}', it does not exist`) | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     // Best effort, we don't want to fail the action if this fails
 | ||||
|     core.debug(`Unable to delete artifact: ${(error as Error).message}`) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function run(): Promise<void> { | ||||
|   try { | ||||
|     const inputs = getInputs() | ||||
|     const searchResult = await findFilesToUpload(inputs.searchPath) | ||||
|     if (searchResult.filesToUpload.length === 0) { | ||||
|       // No files were found, different use cases warrant different types of behavior if nothing is found
 | ||||
|       switch (inputs.ifNoFilesFound) { | ||||
|         case NoFileOptions.warn: { | ||||
|           core.warning( | ||||
|             `No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.` | ||||
|           ) | ||||
|           break | ||||
|         } | ||||
|         case NoFileOptions.error: { | ||||
|           core.setFailed( | ||||
|             `No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.` | ||||
|           ) | ||||
|           break | ||||
|         } | ||||
|         case NoFileOptions.ignore: { | ||||
|           core.info( | ||||
|             `No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.` | ||||
|           ) | ||||
|           break | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       const s = searchResult.filesToUpload.length === 1 ? '' : 's' | ||||
|       core.info( | ||||
|         `With the provided path, there will be ${searchResult.filesToUpload.length} file${s} uploaded` | ||||
|       ) | ||||
|       core.debug(`Root artifact directory is ${searchResult.rootDirectory}`) | ||||
| 
 | ||||
|       if (inputs.overwrite) { | ||||
|         await deleteArtifactIfExists(inputs.artifactName) | ||||
|       } | ||||
| 
 | ||||
|       const options: UploadArtifactOptions = {} | ||||
|       if (inputs.retentionDays) { | ||||
|         options.retentionDays = inputs.retentionDays | ||||
|       } | ||||
| 
 | ||||
|       if (typeof inputs.compressionLevel !== 'undefined') { | ||||
|         options.compressionLevel = inputs.compressionLevel | ||||
|       } | ||||
| 
 | ||||
|       const uploadResponse = await artifact.uploadArtifact( | ||||
|         inputs.artifactName, | ||||
|         searchResult.filesToUpload, | ||||
|         searchResult.rootDirectory, | ||||
|         options | ||||
|       ) | ||||
| 
 | ||||
|       core.info( | ||||
|         `Artifact ${inputs.artifactName} has been successfully uploaded! Final size is ${uploadResponse.size} bytes. Artifact ID is ${uploadResponse.id}` | ||||
|       ) | ||||
|       core.setOutput('artifact-id', uploadResponse.id) | ||||
| 
 | ||||
|       const repository = github.context.repo | ||||
|       const artifactURL = `${github.context.serverUrl}/${repository.owner}/${repository.repo}/actions/runs/${github.context.runId}/artifacts/${uploadResponse.id}` | ||||
| 
 | ||||
|       core.info(`Artifact download URL: ${artifactURL}`) | ||||
|       core.setOutput('artifact-url', artifactURL) | ||||
|     } | ||||
|   } catch (error) { | ||||
|     core.setFailed((error as Error).message) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| run() | ||||
							
								
								
									
										6
									
								
								src/upload/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/upload/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| import * as core from '@actions/core' | ||||
| import {run} from './upload-artifact' | ||||
| 
 | ||||
| run().catch(error => { | ||||
|   core.setFailed((error as Error).message) | ||||
| }) | ||||
							
								
								
									
										77
									
								
								src/upload/upload-artifact.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/upload/upload-artifact.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,77 @@ | ||||
| import * as core from '@actions/core' | ||||
| import artifact, { | ||||
|   UploadArtifactOptions, | ||||
|   ArtifactNotFoundError | ||||
| } from '@actions/artifact' | ||||
| import {findFilesToUpload} from '../shared/search' | ||||
| import {getInputs} from './input-helper' | ||||
| import {NoFileOptions} from './constants' | ||||
| import {uploadArtifact} from '../shared/upload-artifact' | ||||
| 
 | ||||
| async function deleteArtifactIfExists(artifactName: string): Promise<void> { | ||||
|   try { | ||||
|     await artifact.deleteArtifact(artifactName) | ||||
|   } catch (error) { | ||||
|     if (error instanceof ArtifactNotFoundError) { | ||||
|       core.debug(`Skipping deletion of '${artifactName}', it does not exist`) | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     // Best effort, we don't want to fail the action if this fails
 | ||||
|     core.debug(`Unable to delete artifact: ${(error as Error).message}`) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export async function run(): Promise<void> { | ||||
|   const inputs = getInputs() | ||||
|   const searchResult = await findFilesToUpload(inputs.searchPath) | ||||
|   if (searchResult.filesToUpload.length === 0) { | ||||
|     // No files were found, different use cases warrant different types of behavior if nothing is found
 | ||||
|     switch (inputs.ifNoFilesFound) { | ||||
|       case NoFileOptions.warn: { | ||||
|         core.warning( | ||||
|           `No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.` | ||||
|         ) | ||||
|         break | ||||
|       } | ||||
|       case NoFileOptions.error: { | ||||
|         core.setFailed( | ||||
|           `No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.` | ||||
|         ) | ||||
|         break | ||||
|       } | ||||
|       case NoFileOptions.ignore: { | ||||
|         core.info( | ||||
|           `No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.` | ||||
|         ) | ||||
|         break | ||||
|       } | ||||
|     } | ||||
|   } else { | ||||
|     const s = searchResult.filesToUpload.length === 1 ? '' : 's' | ||||
|     core.info( | ||||
|       `With the provided path, there will be ${searchResult.filesToUpload.length} file${s} uploaded` | ||||
|     ) | ||||
|     core.debug(`Root artifact directory is ${searchResult.rootDirectory}`) | ||||
| 
 | ||||
|     if (inputs.overwrite) { | ||||
|       await deleteArtifactIfExists(inputs.artifactName) | ||||
|     } | ||||
| 
 | ||||
|     const options: UploadArtifactOptions = {} | ||||
|     if (inputs.retentionDays) { | ||||
|       options.retentionDays = inputs.retentionDays | ||||
|     } | ||||
| 
 | ||||
|     if (typeof inputs.compressionLevel !== 'undefined') { | ||||
|       options.compressionLevel = inputs.compressionLevel | ||||
|     } | ||||
| 
 | ||||
|     await uploadArtifact( | ||||
|       inputs.artifactName, | ||||
|       searchResult.filesToUpload, | ||||
|       searchResult.rootDirectory, | ||||
|       options | ||||
|     ) | ||||
|   } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Rob Herley
						Rob Herley