Skip to content

Artifact upload: support uploading single un-zipped files#2256

Merged
danwkennedy merged 15 commits intomainfrom
danwkennedy/artifact-upload-support-single-unzipped
Feb 25, 2026
Merged

Artifact upload: support uploading single un-zipped files#2256
danwkennedy merged 15 commits intomainfrom
danwkennedy/artifact-upload-support-single-unzipped

Conversation

@danwkennedy
Copy link
Contributor

@danwkennedy danwkennedy commented Jan 26, 2026

Description

Sometimes we want to upload un-zipped files:

  • Because the file is already compressed: we recommend uploading tar files to handle file permissions. Zipping those files is unhelpful since it requires double decompressing.
  • Browsers can handle displaying common file types directly without needing to download the file to disk (think pdf, png/jpg, json, html, etc.). To support something like that in the UI, we can't have them zipped
  • Some users only have a single file to upload. Zipping that is somewhat redundant and just causes more clicks to view/use the file.
  • Mobile browsers don't really support zipped files (at least not well)

This PR adds support for those scenarios.

The new CI run for this PR demonstrates the browser behavior for this. Note that this is enabled for this repo only, please wait until the feature is fully rolled out before attempting elsewhere.

Screen.Recording.2026-02-24.at.14.27.32.mov

Changes:

  • Support skipping the zip step when uploading a single file
  • Support passing the mime type when we create the artifact
  • Added tests to the CI to validate

Notes

  • Only single file uploads are supported right now (future updates can add support for multiple file uploads).
  • When skipArchive is true, the name parameter is replaced with the file name.
  • The old zip flow should be fully supported with this change if skipArchive is false. Notably, this'll mean the file type is not present in the artifact name. The mime type passed will still be application/zip, though.

@danwkennedy danwkennedy requested a review from a team as a code owner January 26, 2026 21:40
Copilot AI review requested due to automatic review settings January 26, 2026 21:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds support for uploading single files without archiving them into a zip file. The feature introduces a new skipArchive option to UploadArtifactOptions that, when enabled, uploads a single file directly with its original MIME type instead of creating a zip archive.

Changes:

  • Added skipArchive option to allow uploading single uncompressed files
  • Introduced MIME type detection and transmission to the artifact service
  • Refactored stream handling code into a shared module for reusability
  • Updated artifact API version from 4 to 7 to support the new MIME type field

Reviewed changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/artifact/src/internal/upload/types.ts New file providing MIME type mapping for various file extensions
packages/artifact/src/internal/upload/stream.ts New file containing extracted and new stream utilities, including WaterMarkedUploadStream class and createRawFileUploadStream function
packages/artifact/src/internal/upload/zip.ts Refactored to use WaterMarkedUploadStream from the new stream module
packages/artifact/src/internal/upload/blob-upload.ts Renamed uploadZipToBlobStorage to uploadToBlobStorage and added contentType parameter
packages/artifact/src/internal/upload/upload-artifact.ts Main logic for skipArchive feature including validation, file handling, and artifact name adjustment
packages/artifact/src/internal/shared/interfaces.ts Added skipArchive option to UploadArtifactOptions interface with documentation
packages/artifact/src/generated/results/api/v1/artifact.ts Generated protobuf code with mimeType field added to CreateArtifactRequest, removed migration-related types, and updated RepeatType
packages/artifact/src/generated/results/api/v1/artifact.twirp-client.ts Minor formatting change (trailing newline)
packages/artifact/tests/upload-artifact.test.ts Comprehensive test coverage for skipArchive feature including multiple file validation, raw file upload, MIME type handling, and artifact naming
Comments suppressed due to low confidence (1)

packages/artifact/src/internal/upload/upload-artifact.ts:35

  • The validation for skipArchive doesn't check if the files array is empty. If files.length is 0, the error won't be thrown, but then files[0] will be undefined on line 41, leading to unexpected behavior. Add a check for files.length === 0 alongside the files.length > 1 check.
    if (files.length > 1){

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@crazy-max
Copy link

Thanks for this! The ability to upload single un-zipped files with the appropriate MIME type will unlock real workflows where ZIP packaging is actively harmful. I previously opened #1874, which aimed at improving download behavior when artifacts are not zipped. That PR was ultimately closed, but it surfaced a real pain point: today the toolkit and actions/download-artifact assume ZIP-formatted content, leading to broken downloads if the source isn't zipped.

While here it focuses on the upload side, it aligns with that same broader goal: support first-class non-zip artifacts. Together, these capabilities would cover both ends of the artifact lifecycle for non-zipped files and improve compatibility with existing external producers/consumers of artifacts. For example, workflows using gzip blobs (e.g., docker/build-push-action) currently fail during download because the toolkit can't extract non-zip content. So I guess the download case will be addressed as follow-up?

*/
export function getMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase()
return mimeTypes[ext] || 'application/octet-stream'

Choose a reason for hiding this comment

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

Would it be possible to have an extra opt to enforce the desired mime type? In our case we are uploading a .dockerbuild file that is application/gzip type.

Copy link

@crazy-max crazy-max Feb 2, 2026

Choose a reason for hiding this comment

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

Something similar to crazy-max@00e8704 so we can call uploadArtifact directly in crazy-max/docker-actions-toolkit@8027d57#diff-a248ff5ec76368168c745273fee0601b243545fdca37aa3bec76ef9e9365fdb7R172 as with ESM only we can't use internal helpers anymore: docker/actions-toolkit#968 (comment)

Choose a reason for hiding this comment

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

Or maybe it is fine to have application/octet-stream if this is just use as content type in azure blob storage?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Heh, I was literally going to ask that? I think it's actually ok to have an option for specifying though. I hadn't thought of files with no extension. I'm trying to come up with another example though, .dockerbuild would probably work fine since I'd expect us to default to a basic binary stream 🙂 . The types mostly help with specifying how the browser should handle the download in the UI: a PDF or PNG will be opened in a tab in the browser instead of downloading, etc.

Choose a reason for hiding this comment

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

I think it's actually ok to have an option for specifying though.

Would be great!

I hadn't thought of files with no extension.

Actually .dockerbuild is the file extension 😅

For example one of the artifact in this run: https://github.com/docker/build-push-action/actions/runs/21586122185

image

Like: https://github.com/docker/build-push-action/actions/runs/21586122185/artifacts/5341267407

The types mostly help with specifying how the browser should handle the download in the UI: a PDF or PNG will be opened in a tab in the browser instead of downloading, etc.

Ah ok that makes sense! So maybe a contentType opt is not necessary then.

@crazy-max
Copy link

@danwkennedy Is there any sense of timing for when this might move forward? This PR would unblock docker/actions-toolkit#972 (comment). Thanks 🙏

@danwkennedy
Copy link
Contributor Author

@crazy-max sorry for the wait. We're going to pick up the rollout next week at the earliest.

@crazy-max
Copy link

@crazy-max sorry for the wait. We're going to pick up the rollout next week at the earliest.

Awesome thx!

@danwkennedy danwkennedy force-pushed the danwkennedy/artifact-upload-support-single-unzipped branch from 15fbe0f to d584ea9 Compare February 24, 2026 16:46
@danwkennedy danwkennedy requested a review from a team as a code owner February 24, 2026 17:40
Copy link
Member

@philip-gai philip-gai left a comment

Choose a reason for hiding this comment

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

Some comments, lmk if you have any questions! 🚀

@danwkennedy danwkennedy merged commit 6fe3c0f into main Feb 25, 2026
24 of 27 checks passed
@crazy-max
Copy link

@danwkennedy Related to #2256 (review) I made some tests on this repo https://github.com/crazy-max/upload-artifact-skiparchive/actions/runs/22435286802 using latest changes from main of https://github.com/actions/upload-artifact.

jobs:
  json:
    runs-on: ubuntu-latest
    steps:
      -
        name: Checkout
        uses: actions/checkout@v6
      -
        name: Upload artifact
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
        with:
          name: github-repo-1c96c578b3.json
          path: github-repo-1c96c578b3.json
          archive: false

  dockerbuild:
    runs-on: ubuntu-latest
    steps:
      -
        name: Checkout
        uses: actions/checkout@v6
      -
        name: Upload artifact
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
        with:
          name: docker~actions-toolkit~LF571I.dockerbuild
          path: docker~actions-toolkit~LF571I.dockerbuild
          archive: false

It seems to work fine for the json job and downloaded artifact is not a zip one as expected: https://github.com/crazy-max/upload-artifact-skiparchive/actions/runs/22435286802/artifacts/5669038505 but for the dockerbuild job with a .dockerbuild file it still makes a zip archive: https://github.com/crazy-max/upload-artifact-skiparchive/actions/runs/22435286802/artifacts/5669038532

@danwkennedy
Copy link
Contributor Author

@crazy-max Could you rerun the whole pipeline? I've enabled the feature on that repo explicitly (I know the json flow worked but I want to make sure). This time, enable debug logs on the action as well (there's a checkbox on the rerun popup that'll do it for you).

@crazy-max
Copy link

@crazy-max Could you rerun the whole pipeline? I've enabled the feature on that repo explicitly (I know the json flow worked but I want to make sure). This time, enable debug logs on the action as well (there's a checkbox on the rerun popup that'll do it for you).

@danwkennedy Thanks! Looks good after re-run: https://github.com/crazy-max/upload-artifact-skiparchive/actions/runs/22435286802

Artifact: https://github.com/crazy-max/upload-artifact-skiparchive/actions/runs/22435286802/artifacts/5672814224

@danwkennedy
Copy link
Contributor Author

@crazy-max cool, I haven't fully rolled out the final flag (it's a percentage right now so I don't know which repos have it and which don't). The request/response look correct when I tested it in the browser:

Screenshot 2026-02-26 at 08 40 02

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants