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.
Choosing how long a report lives
Every upload expires. Omit the expiry fields and the report serves for 14 days, which suits most nightly output. Ask for a longer window when a report backs a release:
curl --fail-with-body -X POST "https://durablefile.com/v1/upload?expires_after_hours=4320" \
-H "Authorization: Bearer $DURABLEFILE_KEY" \
-F "file=@nightly-report.pdf"
That is the 180-day maximum. A nightly job that re-uploads the same artifact renews the window on every run, so an actively maintained report keeps its URL alive without extra calls. The full contract is in the retention 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.