CI artifacts are awkward to share. Native artifact storage expires, needs a login to download, or produces a URL three redirects deep inside the CI provider. When the consumer of a report is a customer, a ticket thread, or another automated system, what you want is one plain URL that serves the file directly.
That is a one-liner in any CI system that has curl.
The upload step
DurableFile accepts multipart uploads and raw bytes. For a PDF report from a test run:
curl --fail-with-body -X POST https://durablefile.com/v1/upload \
-H "Authorization: Bearer $DURABLEFILE_KEY" \
-F "file=@test-report.pdf"
For a build log, raw bytes work without a form. Send the filename in the
X-Filename header:
curl --fail-with-body -X POST https://durablefile.com/v1/upload \
-H "Authorization: Bearer $DURABLEFILE_KEY" \
-H "Content-Type: text/plain" \
-H "X-Filename: build-4812.log" \
--data-binary @build.log
Both return the same shape:
{
"hash": "…",
"status": "live",
"public_url": "https://f.durablefile.com/f/…/build-4812.log"
}
The URL serves immediately with Content-Disposition: attachment, so a
browser downloads the file instead of rendering it.
A GitHub Actions job
jobs:
publish-report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests and write the report
run: ./run-tests.sh --pdf test-report.pdf
- name: Publish the report
env:
DURABLEFILE_KEY: ${{ secrets.DURABLEFILE_KEY }}
run: |
RESPONSE="$(curl --fail-with-body -s -X POST https://durablefile.com/v1/upload \
-H "Authorization: Bearer $DURABLEFILE_KEY" \
-F "file=@test-report.pdf")"
echo "Report: $(echo "$RESPONSE" | jq -r .public_url)" >> "$GITHUB_STEP_SUMMARY"
Put the API key in the repository secrets, never in the workflow file. The job summary then carries a link anyone can open without a CI login.
A cron job is the same call with the output of whatever the job produced. There is no session to refresh and no token to rotate on a schedule — the bearer key is the whole authentication model.
Why retries are safe in CI
CI steps re-run. DurableFile addresses files by the SHA-256 of their bytes,
so uploading the same report twice returns HTTP 200 with
"deduplicated": true, the same URL, and no second charge against quota. A
flaky network step can simply run the upload again. The details are in
the content-addressing article.
Reports that should not live forever
Some CI output is only useful for a sprint. Give the upload an expiry and the link stops serving on schedule:
curl --fail-with-body -X POST "https://durablefile.com/v1/upload?unpublish_after_hours=720" \
-H "Authorization: Bearer $DURABLEFILE_KEY" \
-F "file=@nightly-report.pdf"
The bytes are kept while unpublished, and the same URL can come back later with a republish call. The full contract is in the lifecycle article.
Two cautions
- Every uploaded file is public to anyone with the URL. CI logs love to leak environment variables — strip or mask secrets before publishing a log.
- The account needs paid quota before the first upload. An unpaid upload
returns HTTP 402 with a
fixfield and payment options; fund the account once withPOST /v1/credit($1 = 1 GiB, stacks) and the job runs indefinitely on that quota.