Tech.euDisplay.dev raises €470K to power document collaboration for AI agentsDisplay.dev raises €470KRead coverage

From GitHub Actions to Authenticated URL: A Better Way to Share CI Artifacts

CI produces HTML worth reading by people who never open GitHub: coverage reports, Playwright runs, build previews, agent-authored dossiers. GitHub's own artifact storage locks those behind a login and a 90-day clock. Thi

From GitHub Actions to Authenticated URL: A Better Way to Share CI Artifacts
TL;DR
Replace `actions/upload-artifact` (requires a GitHub login, expires in 90 days) with `dsp publish` (a URL that never changes, company SSO, readable by PMs). One step change to your GitHub Actions workflow, and the report opens in a browser instead of downloading as a zip.

CI produces HTML worth reading by people who never open GitHub: coverage reports, Playwright runs, build previews, agent-authored dossiers. GitHub's own artifact storage locks those behind a login and a 90-day clock. This guide swaps one workflow step so the report lands at a company-gated URL that renders in the browser and never expires.


The problem with GitHub Actions artifact storage

GitHub Actions stores build artifacts in a zip archive. To access them:

  1. Go to the Actions run in GitHub
  2. Find the artifact section
  3. Click Download
  4. Extract the zip
  5. Open the HTML file locally

This works for engineers. It's unusable for everyone else.

Your PM can't access private repository Actions without a GitHub account. Your QA manager shouldn't need to navigate GitHub's CI interface to read a test report. Your VP shouldn't be extracting zip files to see a build summary.

And after 90 days, the artifact is gone. GitHub retains Actions artifacts for 90 days by default on public repos (up to 400 configurable on private repos), per the GitHub Actions usage-limits docs. Any link shared in a Slack thread, Notion document, or Jira ticket goes dead when that window closes.


The one-line change

Before:

- uses: actions/upload-artifact@v4
  with:
    name: research-dossier
    path: ./research-dossier/

After:

- name: Publish dossier
  run: |
    npm install -g @displaydev/cli
    dsp publish ./research-dossier/ --name "dossier-${{ github.run_id }}"
  env:
    DISPLAYDEV_API_KEY: ${{ secrets.DISPLAYDEV_API_KEY }}

The URL never changes, requires a company email to access, and renders in the browser without downloading.


Full workflow example

name: Test and Publish
 
on: [push, pull_request]
 
jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Lint
        run: npm run lint
      
      - name: Run tests
        run: npm test -- --coverage
        continue-on-error: true
      
      - name: Build
        run: npm run build
      
      - name: Publish test coverage report
        if: always()
        run: |
          npm install -g @displaydev/cli
          # dsp publish prints the URL on stdout line 1 and a status line on line 2;
          # head -n 1 captures just the URL.
          COVERAGE_URL=$(dsp publish ./coverage/lcov-report/ \
            --name "coverage-${{ github.sha }}" | head -n 1)
          echo "COVERAGE_URL=$COVERAGE_URL" >> $GITHUB_ENV
        env:
          DISPLAYDEV_API_KEY: ${{ secrets.DISPLAYDEV_API_KEY }}
      
      - name: Comment on PR
        if: always() && github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `📊 **Coverage report:** ${process.env.COVERAGE_URL}`
            })

Use cases this covers

Agent-authored artifacts: Multi-page research dossiers, market maps, and competitive landscapes generated by a coding agent in CI. With more than half of professional developers now using AI coding tools daily (Stack Overflow 2025 Developer Survey), more of what CI generates is agent-written HTML meant for non-engineers to read.

Build outputs: Preview builds for non-engineers. PMs and designers can review what's shipping without needing a local development environment.

Scheduled report generation: Nightly builds, weekly analytics exports, automated competitive monitoring reports, anything a scheduled workflow generates and stakeholders need to read.


Viewer experience

The PR comment appears with a URL. The QA lead clicks it, signs in with Google in one click, and sees the full interactive test report in their browser. No GitHub account, no zip file, no 90-day expiry.

The URL persists in Slack threads, Notion documents, and Jira tickets indefinitely.


Cost comparison

GitHub Actions artifact storagedisplay.dev
Viewer needs GitHub account
Viewer needs private repo access
Artifact expiry90 daysNever
Browser-renderable (no download)
Company SSO
Monthly cost$0 (included)€49 flat

FAQ

Can I publish only on failure?+

Yes. Use if: failure() in the step condition. The artifact is only published when the workflow fails.

How long are display.dev artifacts retained?+

Indefinitely, until you delete them. No expiry.

What about other CI systems (GitLab, CircleCI, Jenkins)?+

The display CLI works anywhere Node.js runs. For GitLab CI: yaml publish: stage: report script: - npm install -g @displaydev/cli - dsp publish ./report/ --name "ci-$CI_PIPELINE_ID" variables: DISPLAYDEV_API_KEY: $DISPLAYDEV_API_KEY Same pattern for CircleCI and Jenkins: install the CLI, call dsp publish.

How do I keep one stable URL instead of a new link per run?+

Publish to a fixed artifact ID instead of a per-run name. Store the shortId in a repo variable and update in place: yaml - run: | npm install -g @displaydev/cli BASE_VERSION=$(dsp get "${{ vars.DISPLAY_ARTIFACT_ID }}" | jq -r .currentVersion) dsp publish ./coverage/lcov-report/ \ --id "${{ vars.DISPLAY_ARTIFACT_ID }}" --base-version "$BASE_VERSION" env: DISPLAYDEV_API_KEY: ${{ secrets.DISPLAYDEV_API_KEY }} The URL stays the same; every run adds a version to history (50 versions on Pro). Use a per-run --name when you want a distinct URL per commit (as in the PR-comment example above), and a fixed --id for a single "latest report" link.

What token should CI use, and is it safe?+

A scoped API key stored in DISPLAYDEV_API_KEY (a repo secret). Scoped keys are a Pro capability, so CI can publish without a human dsp login. The key only grants publish access; viewers still authenticate with their own company accounts.

Who can open the published report?+

Anyone with an email on your company domain, after they sign in with Google or Microsoft (Pro) or a one-time passcode (Free). The QA lead, PM, or VP opens the link with no GitHub account and no repo access.

Publish your first artifact in 15 seconds.

Free tier. No credit card. One-time password auth for viewers on free, Google + Microsoft SSO on Pro (€49/month flat).

Get started free →See pricing