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 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.