EN /HU | Login

WarpEngine

WarpEngine is a mountable Rails engine that turns any Rails app into a retro software catalog. A game repo carries a one-line .woodpecker.yaml marker, 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 every warp_engine-v* tag.

WarpEngine related pages

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

What you get

  • Catalog domain — Software, Release, ReleaseAsset, ExternalLink, PlatformLink, SoftwareImage, Download. All soft-deleted, with download tracking.
  • CI pipeline server — a Woodpecker configuration extension. Game repos hold only a platform marker. Builder images are server-side, so bumping an image reaches every repo at once.
  • Pluggable CI — from 0.7 every CI feature goes through one adapter. Woodpecker ships with the engine as WarpEngine::CI::Woodpecker and is the default; :none turns CI off, and another server is a host-supplied object.
  • Build endpoints — POST /build/upload and POST /build/publish, authenticated by a shared secret or by database application tokens.
  • Public JSON API — catalog, highlighted title, per-platform build matrix, download tracking, static file server for web builds.
  • Host-owned image library — from 0.6 the 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.
  • Pluggable access — 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.
  • Client sign-in — RFC 8628 device authorization grant over the host's user model, for clients with no browser. Off unless configured.
  • Optional admin — ActiveAdmin resources (catalog editor, file manager, download stats, app tokens). Without ActiveAdmin the engine runs headless.
  • CI management — with an API token the admin drives the CI server: repo sync, pipeline history with manual triggers, application tokens provisioned as CI secrets.
  • Asset checklist — from 0.8, 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.
  • Shelves — from 1.2, a platform's latest builds as a flat folder under fixed, versionless names with an 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.
  • Platforms of your own — from 1.5, a host registers a platform with its service, kinds and pipeline template; the built-in ones are no longer the only ones. See Platforms of your own.
  • Shared version and release steps — from 1.7 the engine adds the 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.
  • Configuration DSL — from 1.8 the initializer is a block of named settings (shelf, admin_api, additional_resource, ci … do platform … end) instead of hashes; the old |c| form still works. See Installing into a host.
  • Releases from tags the pipeline makes — from 1.9 a push builds, and a new version in the source gets its 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.

Supported platforms

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.

Which assets are missing

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:

TXT
$ 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.

Endpoints

Public API

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.

SH
$ 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.

Build / CI

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.

Access and identity

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.

Who may see and download what

RUBY
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:

JSON
"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.

Signing a client in

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.

RUBY
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
  1. the client POSTs /api/auth/device and shows the userCode;
  2. the person opens the verification page and types that code;
  3. the host's page calls WarpEngine::DeviceGrantService#approve(user_code:, subject:) — 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 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.

The image library

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:

RUBY
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.

Using Woodpecker with WarpEngine

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.

  1. Woodpecker is configured with WarpEngine as a configuration extension.
  2. The repo's .woodpecker.yaml holds only platform: (and name: when the software name differs from the repo name).
  3. A push sends the marker to POST /build/config.
  4. The CI adapter renders 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.
  5. The pipeline runs. Since 1.9, after a push: version → build → tag; on the tag that step made: version → build → upload → publish (→ release, where the host turns it on). See Versions and releases.

A repo opts out by committing a real pipeline YAML: a non-marker config gets 204 No Content and runs unchanged.

Setting up a game repo

YAML
# .woodpecker.yaml
platform: godot
name: mygame     # optional, defaults to the repo name
JSON
// 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:

LUA
-- title: My TIC Game
-- name: mygame
-- author: You
-- desc: A TIC-80 game
-- category: puzzle
-- version: 1.0.0
function TIC() end

Marker options

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.

Preview a pipeline

SH
curl "https://your-host/build/config?platform=godot"

Paging and vocabulary (0.13)

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.

Secrets and versions

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).

Steps every pipeline shares

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.

Versions and releases

Since 1.9 nobody tags by hand and nobody publishes by hand: the version in the source decides everything.

  1. The version lives in the source — version in metadata.json, the -- version: line on TIC-80. Raising it is the release.
  2. A push to the default branch with a new version builds, and the 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.
  3. A push with a version that is tagged already only builds. Committing without raising the version publishes nothing.
  4. A tag made by hand works as before: it has to match the version in the source, or the version step fails the pipeline, and the tag step leaves an existing tag alone.
  5. Versions only grow. /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.
  6. Dev builds always follow the branch. Off the default branch the version is 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.
  7. Opting out: 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.

Values a template takes from the host

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.

Platforms of your own

A host adds a platform in its initializer — a service class, the kinds its CI builds, and a pipeline template per CI adapter:

RUBY
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.

Shelves

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

CI management (Woodpecker API)

Active only where the adapter has a url and an api_token (WarpEngine.ci.configured?). Until then the admin pages hide themselves.

  • Repo sync — Admin → Pipelines → Sync from Woodpecker (the button carries the adapter's name). Mirrors the repo list into 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.
  • Pipelines — each entry lists its recent runs (status, branch, message, time) with a manual Trigger. The newest run refreshes the cached status on the index. A software's admin page links to its pipelines.
  • Secret provisioning — creating a token provisions the 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.

Build authentication

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 :database invalidates the shared secret immediately. Create the tokens first.

Installing into a host

RUBY
# 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
SH
rails g warp_engine:install   # initializer, image library, migrations
rails db:migrate
RUBY
# 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:

RUBY
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.

Admin integration

The host owns the ActiveAdmin instance (auth, theme, routes); WarpEngine appends its resources to ActiveAdmin.application.load_paths. A new host needs two things:

  • the file-picker JS for release-asset path inputs (iframe → /admin/files?picker=1&field=<dom_id>) in its active_admin.js;
  • with apipie docs, "#{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.

Behaviour notes

  • Every model is soft-deleted: a 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.
  • 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: ActiveSupport.on_load(:warp_engine_<model>).

Testing

SH
bundle install
bundle exec rake app:db:prepare RAILS_ENV=test
bundle exec rspec