Run the screenshot CLI in CI

This guide puts the rs CLI into a GitHub Actions workflow and a GitLab CI job. Both install a pinned release, check its SHA-256 checksum, read the API key from a CI secret and keep the screenshots as build artifacts. For other guides, go back to the guides hub.

Tested on 2026-10-15 with rs v0.1.2 (rs_linux_amd64.tar.gz) in alpine:3.20 and ubuntu:24.04 containers. The workflow file passed actionlint 1.7.7.

What you need

  • An API key that starts with rs_live_. Create one after you sign up.
  • The key stored as a CI secret named RS_API_KEY. The CLI reads this variable before its config file, as the CLI authentication page explains.

Install a pinned release, not latest

The release assets are archives named rs_<os>_<arch>.tar.gz, with a checksums.txt file beside them. Pin the version in one variable and check the archive before you run it. A pinned version keeps a new CLI release from changing a build that passed yesterday.

RS_VERSION=v0.1.2
base="https://github.com/Render-Screenshot/rs-cli/releases/download/${RS_VERSION}"
curl -fsSLO "${base}/rs_linux_amd64.tar.gz"
curl -fsSLO "${base}/checksums.txt"
grep ' rs_linux_amd64.tar.gz$' checksums.txt | sha256sum -c -
tar -xzf rs_linux_amd64.tar.gz rs
sudo install -m 0755 rs /usr/local/bin/rs
rs --version

The binary is statically linked, so the same archive runs on Ubuntu runners and on Alpine images. On an ARM runner, use rs_linux_arm64.tar.gz.

GitHub Actions

Save this as .github/workflows/screenshots.yml:

name: Screenshots

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  capture:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    env:
      RS_VERSION: v0.1.2
    steps:
      - name: Install the rs CLI (pinned, checksum checked)
        run: |
          set -euo pipefail
          base="https://github.com/Render-Screenshot/rs-cli/releases/download/${RS_VERSION}"
          curl -fsSLO "${base}/rs_linux_amd64.tar.gz"
          curl -fsSLO "${base}/checksums.txt"
          grep ' rs_linux_amd64.tar.gz$' checksums.txt | sha256sum -c -
          tar -xzf rs_linux_amd64.tar.gz rs
          sudo install -m 0755 rs /usr/local/bin/rs
          rs --version

      - name: Capture screenshots
        env:
          RS_API_KEY: ${{ secrets.RS_API_KEY }}
        run: |
          set -euo pipefail
          if [ -z "${RS_API_KEY:-}" ]; then
            echo "::notice::RS_API_KEY is not available to this run (for example a pull request from a fork). Skipping."
            exit 0
          fi
          mkdir -p screenshots
          rs take https://example.com --preset og_card -o screenshots/home-og.png --quiet
          rs take https://example.com --device iphone_14_pro -o screenshots/home-mobile.png --quiet
          test -s screenshots/home-og.png
          test -s screenshots/home-mobile.png

      - name: Upload screenshots
        if: ${{ hashFiles('screenshots/*.png') != '' }}
        uses: actions/upload-artifact@v4
        with:
          name: screenshots
          path: screenshots/
          retention-days: 7

Three choices in this file matter for secrets:

  • The key is set as env on the one step that needs it. The install step never sees it.
  • permissions: contents: read limits what the job token can do.
  • GitHub does not pass repository secrets to runs for pull requests from forks. The -z check turns that case into a notice instead of a failed build.

GitLab CI

Add a CI/CD variable named RS_API_KEY in Settings > CI/CD > Variables. Mark it Masked so the value is hidden in job logs. Mark it Protected if only protected branches may spend credits. A protected variable is empty on other branches, so the job below runs only on the default branch.

screenshots:
  image: alpine:3.20
  variables:
    RS_VERSION: v0.1.2
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  before_script:
    - apk add --no-cache curl
    - base="https://github.com/Render-Screenshot/rs-cli/releases/download/${RS_VERSION}"
    - curl -fsSLO "${base}/rs_linux_amd64.tar.gz"
    - curl -fsSLO "${base}/checksums.txt"
    - grep ' rs_linux_amd64.tar.gz$' checksums.txt | sha256sum -c -
    - tar -xzf rs_linux_amd64.tar.gz rs
    - install -m 0755 rs /usr/local/bin/rs
  script:
    - mkdir -p screenshots
    - rs take https://example.com --preset og_card -o screenshots/home-og.png --quiet
    - test -s screenshots/home-og.png
  artifacts:
    paths:
      - screenshots/
    expire_in: 1 week

Exit codes we measured

We ran rs v0.1.2 in an Alpine container with three key states:

Key state Exit code Output
RS_API_KEY not set and no config file 2 Error: no API key configured. Run 'rs login' or set RS_API_KEY
Key set, API rejects it (HTTP 401) 1 Nothing, even with --verbose
Valid key, page captured 0 The file you named with -o

A non-zero exit fails the step, so a bad key cannot pass the build. Because a rejected key is silent in this version, the troubleshooting table starts with the key.

The same request without the CLI

If your CI image cannot download binaries, send the request with curl. The GET form:

curl -sS --fail \
  -H "Authorization: Bearer $RS_API_KEY" \
  "https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com&preset=og_card" \
  -o home-og.png

The POST form with a JSON body:

curl -sS --fail -X POST https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "preset": "og_card"}' \
  -o home-og.png

--fail makes curl exit 22 on an HTTP error and keeps the error body out of your PNG file. Remove it while you debug to see the JSON error.

Troubleshooting

Symptom Cause Fix
Step exits 1 with no output The API rejected the key Check that the secret name is RS_API_KEY, that the key starts with rs_live_ and that it was not revoked.
Exit 2, "no API key configured" The variable is empty in this job On GitHub, check fork pull requests and the step env. On GitLab, check protected-variable rules.
sha256sum: WARNING: 1 computed checksum did NOT match Corrupt or changed download Do not install it. Run the job again; if it still fails, check the release page.
429 errors in a matrix build Too many parallel jobs for your plan Lower the matrix max-parallel value. See rate limits.

Limits to plan for

  • Each capture renders fresh and uses one credit, unless you opt in to cached results (caching). A scheduled workflow that runs every hour on 10 URLs uses 240 credits a day.
  • A page has at most 60 seconds to load (wait.timeout).
  • Requests per minute and concurrent requests depend on your plan, listed on the rate limits page.

Related in this topic

Was this page helpful?