GitHub Actions
Render App Store screenshots and icons in GitHub Actions with Open Assets: validate, render incrementally with assets.lock, then commit or upload the PNGs.
Open Assets runs anywhere Node and Chromium run, so a GitHub Actions job is a few steps: install, validate, render, then keep the output.
The minimal workflow
This is the workflow from the README. It renders on every push that touches the asset project and uploads the PNGs as a build artifact:
name: Export Marketing Assets
on:
push:
paths:
- 'marketing/screenshots/**'
jobs:
export:
runs-on: ubuntu-latest
defaults:
run:
working-directory: marketing/screenshots
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx open-assets validate
- run: npx open-assets render --json --quiet
- uses: actions/upload-artifact@v4
with:
name: screenshots
path: marketing/screenshots/exports/The steps:
npm ciinstalls@open-assets/open-assets, which installs Puppeteer, which downloads a matching Chrome build.validateexits with code 1 ifassets.jsonis invalid, a template file is missing, or a requested locale is not defined. The job fails before any rendering.render --json --quietsuppresses progress logs and prints one JSON object at the end. On failure it printsError: ...and exits 1.
render has no --ci flag. The --ci flag (and the CI environment variable) only applies to open-assets dev, where it disables the browser and logs. For renders, --json --quiet is the CI combination.
The JSON output
render --json prints:
{
"ok": true,
"assets": [
{
"collection": "screenshots",
"template": "01-hero",
"locale": "en",
"path": "/home/runner/work/app/app/exports/screenshots/en/iphone-6.9/01-hero.png",
"width": 1320,
"height": 2868,
"size": 812345
}
],
"skipped": 31,
"elapsed": "4.2s"
}path is absolute, size is in bytes, and skipped counts variants that were unchanged. Pipe it to jq to drive later steps, for example jq '.assets | length' to see whether anything was rendered.
Only render what changed
assets.lock makes renders incremental in CI too. A variant is skipped when the template's checksum, its export config and its output path all match the lockfile, and the output file exists. So the cache only helps in CI if both of these are committed:
assets.lock- the rendered files (
exports/, or whereveroutFilepoints)
If you upload artifacts instead of committing renders, every CI run starts with no output files and renders everything. That is correct, just slower.
See Lockfile for what the checksum covers. In short: the template plus files it references directly by relative path. Changes to localizations.json, or to images referenced from inside CSS, are not detected. Add a --force step when those change, or trigger on them separately.
Commit renders back to the branch
This workflow renders on pull requests and pushes the results to the PR branch, so reviewers see the new screenshots as image diffs in the "Files changed" tab:
name: Render assets
on:
pull_request:
paths:
- 'assets/**'
- 'public/**'
- 'src/**'
- 'assets.json'
- 'localizations.json'
permissions:
contents: write
jobs:
render:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Cache Chrome for Puppeteer
uses: actions/cache@v4
with:
path: ~/.cache/puppeteer
key: puppeteer-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci
- run: npx open-assets validate
# Tailwind projects: compile CSS first (see the examples' "render" script)
- run: npx @tailwindcss/cli -i src/styles.css -o dist/styles.css --minify
- run: npx open-assets render --json --quiet > render.json
- name: Commit renders
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add assets.lock exports/
git diff --cached --quiet || git commit -m "chore(assets): render"
git pushNotes:
~/.cache/puppeteeris Puppeteer's default download location for Chrome. Caching it saves the download on each run. The key uses the lockfile, so a Puppeteer upgrade gets a fresh browser.- The checkout uses the PR branch (
github.head_ref) so the push lands on it. This works for branches in the same repo, not for forks. - If
localizations.jsonchanged, a plain render skips unchanged templates. Userender --forcein this workflow if your PRs often change only strings.
Upload artifacts instead
If you would rather not commit PNGs, upload them and download the artifact from the run page:
- run: npx open-assets render --json --quiet
- uses: actions/upload-artifact@v4
with:
name: marketing-assets
path: exports/From there you can hand them to fastlane deliver or the App Store Connect API. See Upload to App Store Connect.
Reviewing screenshot changes in a PR
- Template changes are HTML diffs, which read like any code change.
- Committed PNGs show as GitHub image diffs, with side-by-side, swipe and onion-skin views.
- For a quick visual check without committing, open the artifact from the run summary.
Troubleshooting on runners
If Chrome fails to launch on ubuntu-latest with a sandbox error, see Chromium fails to launch. Renders that time out on slow runners can be given more time with --render-timeout 60000 (or OPEN_ASSETS_RENDER_TIMEOUT). Run OPEN_ASSETS_CONCURRENCY=1 if a runner is short on memory.
Last updated on
Other agents
Use Open Assets with Cursor, Codex or any coding agent that runs shell commands, by pointing it at llms.txt, per-page Markdown, or an AGENTS.md snippet.
Other CI and Docker
Run Open Assets in GitLab CI, CircleCI or a Docker image: install Chromium dependencies for Puppeteer, render headlessly, and use validate as a required check.