WarpEngine is a mountable Rails engine that turns any Rails app into a retro software catalog. A game repo carries a one-line
.woodpecker.yamlmarker, the engine serves the whole CI pipeline, builds are uploaded over HTTP, and a publish call upserts the catalog. A public read-only JSON API and optional ActiveAdmin resources are included. Every class, service and controller is documented in the source: the API reference (RDoc) is rebuilt from it on everywarp_engine-v*tag.
| Page | What it is | Side |
|---|---|---|
| WECP — WarpEngine Catalog Protocol | The read-side HTTP/JSON contract, endpoint by endpoint: what every catalog reader talks to. | protocol |
| WarpEngine Admin API | Since 1.0: what the admin does, as a JSON API with an OpenAPI 3.1 document and an MCP server. Off unless the host turns it on. | protocol |
| WarpEngine Client | The desktop app: the catalog as a grid of cards, install, play, remove. Linux, macOS, Windows. | client |
| warpstore | The shared core of the shell-script store engines: one file that knows the WarpEngine catalog. | client |
| WarpEngine Batocera Store | Any WarpEngine catalog on a Batocera box, under its own EmulationStation menu. | client |
| WarpEngine RetroArch Store | Any WarpEngine catalog as RetroArch playlists with box art. | client |
| TTG Batocera Store · TTG RetroArch Store | Our own two stores: a config on top of each engine above. | store |
| Public site | teletypegames.org, the first host: the monorepo the engine is developed in, and the catalog most readers point at. | host |
| Teletype Orbit | The shop platform on WarpEngine: purchases, entitlements, gated downloads, device sign-in. | host |
Software, Release, ReleaseAsset, ExternalLink, PlatformLink, SoftwareImage, Download. All soft-deleted, with download tracking.WarpEngine::CI::Woodpecker and is the default; :none turns CI off, and another server is a host-supplied object.POST /build/upload and POST /build/publish, authenticated by a shared secret or by database application tokens.Image model, its files, its /api/image/:id 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.rake warp_engine:builds:check prints per software and release which assets exist and which the platform's pipeline could still deliver. See Which assets are missing.INDEX.TXT, for clients that cannot read the JSON API (the WARP64U C64 store reads the C64 shelf over plain HTTP). From 1.7 a shelf can also be one folder per title, the layout DOSINST reads, so the DOS store is built from the catalog too. See Shelves.version step to every generated pipeline and, where the host asks for it, a forge release step at the end, on any platform. See Steps every pipeline shares.shelf, admin_api, additional_resource, ci … do platform … end) instead of hashes; the old |c| form still works. See Installing into a host.v<version> tag from the pipeline itself; the tag's pipeline publishes it. A dev build moves its branch's dev-<branch> tag, so the branch's newest build is always the published one. See Versions and releases.Two lists per platform, and the difference matters. Assets accepted is what the updater ingests when a release is published (WarpEngine::Platform#expected_kinds). The pipeline builds is what the Woodpecker templates actually produce. Where they differ, the rest is a manual upload.
Since 1.5 each accepted kind is also required, expected or optional. A publish without a required kind answers 422 with the missing kinds and records nothing. An expected kind may be missing from a release, and the checklist below reports it. An optional kind may be absent for good and is not counted as missing. GET /api/builds lists all three per platform.
| Platform | Label | Assets accepted | Required | The pipeline builds |
|---|---|---|---|---|
godot |
Godot | html, win_x86, win_x64, linux_x64, mac_universal | html | all of them |
tic80 |
TIC-80 | cartridge, source, html, docs, win_x64, linux_x64, mac_x64 | cartridge | all of them |
bevy |
Bevy | html, win_x64, linux_x64, linux_arm64 | html | all of them |
love |
LÖVE | html, win_x64, linux_x64, mac_universal | html | all of them |
ebitengine |
Ebitengine | html, win_x86, win_x64, linux_x64, linux_arm64, mac_x64, mac_arm64 | html | html, win_x86, win_x64, linux_x64, linux_arm64 |
phaser |
Phaser | html | html | all of them |
c64 |
C64 | cartridge, html, win_x64, linux_x64, mac_arm64 | cartridge | all of them |
plus4 |
Plus/4 | cartridge, html, win_x64, linux_x64, mac_arm64 | cartridge | all of them |
dos |
DOS | cartridge (the installable folder as a zip), win_x86 (optional: the Win32 build some programs have) | cartridge | all of them |
Ebitengine is the one gap: a macOS binary needs osxcross, which the linux builder image does not carry, so mac_x64 and mac_arm64 only ever arrive by hand.
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:
$ bin/rails warp_engine:builds:check
WarpEngine release asset coverage — latest release per software
expected: the kinds a platform's updater registers · [ ] the CI builds it · [-] the CI does not, upload it by hand
rabbitroller ebitengine v1.1.1 3/7
[x] html
[x] win_x64
[x] linux_x64
[ ] win_x86
[ ] linux_arm64
[-] mac_x64
[-] mac_arm64
The CI can build these and the release does not have them:
rabbitroller v1.1.1 (ebitengine): win_x86, linux_arm64
12 softwares, 12 releases, 61/78 assets present, 11 missing from CI-built kinds
4 softwares are missing an asset the CI builds
2 assets are expected but this CI does not build them
| Marker | Meaning |
|---|---|
[x] |
the release has this asset |
[ ] |
the platform expects it and the pipeline builds it — a real gap, and the line worth acting on |
[-] |
expected, but this CI cannot build it (see the table above) — a manual upload, or nothing |
[~] |
optional on this platform (DOS's win_x86): absent, and not counted as missing |
[+] |
the release carries an asset the platform does not expect |
Options are environment variables, so it composes in a shell:
| Variable | Effect |
|---|---|
NAME=rabbitroller |
one software |
PLATFORM=ebitengine |
one platform |
RELEASES=all |
every release, not just the newest per software |
ONLY=missing |
only the releases short of a CI-built asset |
STRICT=1 |
exit non-zero when anything is missing, for a scheduled check |
"Newest" is the newest non-dev- version — the same rule the catalog API uses when it names a title's latest release. The numbers are objects too: WarpEngine::AssetCoverage answers #rows, #missing_rows and #totals, and WarpEngine::AssetCoverageChecklist renders them.
Available in both hosts: the public site and Teletype Orbit.
This is the read side of the API — the WECP, WarpEngine Catalog Protocol. Every endpoint's request and response shape is on the WECP page; this table is the map.
| Endpoint | Purpose |
|---|---|
GET /api/service |
What this deployment is: version, whether the catalog gates, how to sign in, and (since 1.7) its shelves with their addresses. |
GET /api/software |
The catalog with releases, assets, links, download counts, an optional category and an access block — whole, or one page at a time (?page=, ?per_page=), filtered by status, platform, category, q, owner_id. |
GET /api/software/facets |
How many softwares each status, platform and category would show under the other filters. |
GET /api/software/vocabulary |
The statuses and categories this catalog uses, with labels and display order. |
GET /api/software/highlighted |
The highlighted title. |
GET /api/builds |
Expected asset kinds per platform. |
GET /api/softwares/:name/builds |
Actual vs. missing build assets per release. |
GET /api/image/:id |
Images. The host's endpoint since 0.6 — the engine publishes the address, the host serves it. |
GET /api/download?path= |
Serves an artifact and logs a download. |
GET /file/*path |
Static build output (web-playable games, docs). |
POST /api/auth/device |
Starts a device sign-in, returns the code pair. 404 when no client identity is configured. |
POST /api/auth/device/token |
Polls a device sign-in for its token. |
DELETE /api/auth/token |
Revokes the bearer token on the request — sign out. |
Every read endpoint accepts an optional Authorization: Bearer …; none requires one. The token only changes what the access policy is asked. An anonymous caller is a supported caller.
Every response carries a WarpEngine-Version 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.
$ curl -sI https://teletypegames.org/api/software | grep -i warpengine
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.
| Endpoint | Purpose |
|---|---|
POST /build/upload |
One artifact (multipart file, optional sha256). Needs X-Update-Secret. |
POST /build/publish |
Register uploaded artifacts as a release. Needs X-Update-Secret. Since 1.9 a version older than the newest published answers 409. |
POST /build/config |
Woodpecker configuration extension; serves the pipeline YAML. Signature-verified (RFC 9421, ed25519). |
GET /build/config?platform= |
Public preview of the generated YAML. |
Two seams added in 0.5.0, 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.
c.access_policy = MyStore::AccessPolicy.new # :open (default) = the catalog as it was
The policy answers three questions:
| Method | Question |
|---|---|
visible_software_scope |
which titles GET /api/software lists |
access_for |
what a client is told about each title |
authorize_download |
may this artifact be handed over |
authorize_download is asked by /api/download and /file/* before anything is served, so a host no longer has to shadow those routes.
Every catalog entry carries an access block, also under the open policy — a client never has to guess whether the catalog is silent or the title is simply not gated:
"access": { "gated": true, "entitled": false,
"price": { "amountCents": 1490, "currency": "EUR" },
"purchaseUrl": "https://shop.example/games/slug",
"webUrl": "https://shop.example/play/slug" }
The field names are deliberately generic, because one client reads several catalogs.
A policy that raises counts as a refusal: empty catalog, denied download, logged. An artifact must never be served because the gatekeeper crashed.
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.
c.access_token_owner_class = "User" # nil (default): no sign-in anywhere
c.identity_verification_url = "/devices" # your page, where a person types the code
c.subject_resolver = ->(request) { request.env["warden"]&.user } # recognise a browser
POSTs /api/auth/device and shows the userCode;WarpEngine::DeviceGrantService#approve(user_code:, subject:) — approving needs a session and HTML, which are not the engine's business;The token is a WarpEngine::ApplicationToken with the catalog scope, so publishing and client credentials stay separable in one table. subject_resolver 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 access_token_owner_class unset, /api/auth/* answers 404 and /api/service reports auth: null, so the client offers no sign-in.
An image is not a catalog concept — the same picture library serves covers,
screenshots and avatars — so from 0.6.0 the engine owns only the link
(SoftwareImage: ordered, one default), while the model, the files, the
endpoint and the admin page belong to the host. rails g warp_engine:install
writes a working set (app/models/image.rb, app/controllers/api/images_controller.rb,
a create_images migration), and the engine reaches it through an adapter:
c.image_class_name = "Image" # default
c.image_adapter = MyLibrary.new # or replace the adapter entirely
| Method | Used for |
|---|---|
model_name / model |
the class SoftwareImage#image belongs to |
url_for(image_id) |
imageUrl and images[].url in the catalog JSON, and the admin previews |
select_options |
the image picker on the software form |
build_from_upload(upload) |
"upload a new image" on that form |
label_for(record), available?(record) |
the admin's label and "is the file there" check |
The default adapter publishes /api/image/<id> — the address clients have
always used — which is why the generated route serves exactly that. The
images table did not move: the engine simply stopped creating it, and
software_images.image_id still points at the same rows.
A game repo has no pipeline of its own. On every pipeline start Woodpecker POSTs the one-line marker to /build/config and gets the full YAML back.
.woodpecker.yaml holds only platform: (and name: when the software name differs from the repo name).POST /build/config.lib/warp_engine/ci/woodpecker/platforms/<platform>/pipeline.yaml.erb 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 tools image when it has one, and install nothing; without it they run in alpine and apk add (or luarocks install) what they need on every run.A repo opts out by committing a real pipeline YAML: a non-marker config gets 204 No Content and runs unchanged.
# .woodpecker.yaml
platform: godot
name: mygame # optional, defaults to the repo name
// metadata.json — same for every platform except TIC-80
{
"name": "mygame",
"version": "1.0.0",
"title": "My Godot Game",
"author": "You",
"desc": "A cool game",
"license": "MIT",
"category": "puzzle"
}
category is optional free text a client may group titles by — puzzle, action, demo. Since 0.12 it lands on the Software record and in GET /api/software; the WarpEngine Client draws a category axis in its filter as soon as a catalog publishes one. site and repo are optional too and become external links.
| Platform | Source expectations | What the pipeline builds |
|---|---|---|
godot |
export presets: Web, Windows x86, Windows x64, Linux x64, Mac universal | godot --headless --export-release per preset |
bevy |
standard Cargo project; assets/ is packaged next to native binaries |
WASM, linux-x64, win-x64 (mingw) |
love |
standard LÖVE project | love.js web, Windows exe, Linux AppImage, universal macOS .app |
ebitengine |
standard Go project | WASM, win x86/x64, linux x64, mac x64/arm64 |
phaser |
JS files in src/ |
syntax check, Phaser runtime download, web zip |
c64 |
main.asm, or a Makefile with build: make |
ACME assembly into a .prg cartridge (or make <name>.prg, cc65 is in the builder image), then commodore-pack in the exporter image: an EmulatorJS web build with the vice_x64sc core and yapeSDL bundles for linux-x64, win-x64 and mac-arm64 |
plus4 |
the same as c64 |
the same as c64 with the vice_xplus4 core and yapeSDL in Plus/4 mode |
dos |
SOURCE/build.sh, its tests, and the dos block of metadata.json (see DOS) |
the commands of the dos-builder image (dos-tools): the build and the tests on every event; on a tag the installable folder zip (and the Win32 one) and the forge release; the FTP store is the DOS shelf, built from the catalog |
tic80 |
<name>.inc listing fragments from inc/ |
concat, luacheck, minify, LDoc, HTML + cartridge + native binaries |
TIC-80 needs no metadata.json — the metadata is the Lua header:
-- title: My TIC Game
-- name: mygame
-- author: You
-- desc: A TIC-80 game
-- category: puzzle
-- version: 1.0.0
function TIC() end
| Key | Required | Description |
|---|---|---|
platform |
yes | godot, tic80, bevy, love, ebitengine, phaser, c64, plus4, dos, or a platform the host registered |
name |
no | Software name; defaults to the repo name |
release |
no | tag (default since 1.9): the build runs on every event, a push ends with the tag step, and the release steps (package, upload, publish, release) run on tags only. push: the way before 1.9, every push publishes and nothing is tagged. A platform can require tags (release_rule "tag" in its service), and then the marker cannot choose push: DOS is such a platform |
build |
no | make on c64 and plus4: make <name>.prg instead of ACME on main.asm, for a program in C (WARP64U is built this way) |
Any other key is an option a platform's template may read: letters, digits and _ . - only, the rest is dropped. The repo's other .woodpecker/*.yaml files are passed back unchanged, so a repo can keep a workflow of its own (its tests, a deploy) next to the generated one.
curl "https://your-host/build/config?platform=godot"
GET /api/software answers in an envelope — api (the JSON shape's own version, 2), page, perPage, total, totalPages, nextPage, and softwares. Without page the whole catalog comes back in that envelope, which is what every reader before API version 2 finds under softwares; with page it is one page of per_page entries (catalog_page_size when unsaid, at most catalog_max_page_size). Filters narrow the catalog before it is paged; the access policy decides visibility before either.
Statuses and categories are a vocabulary in the database — WarpEngine::SoftwareStatus and WarpEngine::SoftwareCategory, editable in the admin — not a list in the code. A software's status 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 db:schema:load 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 needs_review set. GET /api/software/vocabulary publishes both, and the site's catalog page and the WarpEngine Client take their labels from it.
Every generated pipeline expects one Woodpecker secret, application_token, sent as the X-Update-Secret header to /build/upload and /build/publish.
The tag step (1.9) writes tags with the gitea_token secret, a Forgejo token with repository write access (on GitLab a TAG_TOKEN CI/CD variable, an access token with the api scope). On teletypegames.org it is a global Woodpecker secret, so a new repo needs nothing for it.
Versioning is the same everywhere, because since 1.7 the engine adds the version step itself (see below).
Since 1.7 no template carries these steps; the engine adds them to every pipeline it renders:
version, first. The version comes from metadata.json (or the -- version: line of the Lua header on TIC-80). On a tag a version tag (v1.2.3 or 1.2.3) has to match it, and is the version when the source names none; a tag that does not match fails the pipeline. Off the repo's default branch it becomes dev-<version>-<branch>, e.g. dev-1.0.0-my-feature, and so on a dev-<branch> tag; with no version at all the build is dev-<commit>, which is never the latest. Since 1.9 the default branch is the repo's own (CI_REPO_DEFAULT_BRANCH), so a repo whose default is develop releases from develop; before, it was main or master.tag (1.9), after a push build, unless the marker says release: push: tags a new version, or moves the branch's dev tag. See Versions and releases.release, last, on a version tag (a dev- tag makes none), when the host sets forge_release: true in the platform's entry (any platform can have it): the Gitea/Forgejo release of the tag (a GitLab release on GitLab), named after the title and the tag, with RELEASE_NOTES.md as its text, made with the gitea_token secret. It attaches the files the build listed in .release-assets, or else the files the upload step uploaded. On teletypegames.org only dos has it.Since 1.9.1 a generated Woodpecker pipeline starts on every event but release (a workflow when, left out where a template has its own): moving a dev tag makes Forgejo send a release event besides the tag one, and that built the same commit a second time.
Every template names its steps the same way: the one that sends the files to /build/upload is upload (c64, plus4, ebitengine and dos called it artifact before 1.7), the one that calls /build/publish is publish. On c64 and plus4 only the players that were built are uploaded; LÖVE no longer uploads a .love.zip.
Since 1.9 nobody tags by hand and nobody publishes by hand: the version in the source decides everything.
version in metadata.json, the -- version: line on TIC-80. Raising it is the release.tag step tags the pushed commit v<version> (through the forge API). That tag starts a pipeline of its own, which uploads and publishes. A forge release is made too where the platform has forge_release.version step fails the pipeline, and the tag step leaves an existing tag alone./build/publish answers 409 to a version older than the newest one published (by Gem::Version): it would become the latest release and bring back its old metadata. Publishing the same version again, a newer one, or any dev- version goes through.dev-<version>-<branch>; with no version in the source, dev-<commit>. For such a build the tag step moves the branch's dev-<branch> tag onto the pushed commit (creating it the first time), and the tag's pipeline publishes the dev build, overwriting the previous one of the same name. A dev version is never a title's latest release, and makes no forge release.release: push in the marker publishes on every push, with no tags, as before 1.9. DOS cannot opt out.The earlier versions of every engine-built teletypegames.org repo were tagged in one go on 2026-10-27 (script/backfill_version_tags.rb in the monorepo: each version's tag on the first default-branch commit that carried it), so the tags list a repo's whole history.
Since 1.5 the templates hold no host's addresses. A platform's entry in the CI adapter's platforms: carries what differs from host to host:
web_shell: — the page a bevy, ebitengine or phaser browser build is wrapped in. With a URL the pipeline downloads it; without one it takes the repo's own web/index.html.bundle_id_prefix: — the LÖVE macOS app's bundle id is <prefix>.<name> (default org.warpengine).exporter: — on c64 and plus4, the image with commodore-pack <c64|plus4> <name> <version>, which writes the browser player and the three native players next to the PRG.Our hosts set the Teletype Games values, so their pipelines render as before.
A host adds a platform in its initializer — a service class, the kinds its CI builds, and a pipeline template per CI adapter:
c.register_platform "amiga",
service: "AmigaPlatform",
built_kinds: %w[cartridge],
templates: { woodpecker: Rails.root.join("ci/amiga.woodpecker.yaml.erb") }
The service includes WarpEngine::Platforms::Updatable and the build concerns of its kinds, and names itself (platform "amiga", label "Amiga", required_kinds "cartridge"). From then on it is a platform like the others: a software may be on it, a publish runs its service, the admin lists it and the config extension renders its template. Everything that used to read the fixed list reads WarpEngine::Platform.names now.
A shelf (since 1.2) is a folder the engine keeps in step with the catalog: for one platform, the latest release of every public title under a fixed, versionless name, plus an INDEX.TXT (file|version|size|crc32|title|author|description, ASCII, at most 255 bytes a line). It is for clients that cannot read the JSON API over HTTPS. c.shelves names the platform, kind, path, extension and name length; it needs the :local storage adapter. The software's shelf_name (from metadata.json, the admin or the Admin API) names the file. A shelf is rebuilt on every publish and every change to its titles, the index replaced last; rake warp_engine:shelves:check and warp_engine:shelves:sync check and rebuild it by hand.
Since 1.7 a shelf can have layout: "folders": one folder per title, the layout DOSINST reads. The engine unpacks the release zip's top folder, leaves out the settings files of the metadata's dos block (so an update keeps an installed program's configuration), and writes two files into each folder: INFO.INI ([program] with name, description, version and platform DOS or WIN32, CRLF) and FILES.LST (crc32 size path, CRLF). kind may be a list: the DOS shelf holds cartridge and win_x86, so DOSINST, for example, has a DOSINST and a DOSINSTW folder. Only folders the shelf's INDEX.TXT listed are ever removed, so a folder put there by hand (a test program, say) stays.
A shelf also has urls, where the host serves it, and GET /api/service lists the shelves under shelves (platform, kinds, layout, index, urls). Serving stays the host's job: the engine writes the folder and runs no FTP or HTTP server of its own.
teletypegames.org keeps three shelves, served by the FTP server over FTP and plain HTTP:
| Shelf | Folder | Layout | Served at |
|---|---|---|---|
| C64 | shelves/C64 |
flat | ftp://ftp.teletypegames.org/C64, http://archive.teletypegames.org/C64 |
| Plus/4 | shelves/PLUS4 |
flat | ftp://ftp.teletypegames.org/PLUS4, http://archive.teletypegames.org/PLUS4 |
| DOS | shelves/APPS |
folders | ftp://ftp.teletypegames.org/APPS, http://archive.teletypegames.org/APPS |
Active only where the adapter has a url and an api_token (WarpEngine.ci.configured?). Until then the admin pages hide themselves.
Pipeline records, matching each repo to a Software by name and inheriting its platform. Repos gone from Woodpecker are deactivated. Links stay editable by hand.application_token secret to the owner's repos (unrestricted tokens: every active repo); deleting removes it; Rotate creates a replacement, provisions it everywhere and revokes the old one in one step.The PAT must belong to a Woodpecker instance admin — listing repos is an admin-only endpoint, anything else returns 403 User not authorized. Add the user to WOODPECKER_ADMIN, then log out and back in: the admin flag is written at login, a server restart is not enough.
X-Update-Secret carries one of two credential types, chosen by application_token_source. The modes are exclusive.
| Mode | Credential |
|---|---|
:env (default) |
the shared secret in update_secret; unset means every request is rejected |
:database |
WarpEngine::ApplicationToken records: an owner (e.g. AdminUser), scopes (update to publish, upload to upload), optional expiry. Created in the admin; the plain token is shown once, only its SHA256 digest is stored. last_used_at tracks use. |
Cutover warning: switching to
:databaseinvalidates the shared secret immediately. Create the tokens first.
# Gemfile — from git:
gem "warp_engine", git: "https://git.teletypegames.org/engines/warp_engine.git"
# ...or from the Forgejo rubygems registry (tagged releases):
source "https://git.teletypegames.org/api/packages/engines/rubygems" do
gem "warp_engine"
end
rails g warp_engine:install # initializer, image library, migrations
rails db:migrate
# config/routes.rb
namespace :api do
get "image/:id", to: "images#show" # the image library is yours (see below)
end
# keep the mount last so the host's own routes win
mount WarpEngine::Engine => "/"
The generator writes config/initializers/warp_engine.rb. Since 1.8 it is a block without a parameter, one line per setting:
Rails.application.config.to_prepare do
WarpEngine.configure do
file_container_path ENV.fetch("FILE_CONTAINER_PATH", "/softwares")
# The image library is the host's; "Image" is the adapter's default.
# image_class "Media::Picture"
# image_adapter MyImageLibrary.new
update_secret ENV["UPDATE_SECRET"]
# application_tokens from: :database, owner: "AdminUser"
# One adapter carries the whole CI side: server, recipes, signing key.
ci :woodpecker do
url ENV["WOODPECKER_URL"]
api_token ENV["WOODPECKER_API_TOKEN"]
repo_owner ENV["WOODPECKER_REPO_OWNER"]
public_key_url "https://ci.example.org/api/signature/public-key"
update_server "https://catalog.example.org"
tools "registry.example/ci-tools:1.0" # every platform's, unless it names its own (1.1)
platform :godot, builder: "registry.example/godot-builder:4.6"
platform :tic80, builder: "registry.example/tic80-builder:1.0"
platform :bevy, builder: "registry.example/bevy-builder:1.0"
platform :love, builder: "registry.example/love-builder:1.0"
platform :ebitengine, builder: "registry.example/ebitengine-builder:1.0"
platform :phaser, builder: "registry.example/phaser-builder:1.0"
platform :c64, builder: "registry.example/c64-builder:1.0", exporter: "registry.example/commodore-player:1.0"
platform :plus4, builder: "registry.example/plus4-builder:1.0", exporter: "registry.example/commodore-player:1.0"
end
# max_upload_size 500 * 1024 * 1024
# enforce_software_ownership true
end
end
The block also takes shelf …, admin_api do … end and additional_resource … do … end (permitted attributes, the admin form, and an api_schema built from string/integer/… lines in camelCase). An unknown setting or key stops the boot. The settings carry the names of the table below, except image_class (image_class_name), application_tokens from:, owner: (the two application_token_* keys) and ci (ci_adapter). The WarpEngine.configure do |c| c.site_url = … end form of the versions before 1.8 still works; both write the same configuration.
| Key | Default | Description |
|---|---|---|
file_container_path |
ENV["FILE_CONTAINER_PATH"] or /softwares |
Where build artifacts are stored |
update_secret |
ENV["UPDATE_SECRET"] |
Shared secret for /build/* |
application_token_source |
:env |
:env or :database, exclusive |
application_token_owner_class |
nil |
Owner model for :database mode |
max_upload_size |
500MB | Cap for /build/upload |
enforce_software_ownership |
false |
DB token owner isolation |
ci_adapter |
:woodpecker |
Which CI server builds the games; :none turns CI off, or a host object |
image_class_name |
"Image" |
The host model behind the image library |
image_adapter |
nil |
Replaces the default host-model adapter |
access_policy |
:open |
Access policy object |
access_token_owner_class |
nil |
User model for device sign-in; nil = no sign-in |
admin_api.* |
enabled = false |
The Admin API and its MCP server (1.0) |
A platform spec takes three images. builder runs the build steps. exporter is a second image a template may run a step in — the c64 and plus4 pipelines package the .prg into the browser player and the native players there. tools (1.1, optional) runs the helper steps: version, lint, minify, docs, upload, publish. Without it they run in alpine and install git, jq, curl, zip and lua5.4 with apk add, plus luacheck and ldoc with luarocks install for tic80, on every run; with it they install nothing, so the image must already carry those, with Lua 5.4 as lua. In the DSL, tools in the ci block is every platform's default. The upload and publish steps get the application_token secret, so reference the tools image by digest. teletypegames.org gives every platform its build/ci-tools image this way.
The host owns the ActiveAdmin instance (auth, theme, routes); WarpEngine appends its resources to ActiveAdmin.application.load_paths. A new host needs two things:
/admin/files?picker=1&field=<dom_id>) in its active_admin.js;"#{WarpEngine::Engine.root}/app/controllers/**/*.rb" in api_controllers_matcher.The same resources are a JSON API too since 1.0 — see WarpEngine Admin API. The admin pages and the API call the same services (ApplicationTokenService, DownloadStatsService, FileManagerService), so the two cannot drift apart.
deleted_at column and the kept / discarded scopes. Since 0.10 there is no default_scope, so a query does not filter by itself — a host asks for WarpEngine::Software.kept, and one upgrading from 0.9 or earlier adds .kept where it relied on the default scope.ActiveSupport.on_load(:warp_engine_<model>).bundle install
bundle exec rake app:db:prepare RAILS_ENV=test
bundle exec rspec