Skip to main content

gha-artifact-client

Python wrapper/CLI around @actions/artifact for creating, listing, deleting, and getting signed download URLs for workflow artifacts from inside a GitHub Actions job.

Allows you to upload, list, delete, and get signed download URLs for workflow artifacts dynamically from Python code without needing to invoke the actions/upload-artifact action or the GitHub REST API in your workflow yaml.

Notes

  • Uploading, listing, and deleting artifacts only works during the lifetime of a GitHub Actions job.
  • Unlike other GitHub API interactions requires a ACTIONS_RUNTIME_TOKEN and and not a GITHUB_TOKEN.
  • Since the artifact API is not publicly documented, this package vendors a custom-built node wrapper around the official @actions/artifact package, which is invoked with node. node needs to be provided by the user.
  • Only depends on the Python standard library.
  • Only direct single-file uploads are supported. If you need zip files, you need to create them yourself before uploading.

Usage

Python API

import io

from gha_artifact_client import ArtifactClientApi

# Credentials from environment variables (default)
api = ArtifactClientApi()

# Or supply credentials explicitly — useful when you don't want them
# sitting in os.environ where other subprocesses could inherit them
api = ArtifactClientApi(
    runtime_token="...",
    results_url="...",
)

# Upload a file from disk
result = api.upload_artifact("dist/package.tar.gz")

# Upload with a custom artifact name and expiry time
result = api.upload_artifact(
    "dist/package.tar.gz",
    name="build-output.tar.gz",
    expires_in=7 * 24 * 3600,  # 7 days from now, in seconds
)

# Or set an exact expiry datetime (must be timezone-aware)
import datetime as dt

result = api.upload_artifact(
    "dist/package.tar.gz",
    expires_at=dt.datetime(2026, 12, 31, 23, 59, 59, tzinfo=dt.timezone.utc),
)

print(result.id)
print(result.digest)

# Upload from in-memory bytes
result = api.upload_artifact_bytes(
    b"hello from memory\n",
    name="build-output.txt",
)

print(result.id)

# Upload using a file-like object
with open("dist/package.tar.gz", "rb") as f:
    result = api.upload_artifact_fileobj(f, name="package.tar.gz")

print(result.id)

# Delete an artifact by name
result = api.delete_artifact("package.tar.gz")

print(result.id)

# Get a pre-signed download URL for an artifact
result = api.get_signed_artifact_url("package.tar.gz")

print(result.url)

# List all artifacts for the current workflow job run
result = api.list_artifacts()

for artifact in result.artifacts:
    print(artifact.id, artifact.name, artifact.size)

CLI

# Upload
gha-artifact-client upload dist/package.tar.gz --name package.tar.gz --expires-in 604800

# Delete
gha-artifact-client delete package.tar.gz

# Get a pre-signed download URL
gha-artifact-client get-signed-url package.tar.gz

# List all artifacts
gha-artifact-client list

--expires-in takes seconds (int or float). Use --expires-at for an exact point in time as a timezone-aware ISO 8601 datetime. The two flags are mutually exclusive.

All subcommands accept --json to emit machine-readable output:

gha-artifact-client upload dist/package.tar.gz --json
# {"id": 42, "size": 1234, "digest": "sha256:..."}

gha-artifact-client delete package.tar.gz --json
# {"id": 42}

gha-artifact-client get-signed-url package.tar.gz --json
# {"url": "https://..."}

gha-artifact-client list --json
# {"artifacts": [{"id": 42, "name": "package.tar.gz", "size": 1234, "created_at": "2025-06-01T12:00:00+00:00", "digest": "sha256:..."}]}

Credentials default to ACTIONS_RUNTIME_TOKEN and ACTIONS_RESULTS_URL from the environment, but can be supplied explicitly:

gha-artifact-client --runtime-token "$MY_TOKEN" --results-url "$MY_RESULTS_URL" \
  upload dist/package.tar.gz

Credentials & Security Considerations

Uploading and deleting artifacts requires a URL and a credential that is only available inside a live GitHub Actions job:

  • ACTIONS_RUNTIME_TOKEN — a token created for the current job.
  • ACTIONS_RESULTS_URL — the endpoint for the artifact storage backend.

These are not the same as GITHUB_TOKEN and are not exposed as regular environment variables. They are only exposed to action steps and not run steps. To make them available in run steps you can extract them via actions/github-script:

permissions: {}

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Get artifact credentials
        id: vars
        uses: actions/github-script@v8
        with:
          script: |
            core.setOutput('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN'])
            core.setOutput('ACTIONS_RESULTS_URL', process.env['ACTIONS_RESULTS_URL'])

      - name: Upload artifact
        env:
          ACTIONS_RUNTIME_TOKEN: ${{ steps.vars.outputs.ACTIONS_RUNTIME_TOKEN }}
          ACTIONS_RESULTS_URL: ${{ steps.vars.outputs.ACTIONS_RESULTS_URL }}
        run: python your_script.py

Notes on the Token and Security

  • The token is valid for timeout-minutes of the current job, which defaults to 360 minutes (6 hours). After that, it expires and cannot be used to upload artifacts.

  • Even if the token hasn't expired, it appears to be invalidated after the job completes. Using it after the current job completes results in:

    Failed to CreateArtifact: Received non-retryable error: Failed request: (403) Forbidden: job is complete

  • From what I understand, the token can also be used to upload cache entries and job logs, but I haven't tested that. If you are passing them to third party code, consider the security implications of that. I'd recommend to remove the token from the environment when calling third-party code that doesn't need it, to avoid accidental leaks.

  • ACTIONS_RESULTS_URL for github.com on hosted runners, at the time of writing, is https://results-receiver.actions.githubusercontent.com/.

Notes on Names / IDs and Signed URLs

  • The reason the API seems to use names as the ID is that the blob backend identifies artifacts by name. The returned IDs are only relevant for the REST API.

  • Uploading an artifact with the same name as an existing one fails.

  • Deleting an artifact frees the name again, allowing you to upload a new artifact with the same name.

  • Signed URLs only sign the backend blob URL, which is the same for two artifacts with the same name. This means that if you upload an artifact, get a signed URL for it, delete the artifact, and then upload a new artifact with the same name, the previously obtained signed URL will work for the new artifact. Be cautious of this if you are using signed URLs and reusing artifact names.

Development

  • Install Python dependencies with uv sync.
  • Install node wrapper dependencies with npm ci in node-wrapper/.
  • Lint the node wrapper with npm run lint in node-wrapper/.
  • Type-check the node wrapper with npm run tsc in node-wrapper/.
  • Rebuild the vendored node wrapper with npm run build in node-wrapper/.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gha_artifact_client-0.2.3.tar.gz (384.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gha_artifact_client-0.2.3-py3-none-any.whl (310.4 kB view details)

Uploaded Python 3

File details

Details for the file gha_artifact_client-0.2.3.tar.gz.

File metadata

  • Download URL: gha_artifact_client-0.2.3.tar.gz
  • Upload date:
  • Size: 384.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.4

File hashes

Hashes for gha_artifact_client-0.2.3.tar.gz
Algorithm Hash digest
SHA256 ab210b51df15729cf85d72b34c154982770d4253271299fdc5cf866bacef60c0
MD5 f6c0811df540cffeb15549624b0ff161
BLAKE2b-256 b4c9b7357c956d4570af92b50a79fe1d847a7d77fd3b6b83701957b56cc77d15

See more details on using hashes here.

File details

Details for the file gha_artifact_client-0.2.3-py3-none-any.whl.

File metadata

File hashes

Hashes for gha_artifact_client-0.2.3-py3-none-any.whl
Algorithm Hash digest
SHA256 c0f58da5664e6e3ee9ec7a77764c00d1f6e55ce165fd94128009e7d3ea8b7d59
MD5 908ef2da96cd63e7277c6b6fb30a670b
BLAKE2b-256 df51ee521e7c6f809a0863fcdfbaca5eccf1c351b25087b4f9bad31d35c54f87

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.3 This release

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page