Drop-in replacements for actions/upload-artifact and actions/download-artifact
that store artifacts on Devino's own MinIO (https://storage.devino.ca, bucket
gh-artifacts) instead of GitHub's metered artifact storage.
permissions:
id-token: write # required: the action authenticates with the job's OIDC token
steps:
- uses: DevinoSolutions/artifact/upload@v1
with:
name: playwright-report
path: playwright-report/
retention-days: 7
- uses: DevinoSolutions/artifact/download@v1
with:
pattern: release-evidence-*
merge-multiple: true
path: release-evidenceThe org is on GitHub's free plan (500 MB of artifact storage, hard-capped by a
$0 Actions budget). Once the quota is crossed every upload-artifact step is
rejected. Self-hosted runners do not help: the upstream action always writes
to GitHub's blob store. This action writes to devino instead.
-
The job's GitHub OIDC token (audience
storage.devino.ca) is exchanged with MinIO STS (AssumeRoleWithWebIdentity) for one-hour credentials. MinIO picks the policy named after the token'srepository_owner_idclaim, so only workflows owned by DevinoSolutions get access; tokens from any other owner map to no policy and are refused. -
A pinned
mc(MinIO client) is fetched fromstorage.devino.ca/tools/and cached in the runner tool cache. Two fallbacks follow the org mirror:dl.min.io, and the release assets of the archivedgithub.com/minio/mcrepository. Whichever source answers, the bytes are checked against the per-platform sha256 pinned inlib/main.py(MC_SHA256) before the binary is installed; a mismatch is never installed and the next source is tried. If every source fails, the error names each URL with its own reason.dl.min.iohas answered410 Gonesince 2026-09-11/12 — MinIO archived the client and stopped serving those files. The org mirror is populated and is normally the source that answers, so the third entry exists for the case where the mirror itself is down: with the secondary retired there is no longer anything behind it. Re-pinMC_SHA256from each release's.sha256sumasset wheneverMC_VERSIONchanges. -
Upload: matched files are packed into one
.tgzwhose root mirrors upstream semantics (a single directory uploads its contents; several paths share their least common ancestor), then copied togh-artifacts/<owner>/<repo>/<run_id>/<name>.tgz. Large files are uploaded as 16 MiB multipart chunks, which keeps each request under Cloudflare's 100 MB limit. -
Download: fetches by
name, bypattern(withmerge-multiple), or all artifacts of the run, and extracts safely intopath.
Retention is enforced by bucket lifecycle rules: retention-days is rounded
up to 1/3/5/7/14/30/90 days and stored as an object tag (values above 90 are
capped at 90). When retention-days is not set the artifact expires after
14 days, which matches how the org's test reports and screenshots are used.
Every mc call is retried when it fails for a transport reason — connection
reset, refused or timed out, TLS handshake timeout, DNS failure, or a 5xx from
the endpoint. Five attempts, exponential backoff (2, 4, 8, 16 s) with +/- 25%
jitter. Both verbs are safe to repeat: cp writes a whole object under a key
derived from the run, and ls is read-only.
A real 404 (NoSuchKey, "object does not exist") and a rejected credential are
never retried — they are deterministic answers, so retrying only delays the
report. The download step also words its error after the real cause: only a
genuine 404 says Artifact not found. Anything else leads with mc's own
message, so a network failure is not mistaken for an upload that never
happened.
upload: name, path, if-no-files-found, retention-days,
compression-level, include-hidden-files, overwrite (accepted, always
overwrites). Outputs: artifact-id, artifact-url, artifact-digest.
download: name, path, pattern, merge-multiple, run-id,
repository, github-token (ignored). Output: download-path.
Both accept endpoint, bucket, access-key, secret-key to bypass OIDC.
Every step writes the s3:// location to the job summary. Fetch it with:
mc cp devino/gh-artifacts/<owner>/<repo>/<run_id>/<name>.tgz .or browse https://minio-console.devino.ca.
Python 3.8+ and bash (all GitHub-hosted images and the ubuntu-devino
ARC image qualify). Supported platforms: Linux x64/arm64, macOS x64/arm64,
Windows x64.