TITLE=WarpEngine ROUTE=/engines/warp-engine LOCALE=en MODIFIED=1792417593 URL=https://wiki.teletypegames.org/engines/warp-engine L=1 #54 L=2 #53 L=3 #46 L=4 #24 L=5 #45 L=6 #47 L=7 #43 L=8 #44 L=9 #68 L=10 #70 L=11 https://woodpecker-ci.org/docs/usage/extensions/configuration-extension q│ WarpEngine is a mountable Rails engine that turns any Rails app into a retro q│ software catalog. A game repo carries a one-line c.woodpecker.yamlq marker, q│ the engine serves the whole CI pipeline, builds are uploaded over HTTP, and q│ a publish call upserts the catalog. A public read-only JSON API and optional q│ ActiveAdmin resources are included. 1WarpEngine related pages 1════════════════════════ t┌────────────────────────────────┬────────────────────────────────┬──────────┐ t│h Page t│h What it is t│h Side t│ t├────────────────────────────────┼────────────────────────────────┼──────────┤ t│0 1WECP — WarpEngine Catalog t│0 The read-side HTTP/JSON t│0 protocol t│ t│0 1Protocol t│0 contract, endpoint by t│0 t│ t│0 t│0 endpoint: what every catalog t│0 t│ t│0 t│0 reader talks to. t│0 t│ t├────────────────────────────────┼────────────────────────────────┼──────────┤ t│0 2WarpEngine Admin API t│0 Since 1.0: what the admin t│0 protocol t│ t│0 t│0 does, as a JSON API with an t│0 t│ t│0 t│0 OpenAPI 3.1 document and an t│0 t│ t│0 t│0 MCP server. Off unless the t│0 t│ t│0 t│0 host turns it on. t│0 t│ t├────────────────────────────────┼────────────────────────────────┼──────────┤ t│0 3WarpEngine Client t│0 The desktop app: the catalog t│0 client t│ t│0 t│0 as a grid of cards, install, t│0 t│ t│0 t│0 play, remove. Linux, macOS, t│0 t│ t│0 t│0 Windows. t│0 t│ t├────────────────────────────────┼────────────────────────────────┼──────────┤ t│0 4warpstore t│0 The shared core of the t│0 client t│ t│0 t│0 shell-script store engines: t│0 t│ t│0 t│0 one file that knows the t│0 t│ t│0 t│0 WarpEngine catalog. t│0 t│ t├────────────────────────────────┼────────────────────────────────┼──────────┤ t│0 5WarpEngine Batocera Store t│0 Any WarpEngine catalog on a t│0 client t│ t│0 t│0 Batocera box, under its own t│0 t│ t│0 t│0 EmulationStation menu. t│0 t│ t├────────────────────────────────┼────────────────────────────────┼──────────┤ t│0 6WarpEngine RetroArch Store t│0 Any WarpEngine catalog as t│0 client t│ t│0 t│0 RetroArch playlists with box t│0 t│ t│0 t│0 art. t│0 t│ t├────────────────────────────────┼────────────────────────────────┼──────────┤ t│0 7TTG Batocera Store · 8TTG t│0 Our own two stores: a config t│0 store t│ t│0 8RetroArch Store t│0 on top of each engine above. t│0 t│ t├────────────────────────────────┼────────────────────────────────┼──────────┤ t│0 9Public site t│0 teletypegames.org, the first t│0 host t│ t│0 t│0 host: the monorepo the engine t│0 t│ t│0 t│0 is developed in, and the t│0 t│ t│0 t│0 catalog most readers point at. t│0 t│ t├────────────────────────────────┼────────────────────────────────┼──────────┤ t│0 10Teletype Orbit t│0 The shop platform on t│0 host t│ t│0 t│0 WarpEngine: purchases, t│0 t│ t│0 t│0 entitlements, gated downloads, t│0 t│ t│0 t│0 device sign-in. t│0 t│ t└────────────────────────────────┴────────────────────────────────┴──────────┘ 1What you get 1════════════ • bCatalog domain0 — cSoftware0, cRelease0, cReleaseAsset0, cExternalLink0, cPlatformLink0, cSoftwareImage0, cDownload0. All soft-deleted, with download tracking. • bCI pipeline server0 — a Woodpecker 11configuration extension. Game repos hold only a platform marker. Builder images are server-side, so bumping an image reaches every repo at once. • bPluggable CI0 — from 0.7 every CI feature goes through one adapter. Woodpecker ships with the engine as cWarpEngine::CI::Woodpecker0 and is the default; c:none0 turns CI off, and another server is a host-supplied object. • bBuild endpoints0 — cPOST /build/upload0 and cPOST /build/publish0, authenticated by a shared secret or by database application tokens. • bPublic JSON API0 — catalog, highlighted title, per-platform build matrix, download tracking, static file server for web builds. • bHost-owned image library0 — from 0.6 the cImage0 model, its files, its c/api/image/:id0 endpoint and its admin page live in the host; the engine links a software to an image and publishes the URL through an adapter. The same table can then serve avatars or anything else the site has. • bPluggable access0 — the host supplies a policy and the catalog gains prices, entitlements and gated downloads, and says so in the API, so a client can show a paid title as paid instead of failing at download time. Default: the open catalog. • bClient sign-in0 — RFC 8628 device authorization grant over the host's user model, for clients with no browser. Off unless configured. • bOptional admin0 — ActiveAdmin resources (catalog editor, file manager, download stats, app tokens). Without ActiveAdmin the engine runs headless. • bCI management0 — with an API token the admin drives the CI server: repo sync, pipeline history with manual triggers, application tokens provisioned as CI secrets. • bAsset checklist0 — from 0.8, crake warp_engine:builds:check0 prints per software and release which assets exist and which the platform's pipeline could still deliver. See iWhich assets are missing0. 2Supported platforms 2─────────────────── Two lists per platform, and the difference matters. bAssets accepted0 is what the updater ingests when a release is published (cWarpEngine::Platform#expected_kinds0). bThe pipeline builds0 is what the Woodpecker templates actually produce. Where they differ, the rest is a manual upload. t┌────────────┬────────────┬────────────────────────┬─────────────────────────┐ t│h Platform t│h Label t│h Assets accepted t│h The pipeline builds t│ t├────────────┼────────────┼────────────────────────┼─────────────────────────┤ t│0 cgodot0 t│0 Godot t│0 html, win_x86, t│0 all of them t│ t│0 t│0 t│0 win_x64, linux_x64, t│0 t│ t│0 t│0 t│0 mac_universal t│0 t│ t├────────────┼────────────┼────────────────────────┼─────────────────────────┤ t│0 ctic800 t│0 TIC-80 t│0 cartridge, source, t│0 all of them t│ t│0 t│0 t│0 html, docs, win_x64, t│0 t│ t│0 t│0 t│0 linux_x64, mac_x64 t│0 t│ t├────────────┼────────────┼────────────────────────┼─────────────────────────┤ t│0 cbevy0 t│0 Bevy t│0 html, win_x64, t│0 all of them t│ t│0 t│0 t│0 linux_x64, linux_arm64 t│0 t│ t├────────────┼────────────┼────────────────────────┼─────────────────────────┤ t│0 clove0 t│0 LÖVE t│0 html, win_x64, t│0 all of them t│ t│0 t│0 t│0 linux_x64, t│0 t│ t│0 t│0 t│0 mac_universal t│0 t│ t├────────────┼────────────┼────────────────────────┼─────────────────────────┤ t│0 cebitengine0 t│0 Ebitengine t│0 html, win_x86, t│0 html, win_x86, win_x64, t│ t│0 t│0 t│0 win_x64, linux_x64, t│0 linux_x64, linux_arm64 t│ t│0 t│0 t│0 linux_arm64, mac_x64, t│0 t│ t│0 t│0 t│0 mac_arm64 t│0 t│ t├────────────┼────────────┼────────────────────────┼─────────────────────────┤ t│0 cphaser0 t│0 Phaser t│0 html t│0 all of them t│ t├────────────┼────────────┼────────────────────────┼─────────────────────────┤ t│0 cc640 t│0 C64 t│0 cartridge, html, t│0 all of them t│ t│0 t│0 t│0 win_x64, linux_x64, t│0 t│ t│0 t│0 t│0 mac_universal t│0 t│ t├────────────┼────────────┼────────────────────────┼─────────────────────────┤ t│0 cplus40 t│0 Plus/4 t│0 cartridge, html, t│0 all of them t│ t│0 t│0 t│0 win_x64, linux_x64, t│0 t│ t│0 t│0 t│0 mac_universal t│0 t│ t└────────────┴────────────┴────────────────────────┴─────────────────────────┘ Ebitengine is the one gap: a macOS binary needs osxcross, which the linux builder image does not carry, so cmac_x640 and cmac_arm640 only ever arrive by hand. 2Which assets are missing 2──────────────────────── A release is complete when it carries every asset kind its platform's pipeline can produce — and nothing enforces that. A build step fails, a platform gains a target, a title gets published from a laptop, and the catalog serves a release with three of its seven assets. The engine ships a checklist for it: p $ bin/rails warp_engine:builds:check p p WarpEngine release asset coverage — latest release per software p expected: the kinds a platform's updater registers · [ ] the CI builds it · » p [-] the CI does not, upload it by hand p p rabbitroller ebitengine v1.1.1 3/7 p [x] html p [x] win_x64 p [x] linux_x64 p [ ] win_x86 p [ ] linux_arm64 p [-] mac_x64 p [-] mac_arm64 p p The CI can build these and the release does not have them: p rabbitroller v1.1.1 (ebitengine): win_x86, linux_arm64 p p 12 softwares, 12 releases, 61/78 assets present, 11 missing from CI-built ki» p nds p 4 softwares are missing an asset the CI builds p 2 assets are expected but this CI does not build them t┌────────┬───────────────────────────────────────────────────────────────────┐ t│h Marker t│h Meaning t│ t├────────┼───────────────────────────────────────────────────────────────────┤ t│0 c[x]0 t│0 the release has this asset t│ t├────────┼───────────────────────────────────────────────────────────────────┤ t│0 c[ ]0 t│0 the platform expects it band0 the pipeline builds it — a real gap, t│ t│0 t│0 and the line worth acting on t│ t├────────┼───────────────────────────────────────────────────────────────────┤ t│0 c[-]0 t│0 expected, but this CI cannot build it (see the table above) — a t│ t│0 t│0 manual upload, or nothing t│ t├────────┼───────────────────────────────────────────────────────────────────┤ t│0 c[+]0 t│0 the release carries an asset the platform does not expect t│ t└────────┴───────────────────────────────────────────────────────────────────┘ Options are environment variables, so it composes in a shell: t┌─────────────────────┬──────────────────────────────────────────────────────┐ t│h Variable t│h Effect t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cNAME=rabbitroller0 t│0 one software t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cPLATFORM=ebitengine0 t│0 one platform t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cRELEASES=all0 t│0 every release, not just the newest per software t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cONLY=missing0 t│0 only the releases short of a CI-built asset t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cSTRICT=10 t│0 exit non-zero when anything is missing, for a t│ t│0 t│0 scheduled check t│ t└─────────────────────┴──────────────────────────────────────────────────────┘ "Newest" is the newest non-cdev-0 version — the same rule the catalog API uses when it names a title's latest release. The numbers are objects too: cWarpEngine::AssetCoverage0 answers c#rows0, c#missing_rows0 and c#totals0, and cWarpEngine::AssetCoverageChecklist0 renders them. Available in both hosts: the 9public site and 10Teletype Orbit. 1Endpoints 1═════════ 2Public API 2────────── This is the read side of the API — the bWECP, WarpEngine Catalog Protocol0. Every endpoint's request and response shape is on the 1WECP page; this table is the map. t┌─────────────────────────────────┬──────────────────────────────────────────┐ t│h Endpoint t│h Purpose t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cGET /api/service0 t│0 What this deployment is: version, t│ t│0 t│0 whether the catalog gates, how to sign t│ t│0 t│0 in. t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cGET /api/software0 t│0 The catalog with releases, assets, t│ t│0 t│0 links, download counts, an optional t│ t│0 t│0 ccategory0 and an caccess0 block — whole, or t│ t│0 t│0 one page at a time (c?page=0, c?per_page=0), t│ t│0 t│0 filtered by cstatus0, cplatform0, ccategory0, t│ t│0 t│0 cq0, cowner_id0. t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cGET /api/software/facets0 t│0 How many softwares each status, platform t│ t│0 t│0 and category would show under the other t│ t│0 t│0 filters. t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cGET /api/software/vocabulary0 t│0 The statuses and categories this catalog t│ t│0 t│0 uses, with labels and display order. t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cGET /api/software/highlighted0 t│0 The highlighted title. t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cGET /api/builds0 t│0 Expected asset kinds per platform. t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cGET /api/softwares/:name/builds0 t│0 Actual vs. missing build assets per t│ t│0 t│0 release. t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cGET /api/image/:id0 t│0 Images. bThe host's endpoint0 since 0.6 — t│ t│0 t│0 the engine publishes the address, the t│ t│0 t│0 host serves it. t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cGET /api/download?path=0 t│0 Serves an artifact and logs a download. t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cGET /file/*path0 t│0 Static build output (web-playable games, t│ t│0 t│0 docs). t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cPOST /api/auth/device0 t│0 Starts a device sign-in, returns the t│ t│0 t│0 code pair. 404 when no client identity t│ t│0 t│0 is configured. t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cPOST /api/auth/device/token0 t│0 Polls a device sign-in for its token. t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 cDELETE /api/auth/token0 t│0 Revokes the bearer token on the request t│ t│0 t│0 — sign out. t│ t└─────────────────────────────────┴──────────────────────────────────────────┘ Every read endpoint accepts an optional cAuthorization: Bearer …0; none requires one. The token only changes what the access policy is asked. An anonymous caller is a supported caller. Every response carries a cWarpEngine-Version0 header, set before the action runs — so error responses have it too, and a client can branch on the engine version without an extra request. p $ curl -sI https://teletypegames.org/api/software | grep -i warpengine p WarpEngine-Version: 0.13.1 The host app's own endpoints (the store registry, for example) do not carry it — they are not part of the engine. 2Build / CI 2────────── t┌─────────────────────────────┬──────────────────────────────────────────────┐ t│h Endpoint t│h Purpose t│ t├─────────────────────────────┼──────────────────────────────────────────────┤ t│0 cPOST /build/upload0 t│0 One artifact (multipart cfile0, optional t│ t│0 t│0 csha2560). Needs cX-Update-Secret0. t│ t├─────────────────────────────┼──────────────────────────────────────────────┤ t│0 cPOST /build/publish0 t│0 Register uploaded artifacts as a release. t│ t│0 t│0 Needs cX-Update-Secret0. t│ t├─────────────────────────────┼──────────────────────────────────────────────┤ t│0 cPOST /build/config0 t│0 Woodpecker configuration extension; serves t│ t│0 t│0 the pipeline YAML. Signature-verified (RFC t│ t│0 t│0 9421, ed25519). t│ t├─────────────────────────────┼──────────────────────────────────────────────┤ t│0 cGET /build/config?platform=0 t│0 Public preview of the generated YAML. t│ t└─────────────────────────────┴──────────────────────────────────────────────┘ 1Access and identity 1═══════════════════ Two seams added in b0.5.00, both shaped like the storage adapter: a documented contract, a default identical to the old behaviour, and one config key to replace it. Set neither and nothing changes. 2Who may see and download what 2───────────────────────────── p c.access_policy = MyStore::AccessPolicy.new # :open (default) = the catalo» p g as it was The policy answers three questions: t┌────────────────────────┬────────────────────────────────────────┐ t│h Method t│h Question t│ t├────────────────────────┼────────────────────────────────────────┤ t│0 cvisible_software_scope0 t│0 which titles cGET /api/software0 lists t│ t│0 caccess_for0 t│0 what a client is told about each title t│ t│0 cauthorize_download0 t│0 may this artifact be handed over t│ t└────────────────────────┴────────────────────────────────────────┘ cauthorize_download0 is asked by c/api/download0 band0 c/file/*0 before anything is served, so a host no longer has to shadow those routes. Every catalog entry carries an caccess0 block, balso under the open policy0 — a client never has to guess whether the catalog is silent or the title is simply not gated: p "access": { "gated": true, "entitled": false, p "price": { "amountCents": 1490, "currency": "EUR" }, p "purchaseUrl": "https://shop.example/games/slug", p "webUrl": "https://shop.example/play/slug" } The field names are deliberately generic, because one client reads several catalogs. bA policy that raises counts as a refusal0: empty catalog, denied download, logged. An artifact must never be served because the gatekeeper crashed. 2Signing a client in 2─────────────────── A desktop client has no cookie jar and no browser session, so the engine uses the device authorization grant (RFC 8628) instead of a login form. p c.access_token_owner_class = "User" # nil (default): no sign-in anywhe» p re p c.identity_verification_url = "/devices" # your page, where a person types » p the code p c.subject_resolver = ->(request) { request.env["warden"]&.user } # recogni» p se a browser 1. the client cPOST0s c/api/auth/device0 and shows the cuserCode0; 2. the person opens the verification page and types that code; 3. bthe host's page0 calls cWarpEngine::DeviceGrantService#approve(user_code:, csubject:)0 — approving needs a session and HTML, which are not the engine's business; 4. the client's next poll takes the token. It is handed over exactly once. The token is a cWarpEngine::ApplicationToken0 with the ccatalog0 scope, so publishing and client credentials stay separable in one table. csubject_resolver0 covers the other case: a browser has a session, not a bearer token, and without it a gated store would refuse its own signed-in visitors. With caccess_token_owner_class0 unset, c/api/auth/*0 answers 404 and c/api/service reports cauth: null0, so the client offers no sign-in. 1The image library 1═════════════════ An image is not a catalog concept — the same picture library serves covers, screenshots and avatars — so from b0.6.00 the engine owns only the link (cSoftwareImage0: ordered, one default), while the model, the files, the endpoint and the admin page belong to the host. crails g warp_engine:install writes a working set (capp/models/image.rb0, capp/controllers/api/images_controller.rb0, a ccreate_images0 migration), and the engine reaches it through an adapter: p c.image_class_name = "Image" # default p c.image_adapter = MyLibrary.new # or replace the adapter entirely t┌─────────────────────────────────────┬──────────────────────────────────────┐ t│h Method t│h Used for t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cmodel_name0 / cmodel0 t│0 the class cSoftwareImage#image0 t│ t│0 t│0 belongs to t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 curl_for(image_id)0 t│0 cimageUrl0 and cimages[].url0 in the t│ t│0 t│0 catalog JSON, and the admin previews t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cselect_options0 t│0 the image picker on the software t│ t│0 t│0 form t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cbuild_from_upload(upload)0 t│0 "upload a new image" on that form t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 clabel_for(record)0, t│0 the admin's label and "is the file t│ t│0 cavailable?(record)0 t│0 there" check t│ t└─────────────────────────────────────┴──────────────────────────────────────┘ The default adapter publishes c/api/image/0 — the address clients have always used — which is why the generated route serves exactly that. bThe cimages btable did not move0: the engine simply stopped creating it, and csoftware_images.image_id0 still points at the same rows. 1Using Woodpecker with WarpEngine 1════════════════════════════════ A game repo has no pipeline of its own. On every pipeline start Woodpecker POSTs the one-line marker to c/build/config0 and gets the full YAML back. 1. Woodpecker is configured with WarpEngine as a configuration extension. 2. The repo's c.woodpecker.yaml0 holds only cplatform:0 (and cname:0 when the software name differs from the repo name). 3. A push sends the marker to cPOST /build/config0. 4. The CI adapter renders clib/warp_engine/ci/woodpecker/platforms//pipeline.yaml.erb0 with the configured builder image — the YAML dialect belongs to the provider, so it lives with the adapter. From 1.1 the helper steps (version, lint, minify, docs, upload, publish) run in the platform's ctools0 image when it has one, and install nothing; without it they run in calpine0 and capk add0 (or cluarocks install0) what they need on every run. 5. The pipeline runs: version → build → upload → publish. A repo opts out by committing a real pipeline YAML: a non-marker config gets c204 No Content0 and runs unchanged. 2Setting up a game repo 2────────────────────── p # .woodpecker.yaml p platform: godot p name: mygame # optional, defaults to the repo name p // metadata.json — same for every platform except TIC-80 p { p "name": "mygame", p "version": "1.0.0", p "title": "My Godot Game", p "author": "You", p "desc": "A cool game", p "license": "MIT", p "category": "puzzle" p } ccategory0 is optional free text a client may group titles by — ipuzzle0, iaction0, idemo0. Since 0.12 it lands on the cSoftware0 record and in cGET /api/software0; the WarpEngine Client draws a category axis in its filter as soon as a catalog publishes one. csite0 and crepo0 are optional too and become external links. t┌────────────┬───────────────────────────────┬───────────────────────────────┐ t│h Platform t│h Source expectations t│h What the pipeline builds t│ t├────────────┼───────────────────────────────┼───────────────────────────────┤ t│0 cgodot0 t│0 export presets: Web, Windows t│0 cgodot --headless0 t│ t│0 t│0 x86, Windows x64, Linux x64, t│0 c--export-release0 per preset t│ t│0 t│0 Mac universal t│0 t│ t├────────────┼───────────────────────────────┼───────────────────────────────┤ t│0 cbevy0 t│0 standard Cargo project; t│0 WASM, linux-x64, win-x64 t│ t│0 t│0 cassets/0 is packaged next to t│0 (mingw) t│ t│0 t│0 native binaries t│0 t│ t├────────────┼───────────────────────────────┼───────────────────────────────┤ t│0 clove0 t│0 standard LÖVE project t│0 love.js web, Windows exe, t│ t│0 t│0 t│0 Linux AppImage, universal t│ t│0 t│0 t│0 macOS c.app0 t│ t├────────────┼───────────────────────────────┼───────────────────────────────┤ t│0 cebitengine0 t│0 standard Go project t│0 WASM, win x86/x64, linux x64, t│ t│0 t│0 t│0 mac x64/arm64 t│ t├────────────┼───────────────────────────────┼───────────────────────────────┤ t│0 cphaser0 t│0 JS files in csrc/0 t│0 syntax check, Phaser runtime t│ t│0 t│0 t│0 download, web zip t│ t├────────────┼───────────────────────────────┼───────────────────────────────┤ t│0 cc640 t│0 cmain.asm0 t│0 ACME assembly into a c.prg0 t│ t│0 t│0 t│0 cartridge, then t│ t│0 t│0 t│0 ccommodore-pack0 in the t│ t│0 t│0 t│0 cexporter0 image: an EmulatorJS t│ t│0 t│0 t│0 web build with the cvice_x64sc0 t│ t│0 t│0 t│0 core and yapeSDL bundles for t│ t│0 t│0 t│0 linux-x64, win-x64 and t│ t│0 t│0 t│0 mac-universal t│ t├────────────┼───────────────────────────────┼───────────────────────────────┤ t│0 cplus40 t│0 cmain.asm0 t│0 the same as cc640 with the t│ t│0 t│0 t│0 cvice_xplus40 core and yapeSDL t│ t│0 t│0 t│0 in Plus/4 mode t│ t├────────────┼───────────────────────────────┼───────────────────────────────┤ t│0 ctic800 t│0 c.inc0 listing fragments t│0 concat, luacheck, minify, t│ t│0 t│0 from cinc/0 t│0 LDoc, HTML + cartridge + t│ t│0 t│0 t│0 native binaries t│ t└────────────┴───────────────────────────────┴───────────────────────────────┘ TIC-80 needs no cmetadata.json0 — the metadata is the Lua header: p -- title: My TIC Game p -- name: mygame p -- author: You p -- desc: A TIC-80 game p -- category: puzzle p -- version: 1.0.0 p function TIC() end 2Marker options 2────────────── t┌──────────┬──────────┬──────────────────────────────────────────────────────┐ t│h Key t│h Required t│h Description t│ t├──────────┼──────────┼──────────────────────────────────────────────────────┤ t│0 cplatform0 t│0 yes t│0 cgodot0, ctic800, cbevy0, clove0, cebitengine0, cphaser0, cc640, t│ t│0 t│0 t│0 cplus40 t│ t├──────────┼──────────┼──────────────────────────────────────────────────────┤ t│0 cname0 t│0 no t│0 Software name; defaults to the repo name t│ t└──────────┴──────────┴──────────────────────────────────────────────────────┘ 2Preview a pipeline 2────────────────── p curl "https://your-host/build/config?platform=godot" 2Paging and vocabulary (0.13) 2──────────────────────────── cGET /api/software0 answers in an envelope — capi0 (the JSON shape's own version, 2), cpage0, cperPage0, ctotal0, ctotalPages0, cnextPage0, and csoftwares0. bWithout cpage bthe whole catalog comes back0 in that envelope, which is what every reader before API version 2 finds under csoftwares0; with cpage0 it is one page of cper_page0 entries (ccatalog_page_size0 when unsaid, at most ccatalog_max_page_size0). Filters narrow the catalog before it is paged; the access policy decides visibility before either. Statuses and categories are a bvocabulary in the database0 — cWarpEngine::SoftwareStatus0 and cWarpEngine::SoftwareCategory0, editable in the admin — not a list in the code. A software's cstatus0 must be a known key: the migration seeds the four, and since 0.13.1 the model puts them back into an empty table the first time anything asks — a database built with cdb:schema:load0 rather than the migration, which is every test database, has no rows otherwise. A category a publish brings that nobody has named yet is added to the vocabulary with its key as its label and cneeds_review0 set. cGET c/api/software/vocabulary0 publishes both, and the site's catalog page and the WarpEngine Client take their labels from it. 2Secrets and versions 2──────────────────── Every generated pipeline expects one Woodpecker secret, capplication_token0, sent as the cX-Update-Secret0 header to c/build/upload0 and c/build/publish0. Versioning is the same everywhere: on cmain0/cmaster0 the version comes from cmetadata.json0 (or c-- version:0 for TIC-80); on other branches it is prefixed, e.g. cdev-1.0.0-my-feature0. 1CI management (Woodpecker API) 1══════════════════════════════ Active only where the adapter has a curl0 and an capi_token (cWarpEngine.ci.configured?0). Until then the admin pages hide themselves. • bRepo sync0 — iAdmin → Pipelines → Sync from Woodpecker0 (the button carries the adapter's name). Mirrors the repo list into cPipeline0 records, matching each repo to a cSoftware0 by name and inheriting its platform. Repos gone from Woodpecker are deactivated. Links stay editable by hand. • bPipelines0 — each entry lists its recent runs (status, branch, message, time) with a manual bTrigger0. The newest run refreshes the cached status on the index. A software's admin page links to its pipelines. • bSecret provisioning0 — creating a token provisions the capplication_token secret to the owner's repos (unrestricted tokens: every active repo); deleting removes it; bRotate0 creates a replacement, provisions it everywhere and revokes the old one in one step. The PAT must belong to a Woodpecker binstance admin0 — listing repos is an admin-only endpoint, anything else returns c403 User not authorized0. Add the user to cWOODPECKER_ADMIN0, then blog out and back in0: the admin flag is written at login, a server restart is not enough. 1Build authentication 1════════════════════ cX-Update-Secret0 carries one of two credential types, chosen by capplication_token_source0. The modes are exclusive. t┌────────────────┬───────────────────────────────────────────────────────────┐ t│h Mode t│h Credential t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 c:env0 (default) t│0 the shared secret in cupdate_secret0; unset means every t│ t│0 t│0 request is rejected t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 c:database0 t│0 cWarpEngine::ApplicationToken0 records: an owner (e.g. t│ t│0 t│0 cAdminUser0), scopes (cupdate0 to publish, cupload0 to upload), t│ t│0 t│0 optional expiry. Created in the admin; the plain token is t│ t│0 t│0 shown once, only its SHA256 digest is stored. t│ t│0 t│0 clast_used_at0 tracks use. t│ t└────────────────┴───────────────────────────────────────────────────────────┘ q│ bCutover warning:q switching to c:databaseq invalidates the shared secret q│ immediately. Create the tokens first. 1Installing into a host 1══════════════════════ p # Gemfile — from git: p gem "warp_engine", git: "https://git.teletypegames.org/engines/warp_engine.g» p it" p p # ...or from the Forgejo rubygems registry (tagged releases): p source "https://git.teletypegames.org/api/packages/engines/rubygems" do p gem "warp_engine" p end p rails g warp_engine:install # initializer, image library, migrations p rails db:migrate p # config/routes.rb p namespace :api do p get "image/:id", to: "images#show" # the image library is yours (see bel» p ow) p end p p # keep the mount last so the host's own routes win p mount WarpEngine::Engine => "/" The generator writes cconfig/initializers/warp_engine.rb0: p Rails.application.config.to_prepare do p WarpEngine.configure do |c| p c.file_container_path = ENV.fetch("FILE_CONTAINER_PATH", "/softwares") p p # The image library is the host's; "Image" is the adapter's default. p # c.image_class_name = "Media::Picture" p # c.image_adapter = MyImageLibrary.new p p c.update_secret = ENV["UPDATE_SECRET"] p p # c.application_token_source = :database p # c.application_token_owner_class = "AdminUser" p p # One adapter carries the whole CI side: server, recipes, signing key. p c.ci_adapter = WarpEngine::CI::Woodpecker::Adapter.new( p url: ENV["WOODPECKER_URL"], p api_token: ENV["WOODPECKER_API_TOKEN"], p repo_owner: ENV["WOODPECKER_REPO_OWNER"], p public_key_url: "https://ci.example.org/api/signature/public-key", p update_server: "https://catalog.example.org", p platforms: { p "godot" => { builder: "registry.example/godot-builder:4.6" }, p "tic80" => { builder: "registry.example/tic80-builder:1.0", p exporter: "registry.example/tic80pro:1.0" }, p "bevy" => { builder: "registry.example/bevy-builder:1.0" }, p "love" => { builder: "registry.example/love-builder:1.0" }, p "ebitengine" => { builder: "registry.example/ebitengine-builder:1.0"» p }, p "phaser" => { builder: "registry.example/phaser-builder:1.0" }, p "c64" => { builder: "registry.example/c64-builder:1.0", p exporter: "registry.example/commodore-player:1.0" » p }, p "plus4" => { builder: "registry.example/plus4-builder:1.0", p exporter: "registry.example/commodore-player:1.0" » p } p } p # Any spec may also name a tools image for the helper steps (1.1): p # "love" => { builder: "...", tools: "registry.example/ci-tools:1.0"» p } p ) p p # c.max_upload_size = 500 * 1024 * 1024 p # c.enforce_software_ownership = true p end p end t┌────────────────────────┬─────────────────────────┬─────────────────────────┐ t│h Key t│h Default t│h Description t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cfile_container_path0 t│0 cENV["FILE_CONTAINER_0 t│0 Where build artifacts t│ t│0 t│0 cPATH"]0 or c/softwares0 t│0 are stored t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cupdate_secret0 t│0 cENV["UPDATE_SECRET"]0 t│0 Shared secret for t│ t│0 t│0 t│0 c/build/*0 t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 capplication_token_0 t│0 c:env0 t│0 c:env0 or c:database0, t│ t│0 csource0 t│0 t│0 exclusive t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 capplication_token_0 t│0 cnil0 t│0 Owner model for t│ t│0 cowner_class0 t│0 t│0 c:database0 mode t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cmax_upload_size0 t│0 500MB t│0 Cap for c/build/upload0 t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cenforce_software_0 t│0 cfalse0 t│0 DB token owner t│ t│0 cownership0 t│0 t│0 isolation t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cci_adapter0 t│0 c:woodpecker0 t│0 Which CI server builds t│ t│0 t│0 t│0 the games; c:none0 turns t│ t│0 t│0 t│0 CI off, or a host t│ t│0 t│0 t│0 object t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cimage_class_name0 t│0 c"Image"0 t│0 The host model behind t│ t│0 t│0 t│0 the image library t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cimage_adapter0 t│0 cnil0 t│0 Replaces the default t│ t│0 t│0 t│0 host-model adapter t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 caccess_policy0 t│0 c:open0 t│0 Access policy object t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 caccess_token_owner_0 t│0 cnil0 t│0 User model for device t│ t│0 cclass0 t│0 t│0 sign-in; nil = no t│ t│0 t│0 t│0 sign-in t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cadmin_api.*0 t│0 cenabled = false0 t│0 The 2Admin API and its t│ t│0 t│0 t│0 MCP server (1.0) t│ t└────────────────────────┴─────────────────────────┴─────────────────────────┘ A platform spec takes three images. cbuilder0 runs the build steps. cexporter0 is a second image a template may run a step in — the c64 and plus4 pipelines package the c.prg0 into the browser player and the native players there. ctools (1.1, optional) runs the helper steps: version, lint, minify, docs, upload, publish. Without it they run in calpine0 and install git, jq, curl, zip and lua5.4 with capk add0, plus luacheck and ldoc with cluarocks install0 for tic80, on every run; with it they install nothing, so the image must already carry those, with Lua 5.4 as clua0. The upload and publish steps get the capplication_token0 secret, so reference the tools image by digest. teletypegames.org gives every platform its cbuild/ci-tools0 image this way. 1Admin integration 1═════════════════ The host owns the ActiveAdmin instance (auth, theme, routes); WarpEngine appends its resources to cActiveAdmin.application.load_paths0. A new host needs two things: • the file-picker JS for release-asset path inputs (iframe → c/admin/files?picker=1&field=0) in its cactive_admin.js0; • with apipie docs, c"#{WarpEngine::Engine.root}/app/controllers/**/*.rb"0 in capi_controllers_matcher0. The same resources are a JSON API too since 1.0 — see 2WarpEngine Admin API. The admin pages and the API call the same services (cApplicationTokenService0, cDownloadStatsService0, cFileManagerService0), so the two cannot drift apart. 1Behaviour notes 1═══════════════ • Every model is soft-deleted (cdefault_scope { where(deleted_at: nil) }0). • The JSON shape is stable and bug-compatible with the former Go backend: Go zero-time timestamps, camelCase keys, legacy flat path fields. • Model extension points: cActiveSupport.on_load(:warp_engine_)0. 1Testing 1═══════ p bundle install p bundle exec rake app:db:prepare RAILS_ENV=test p bundle exec rspec