Marketing video in CI (opt-in)
The video suite (Add videos to your project) can build a launch video, social cuts and a preview of video changes in GitHub Actions, from committed sources, with no secrets. It is off by default: a repository that does not opt in gets no jobs, no cost and no new permissions.
← Home · Add videos to your project · The plan behind it
Switch it on
Three switches, all off until you flip them:
- Repository variable. Settings → Secrets and variables → Actions → Variables →
VIDEO_CI=true. Without it every job in the Video workflow is skipped. This is the kill switch: set it back tofalseto stop everything. video/ci.json. Set"enabled": true, then turn on the triggers you want (triggers.release,triggers.pullRequest,triggers.schedule). A manual run does not needenabled.- The
marketingenvironment (Settings → Environments). Add required reviewers. Publishing (attaching to a release, opening a site pull request) waits for approval there, and only runs whenpublish.releaseorpublish.siteistrue.
{
"enabled": true,
"triggers": { "release": true, "pullRequest": false, "schedule": false },
"projects": [
{ "project": "launch", "looks": [{}, { "theme": "midnight", "format": "9:16" }, { "format": "4:5" }] }
],
"publish": { "release": true, "site": false },
"budgets": { "mp4MB": 10, "seconds": 90, "buildMinutes": 20 }
}What runs, and when
| Trigger | When | Builds | Publishes |
|---|---|---|---|
| Manual (Actions → Video → Run workflow) | You pick project, theme, format, adapter | Exactly that, one look | Nothing: download the artifact |
Release (triggers.release) | After the Release workflow succeeds for a tag | Every look in projects (the launch video, a vertical cut, a 4:5 cut) | The release (publish.release) and a site pull request (publish.site), each behind the marketing environment |
Pull request (triggers.pullRequest) | A same-repository PR with the video-preview label, or one that changes a previewPaths file | The preview set, fast (a quicker encode) | Nothing: artifact and a job summary |
Schedule (triggers.schedule) | The first of each month | Every look in projects | Nothing |
Pull requests from forks never build. Nothing here needs a secret: the build uses the workflow's read-only token, and only the two publish jobs get contents: write (and pull-requests: write for the site pull request).
The jobs
plan ──► claims ──► build (one job per look) ──► publish-release / publish-site (reviewed)- plan decides whether to build and what, in seconds.
- claims is the honesty gate: it fails when a video says something the product does not support (for kinhin, any of the planned platforms such as Buildkite or Windows without saying they are planned, and tone words like "fastest"). The rules live in the adapter (
video/adapters/<name>/claims.json) and the project file. - build runs
video/scripts/build.sh --container --runtime dockeron a hosted Linux runner (the same command you run locally), then verifies the result and writes a summary table. A release build pulls the image published for that release (ghcr.io/rabinapps/kinhin-video:<version>, made byvideo-image.yml) instead of building it once per look; a pull request, a manual run, or a release whose image is not published yet builds it from the checked-out sources. - verify is the same checker the end-to-end tests use (
video/scripts/verify.mjs): the frame size, the length against the timeline, an audio track near -16 LUFS, no black frames, one caption per cue, a poster and a hero GIF that are not blank, and the size and length budgets. A video that fails is not published.
A failed video job never blocks a release, a docs deploy or a pull request: nothing depends on it.
Store screenshots in the mobile cut
examples/mobile-30s is a launch cut that shows store screenshots. Its look sets "screens": true in video/ci.json, so the plan adds one job before the build:
mobile-captureruns on the macOS pool (kinhin-xcode), because the iOS simulator needs macOS. It generates the iOS example, runsbundle exec fastlane screenshotsforen-USandfr-FR, and uploads the PNGs as themobile-screensartifact. Only looks that setscreensrun it; other builds are unchanged.build(Linux, in the container) downloadsmobile-screensintovideo/screens/mobile/for that look only, then renders. The cut'sdevicescenes point at those files withprops.screens(see making a video).
There is one capture job per platform a look needs (ios and android), and each look picks its platform in video/ci.json. The Android capture boots the pool's emulator (KINHIN_AVD, default Pixel_9) and needs its system image on the runner; the iOS capture is verified locally, the Android one is not yet verified on the pool.
Your own product's videos on your fleet
kinhin video build <name> --on-fleet runs a video of your repository on a runner kinhin serves, with no video/ folder: kinhin video init --workflow writes .github/workflows/kinhin-video.yml (pinned actions, read-only token, no secrets), and --on-fleet dispatches it with gh, follows the run and downloads the result. See Building on your fleet. The opt-in video.yml described on this page is a different thing: it builds kinhin's own videos from this repository.
Try it without GitHub
The decisions are a script you can run here:
cd video
node scripts/video-ci.mjs validate # is ci.json valid?
node scripts/video-claims-lint.mjs --all # does every video say only true things?
VIDEO_CI=true GITHUB_EVENT_NAME=workflow_dispatch INPUT_PROJECT=launch \
node scripts/video-ci.mjs plan # what would a manual run build?
scripts/build.sh --container --project examples/hello # the build itself
node scripts/video-ci.mjs verify out # the checks, on a finished buildscripts/video-check.sh runs the first two plus the unit tests, and scripts/check.sh and the CI docs job run it on every change. See the tests (kinhin's own guide).
Use it for another product
Do not copy video.yml: it builds kinhin's own videos from this repository. For your product, keep your videos in .kinhin/videos/ and build them with kinhin video build on your Mac, or on your fleet with kinhin video init --workflow and --on-fleet (see Add videos to your project). That generated workflow is manual (workflow_dispatch) and pulls the published video image, so you need no video/ folder; to build on every release, add a release: trigger to it yourself.
When something fails
| Symptom | Likely cause |
|---|---|
| The workflow shows every job skipped | VIDEO_CI is not true, or ci.json has "enabled": false, or the trigger's switch is off. The plan job's log says which. |
claims fails | A scene mentions something the adapter's claims.json lists as planned, or a tone word. The message names the scene and the text. |
build fails in verify | The log lists each failed check (size, length, loudness, black frames, captions). Budgets are in ci.json. |
| A pull request got no preview | It is from a fork, or has neither the video-preview label nor a changed previewPaths file. |
publish-* waits | It is waiting for approval in the marketing environment. |
