Every time a developer pushes code to PlayableAdStudio, something remarkable happens: a new playable ad is compiled, screenshots are captured across multiple device form factors, a preview GIF is rendered, everything is uploaded to a CDN, and the marketing site updates — all without a single human opening a screenshot tool.

This is the hybrid dev+marketing pipeline. It turns every code commit into fresh, accurate marketing collateral automatically. Here is exactly how it works and how you can build the same.

The Problem

Marketing assets for playable ads have a shelf life measured in days. Every UI tweak, every new level, every balance change invalidates the screenshots, demo videos, and app store previews that the marketing team depends on.

Before this pipeline, the workflow looked like this:

- Developer makes a change and pushes to staging

- Marketing team is notified (eventually)

- Someone opens the ad in a simulator, manually frames screenshots

- Screenshots get exported, cropped, resized for App Store, Google Play, ad networks

- A demo GIF is recorded with a screen capture tool

- Assets are emailed or uploaded to a shared drive

- The marketing site is updated (if anyone remembers)

Days elapsed. Screenshots fell behind the actual build. The "latest" assets on the marketing site were often a week out of date.

| Metric | Before Pipeline |

|---|---|

| Time from code push to updated marketing assets | 3-5 days |

| Screenshots matching current build | ~40% |

| Manual steps per asset update | 12+ |

| Marketing site accuracy | Low, often stale |

The Solution

The solution is a CI/CD pipeline that treats marketing assets as first-class build artifacts. When code changes, the pipeline:

1. Builds the playable ad in headless mode using Cocos Creator

2. Launches the ad in a headless Chromium instance via Puppeteer

3. Captures screenshots at every key interaction point

4. Renders a looping preview GIF from the screenshot sequence

5. Uploads everything to the CDN with cache-busting hashes

6. Updates the EmDash CMS with new asset URLs

7. Optionally triggers a marketing site rebuild

The entire cycle takes under 4 minutes for a typical playable ad project. Every push to `main` or `staging` produces a complete, fresh set of marketing assets.

Architecture Overview

```

Git Push

GitHub Actions (workflow dispatch / push trigger)

[cocos-build] → Headless Cocos Creator build → WebGL/HTML5 build artifact

[screenshot] → Puppeteer launches headless Chromium, loads build

[capture] → Script walks through ad flow, captures PNG screenshots

[gif-render] → FFmpeg / Sharp compositing → animated preview GIF

[cdn-deploy] → Upload to S3/CDN with content-hashed filenames

[cms-update] → EmDash CMS API call → updates asset references

Marketing Site → Fresh assets live within minutes

```

The key insight is that a playable ad is itself a self-contained WebGL or HTML5 application. By running it headlessly, we can extract any visual state programmatically — no simulator, no manual clicking, no screen recording.

Implementation

GitHub Actions Workflow

The pipeline is defined in a single GitHub Actions workflow file. Here is the core structure:

```yaml

name: Build + Marketing Assets

on:

push:

branches: [main, staging]

workflow_dispatch:

jobs:

build-and-market:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- name: Setup Cocos Creator

run: |

wget -q https://download.cocos.com/CocosCreator/v2.4.10/CocosCreator_v2.4.10_linux.zip

unzip -q CocosCreator_v2.4.10_linux.zip -d /opt/cocos

export PATH="/opt/cocos/CocosCreator.app/Contents/MacOS:$PATH"

- name: Headless Build

run: |

cocos build -p web-mobile --no-compress

echo "BUILD_HASH=$(sha256sum build/web-mobile/index.html | cut -c1-8)" >> $GITHUB_ENV

- name: Launch HTTP Server for Build

run: |

npx serve build/web-mobile -p 3000 &

sleep 3

curl -s -o /dev/null -w "%{http_code}" http://localhost:3000

- name: Capture Screenshots via Puppeteer

run: |

node scripts/capture-screenshots.js \

--url http://localhost:3000 \

--output ./assets/screenshots \

--viewport 375x812,414x896,1280x720

- name: Generate Preview GIF

run: |

node scripts/render-gif.js \

--input ./assets/screenshots/sequence \

--output ./assets/preview.gif \

--fps 2 \

--width 375

- name: Upload to CDN

env:

AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}

AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

run: |

aws s3 sync ./assets \

s3://cdn.playableadstudio.com/builds/${{ env.BUILD_HASH }}/ \

--cache-control "public, max-age=31536000, immutable"

- name: Update EmDash CMS

run: |

curl -X POST https://cms.playableadstudio.com/api/assets \

-H "Authorization: Bearer ${{ secrets.CMS_API_TOKEN }}" \

-H "Content-Type: application/json" \

-d '{

"buildHash": "${{ env.BUILD_HASH }}",

"branch": "${{ github.ref_name }}",

"screenshots": {

"iphone": "https://cdn.playableadstudio.com/builds/${{ env.BUILD_HASH }}/screenshots/iphone-375x812.png",

"android": "https://cdn.playableadstudio.com/builds/${{ env.BUILD_HASH }}/screenshots/android-414x896.png",

"tablet": "https://cdn.playableadstudio.com/builds/${{ env.BUILD_HASH }}/screenshots/tablet-1280x720.png"

},

"previewGif": "https://cdn.playableadstudio.com/builds/${{ env.BUILD_HASH }}/preview.gif",

"status": "published"

}'

```

Puppeteer Screenshot Script

```javascript

// scripts/capture-screenshots.js

const puppeteer = require('puppeteer');

const { program } = require('commander');

program

.requiredOption('--url <url>', 'URL of the playable ad')

.requiredOption('--output <path>', 'output directory')

.option('--viewport <sizes>', 'comma-separated viewport sizes', '375x812');

program.parse();

const opts = program.opts();

async function capture() {

const browser = await puppeteer.launch({

headless: 'new',

args: ['--no-sandbox', '--disable-setuid-sandbox']

});

const viewports = opts.viewport.split(',').map(s => {

const [w, h] = s.split('x').map(Number);

return { width: w, height: h };

});

for (const vp of viewports) {

const page = await browser.newPage();

await page.setViewport(vp);

await page.goto(opts.url, { waitUntil: 'networkidle0' });

// Walk through the ad flow at each step

const steps = ['intro', 'gameplay', 'endcard', 'cta'];

for (const step of steps) {

// Trigger ad state transitions via exposed game API

await page.evaluate((s) => {

if (window.__adAPI && window.__adAPI.gotoState) {

window.__adAPI.gotoState(s);

}

}, step);

await page.waitForTimeout(500);

await page.screenshot({

path: `${opts.output}/${vp.width}x${vp.height}-${step}.png`,

fullPage: false

});

}

await page.close();

}

await browser.close();

}

capture().catch(console.error);

```

CDN Deployment Strategy

Assets are uploaded with immutable cache headers and content-hashed filenames. The CDN URL pattern is:

```

https://cdn.playableadstudio.com/builds/{buildHash}/{type}/{filename}

```

This guarantees:

- **Cache invalidation is free**: new hash = new URL, no cache purging needed

- **Rollback is instant**: point the CMS at a previous build hash

- **Branch isolation**: staging builds go to `builds/staging/{hash}/`, production to `builds/main/{hash}/`

EmDash CMS Integration

The final step calls the EmDash CMS API to update the asset references for the playable ad entry. The CMS stores:

| Field | Source |

|---|---|

| `buildHash` | From build step |

| `screenshotUrls` | Generated by Puppeteer |

| `previewGifUrl` | Generated by FFmpeg |

| `lastUpdated` | Timestamp of the workflow run |

| `version` | Incremented on each publish |

| `status` | `draft` or `published` |

When the marketing site renders a playable ad page, it fetches the latest published entry and serves CDN-backed assets. The site itself is a static build that re-deploys on CMS webhook, but that's optional — the CDN URLs work anywhere.

Results

After implementing this pipeline:

| Metric | Before | After |

|---|---|---|

| Time from push to updated marketing assets | 3-5 days | ~4 minutes |

| Screenshots matching current build | ~40% | 100% |

| Manual steps per asset update | 12+ | 0 |

| Marketing site accuracy | Low, often stale | Always current |

| Human effort per asset refresh | ~2 hours | Zero |

| Asset formats per build | 3 (manual) | 6 (3 viewports x 2 formats) |

**Marketing velocity** improved dramatically. Because assets were always fresh, the team started creating more content — A/B test variants, social media previews, ad network experiments — all using auto-generated screenshots. The pipeline didn't just save time; it enabled new workflows.

**Regression detection** emerged as a side benefit. When code broke the ad's visual flow, the screenshot step would fail or produce garbled output, alerting developers immediately. The pipeline became a visual smoke test for free.

Key Takeaways

1. **Marketing assets are build artifacts** — treat them with the same rigor as compiled binaries.

2. **Headless browsers eliminate manual screenshots** — Puppeteer can capture any WebGL/HTML5 app's state.

3. **The pipeline is the source of truth** — generated from every commit, assets never drift.

4. **CMS-as-code** — URL updates via API keep the marketing site synchronized automatically.

5. **Side effects are features** — regression detection and automated QA emerge naturally.

6. **Start simple, extend later** — a single-viewport version in an afternoon, then iterate.

PlayableAdStudio's hybrid dev+marketing pipeline turns a push hook into a full marketing production line. Every commit is fresh creative collateral ready for distribution — in an industry where ad creative has a shelf life of hours, that speed is the difference between a campaign that converts and one that looks outdated on arrival.