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 fix field and payment options; fund the account once with POST /v1/credit ($1 = 1 GiB, stacks) and the job runs indefinitely on that quota.