-
Notifications
You must be signed in to change notification settings - Fork 762
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
1,100 additions
and
61 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import * as core from '@actions/core' | ||
import artifact from '@actions/artifact' | ||
import {run} from '../src/merge/merge-artifacts' | ||
import {Inputs} from '../src/merge/constants' | ||
import * as search from '../src/shared/search' | ||
|
||
const fixtures = { | ||
artifactName: 'my-merged-artifact', | ||
tmpDirectory: '/tmp/merge-artifact', | ||
filesToUpload: [ | ||
'/some/artifact/path/file-a.txt', | ||
'/some/artifact/path/file-b.txt', | ||
'/some/artifact/path/file-c.txt' | ||
], | ||
artifacts: [ | ||
{ | ||
name: 'my-artifact-a', | ||
id: 1, | ||
size: 100, | ||
createdAt: new Date('2024-01-01T00:00:00Z') | ||
}, | ||
{ | ||
name: 'my-artifact-b', | ||
id: 2, | ||
size: 100, | ||
createdAt: new Date('2024-01-01T00:00:00Z') | ||
}, | ||
{ | ||
name: 'my-artifact-c', | ||
id: 3, | ||
size: 100, | ||
createdAt: new Date('2024-01-01T00:00:00Z') | ||
} | ||
] | ||
} | ||
|
||
jest.mock('@actions/github', () => ({ | ||
context: { | ||
repo: { | ||
owner: 'actions', | ||
repo: 'toolkit' | ||
}, | ||
runId: 123, | ||
serverUrl: 'https://github.com' | ||
} | ||
})) | ||
|
||
jest.mock('@actions/core') | ||
|
||
jest.mock('fs/promises', () => ({ | ||
mkdtemp: jest.fn().mockResolvedValue('/tmp/merge-artifact'), | ||
rm: jest.fn().mockResolvedValue(undefined) | ||
})) | ||
|
||
/* eslint-disable no-unused-vars */ | ||
const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => { | ||
const inputs = { | ||
[Inputs.Name]: 'my-merged-artifact', | ||
[Inputs.Pattern]: '*', | ||
[Inputs.SeparateDirectories]: false, | ||
[Inputs.RetentionDays]: 0, | ||
[Inputs.CompressionLevel]: 6, | ||
[Inputs.DeleteMerged]: 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('merge', () => { | ||
beforeEach(async () => { | ||
mockInputs() | ||
|
||
jest | ||
.spyOn(artifact, 'listArtifacts') | ||
.mockResolvedValue({artifacts: fixtures.artifacts}) | ||
|
||
jest.spyOn(artifact, 'downloadArtifact').mockResolvedValue({ | ||
downloadPath: fixtures.tmpDirectory | ||
}) | ||
|
||
jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({ | ||
filesToUpload: fixtures.filesToUpload, | ||
rootDirectory: fixtures.tmpDirectory | ||
}) | ||
|
||
jest.spyOn(artifact, 'uploadArtifact').mockResolvedValue({ | ||
size: 123, | ||
id: 1337 | ||
}) | ||
|
||
jest | ||
.spyOn(artifact, 'deleteArtifact') | ||
.mockImplementation(async artifactName => { | ||
const artifact = fixtures.artifacts.find(a => a.name === artifactName) | ||
if (!artifact) throw new Error(`Artifact ${artifactName} not found`) | ||
return {id: artifact.id} | ||
}) | ||
}) | ||
|
||
it('merges artifacts', async () => { | ||
await run() | ||
|
||
for (const a of fixtures.artifacts) { | ||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(a.id, { | ||
path: fixtures.tmpDirectory | ||
}) | ||
} | ||
|
||
expect(artifact.uploadArtifact).toHaveBeenCalledWith( | ||
fixtures.artifactName, | ||
fixtures.filesToUpload, | ||
fixtures.tmpDirectory, | ||
{compressionLevel: 6} | ||
) | ||
}) | ||
|
||
it('fails if no artifacts found', async () => { | ||
mockInputs({[Inputs.Pattern]: 'this-does-not-match'}) | ||
|
||
expect(run()).rejects.toThrow() | ||
|
||
expect(artifact.uploadArtifact).not.toBeCalled() | ||
expect(artifact.downloadArtifact).not.toBeCalled() | ||
}) | ||
|
||
it('supports custom compression level', async () => { | ||
mockInputs({ | ||
[Inputs.CompressionLevel]: 2 | ||
}) | ||
|
||
await run() | ||
|
||
expect(artifact.uploadArtifact).toHaveBeenCalledWith( | ||
fixtures.artifactName, | ||
fixtures.filesToUpload, | ||
fixtures.tmpDirectory, | ||
{compressionLevel: 2} | ||
) | ||
}) | ||
|
||
it('supports custom retention days', async () => { | ||
mockInputs({ | ||
[Inputs.RetentionDays]: 7 | ||
}) | ||
|
||
await run() | ||
|
||
expect(artifact.uploadArtifact).toHaveBeenCalledWith( | ||
fixtures.artifactName, | ||
fixtures.filesToUpload, | ||
fixtures.tmpDirectory, | ||
{retentionDays: 7, compressionLevel: 6} | ||
) | ||
}) | ||
|
||
it('supports deleting artifacts after merge', async () => { | ||
mockInputs({ | ||
[Inputs.DeleteMerged]: true | ||
}) | ||
|
||
await run() | ||
|
||
for (const a of fixtures.artifacts) { | ||
expect(artifact.deleteArtifact).toHaveBeenCalledWith(a.name) | ||
} | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# `@actions/upload-artifact/merge` | ||
|
||
Merge multiple [Actions Artifacts](https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts) in Workflow Runs. Internally powered by [@actions/artifact](https://github.com/actions/toolkit/tree/main/packages/artifact) package. | ||
|
||
- [`@actions/upload-artifact/merge`](#actionsupload-artifactmerge) | ||
- [Usage](#usage) | ||
- [Inputs](#inputs) | ||
- [Outputs](#outputs) | ||
- [Examples](#examples) | ||
|
||
## Usage | ||
|
||
> [!IMPORTANT] | ||
> upload-artifact/merge@v4+ is not currently supported on GHES. | ||
Note: this actions can only merge artifacts created with actions/upload-artifact@v4+ | ||
|
||
### Inputs | ||
|
||
```yaml | ||
- uses: actions/upload-artifact/merge@v4 | ||
with: | ||
# The name of the artifact that the artifacts will be merged into | ||
# Optional. Default is 'merged-artifacts' | ||
name: | ||
|
||
# A glob pattern matching the artifacts that should be merged. | ||
# Optional. Default is '*' | ||
pattern: | ||
|
||
# If true, the artifacts will be merged into separate directories. | ||
# If false, the artifacts will be merged into the root of the destination. | ||
# Optional. Default is 'false' | ||
separate-directories: | ||
|
||
# If true, the artifacts that were merged will be deleted. | ||
# If false, the artifacts will still exist. | ||
# Optional. Default is 'false' | ||
delete-merged: | ||
|
||
# Duration after which artifact will expire in days. 0 means using default retention. | ||
# Minimum 1 day. | ||
# Maximum 90 days unless changed from the repository settings page. | ||
# Optional. Defaults to repository settings. | ||
retention-days: | ||
|
||
# The level of compression for Zlib to be applied to the artifact archive. | ||
# The value can range from 0 to 9. | ||
# For large files that are not easily compressed, a value of 0 is recommended for significantly faster uploads. | ||
# Optional. Default is '6' | ||
compression-level: | ||
``` | ||
### Outputs | ||
| Name | Description | Example | | ||
| - | - | - | | ||
| `artifact-id` | GitHub ID of an Artifact, can be used by the REST API | `1234` | | ||
| `artifact-url` | URL to download an Artifact. Can be used in many scenarios such as linking to artifacts in issues or pull requests. Users must be logged-in in order for this URL to work. This URL is valid as long as the artifact has not expired or the artifact, run or repository have not been deleted | `https://github.com/example-org/example-repo/actions/runs/1/artifacts/1234` | | ||
|
||
## Examples | ||
|
||
TODO(robherley): add examples |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
name: 'Merge Build Artifacts' | ||
description: 'Merge one or more build Artifacts' | ||
author: 'GitHub' | ||
inputs: | ||
name: | ||
description: 'The name of the artifact that the artifacts will be merged into.' | ||
required: true | ||
default: 'merged-artifacts' | ||
pattern: | ||
description: 'A glob pattern matching the artifact names that should be merged.' | ||
default: '*' | ||
separate-directories: | ||
description: 'When multiple artifacts are matched, this changes the behavior of how they are merged in the archive. | ||
If true, the matched artifacts will be extracted into individual named directories within the specified path. | ||
If false, the matched artifacts will combined in the same directory.' | ||
default: 'false' | ||
retention-days: | ||
description: > | ||
Duration after which artifact will expire in days. 0 means using default retention. | ||
Minimum 1 day. | ||
Maximum 90 days unless changed from the repository settings page. | ||
compression-level: | ||
description: > | ||
The level of compression for Zlib to be applied to the artifact archive. | ||
The value can range from 0 to 9: | ||
- 0: No compression | ||
- 1: Best speed | ||
- 6: Default compression (same as GNU Gzip) | ||
- 9: Best compression | ||
Higher levels will result in better compression, but will take longer to complete. | ||
For large files that are not easily compressed, a value of 0 is recommended for significantly faster uploads. | ||
default: '6' | ||
delete-merged: | ||
description: > | ||
If true, the artifacts that were merged will be deleted. | ||
If false, the artifacts will still exist. | ||
default: 'false' | ||
|
||
outputs: | ||
artifact-id: | ||
description: > | ||
A unique identifier for the artifact that was just uploaded. Empty if the artifact upload failed. | ||
This ID can be used as input to other APIs to download, delete or get more information about an artifact: https://docs.github.com/en/rest/actions/artifacts | ||
artifact-url: | ||
description: > | ||
A download URL for the artifact that was just uploaded. Empty if the artifact upload failed. | ||
This download URL only works for requests Authenticated with GitHub. Anonymous downloads will be prompted to first login. | ||
If an anonymous download URL is needed than a short time restricted URL can be generated using the download artifact API: https://docs.github.com/en/rest/actions/artifacts#download-an-artifact | ||
This URL will be valid for as long as the artifact exists and the workflow run and repository exists. Once an artifact has expired this URL will no longer work. | ||
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/merge/index.js' |
Oops, something went wrong.