A checklist of the release assets that exist and the ones the CI could still build

Nothing said whether a release was finished. A pipeline step can fail, a
platform can gain a target, a title can be published from a laptop — and the
catalog happily serves a release with three of its seven assets. The only way
to find out was to open `/api/softwares/<name>/builds` one name at a time.

    $ bin/rails warp_engine:builds:check

    rabbitroller  ebitengine  v1.1.1  1/7
      [x] html
      [ ] win_x86
      [ ] win_x64
      [ ] linux_x64
      [ ] 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, win_x64, linux_x64, linux_arm64

    5 softwares, 5 releases, 14/32 assets present, 16 missing from CI-built kinds

The third marker is the point. Two different questions were being conflated:
what the **updater** can ingest (`Platform#expected_kinds`) and what the
**pipeline** actually produces. For six of the seven platforms they are the
same list; for ebitengine they are not — the updater accepts macOS builds, the
Woodpecker builder cannot cross-compile them (osxcross), so `mac_x64` and
`mac_arm64` would be reported as gaps on every ebitengine release, for ever.
`[ ]` is now a real gap and `[-]` is "upload it by hand or not at all", so the
list stays worth reading.

That knowledge is data, next to the templates that produce it:
`PipelineConfig::BUILT_KINDS`, reachable through the adapter as
`built_kinds(platform)`. An adapter that cannot answer — no CI configured, a
platform with no builder — returns nil, and then nothing is excused: every
missing kind counts as a gap.

`WarpEngine::AssetCoverage` computes it (`#rows`, `#missing_rows`, `#totals`)
and `AssetCoverageChecklist` renders it, so the numbers are testable without a
terminal. Options are env vars: NAME, PLATFORM, RELEASES=all, ONLY=missing, and
STRICT=1 for a non-zero exit in CI. The default is the newest release per
software, using the same non-`dev-` rule the catalog API uses for "latest".

33 new examples, including two that keep BUILT_KINDS honest: every claimed kind
has to be a kind the updater knows, and has to appear in the platform's rendered
pipeline. Adding a platform without teaching this list about it fails the suite.

Ships in 0.8.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 09:34:23 +02:00
co-authored by Claude Opus 5
parent 1fb09a6094
commit 0adde2070d
10 changed files with 557 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
namespace :warp_engine do
namespace :builds do
desc "Checklist of which release assets exist and which the platform expects " \
"(NAME=slug PLATFORM=godot RELEASES=all ONLY=missing STRICT=1)"
task check: :environment do
coverage = WarpEngine::AssetCoverage.new(
name: ENV["NAME"],
platform: ENV["PLATFORM"],
releases: ENV["RELEASES"] == "all" ? :all : :latest
)
checklist = WarpEngine::AssetCoverageChecklist.new(
coverage, only_missing: ENV["ONLY"] == "missing"
)
puts checklist.to_s
if ENV["STRICT"] == "1" && coverage.totals[:missing].positive?
abort "\nSTRICT: #{coverage.totals[:missing]} expected assets are missing."
end
end
end
end
+1
View File
@@ -43,6 +43,7 @@ module WarpEngine
def name = "none"
def configured? = false
def platforms = {}
def built_kinds(_platform) = nil
def update_server = nil
def repos = raise(NotConfigured, MESSAGE)
+2
View File
@@ -28,6 +28,8 @@ module WarpEngine
def platforms = @config.platforms
def built_kinds(platform) = @config.built_kinds(platform)
def client
@client ||= Client.new(url: @url, token: @api_token)
end
@@ -7,6 +7,16 @@ module WarpEngine
class PipelineConfig
PLATFORM_FORMAT = /\A[a-z0-9_-]+\z/
BUILT_KINDS = {
"tic80" => %w[cartridge source html docs win_x64 linux_x64 mac_x64],
"ebitengine" => %w[html win_x86 win_x64 linux_x64 linux_arm64],
"godot" => %w[html win_x86 win_x64 linux_x64 mac_universal],
"love" => %w[html win_x64 linux_x64 mac_universal],
"bevy" => %w[html win_x64 linux_x64 linux_arm64],
"c64" => %w[cartridge],
"phaser" => %w[html]
}.freeze
def initialize(platforms: {})
@platforms = platforms.to_h { |key, spec| [ key.to_s, spec.to_h.symbolize_keys ] }
end
@@ -33,6 +43,13 @@ module WarpEngine
)
end
def built_kinds(platform)
platform = platform.to_s
return nil unless @platforms.key?(platform)
BUILT_KINDS[platform]
end
def self.templates_dir
Pathname.new(__dir__).join("platforms")
end