challenge.yml Reference
Every challenge in this platform is described by a single challenge.yaml (or challenge.yml) file at the root of its package directory. The same file is read in three situations: an admin uploads a one-shot tarball/zip, a public GitHub repo is imported, and a repo binding re-syncs a checkout. All three paths share one parser, so the schema below is identical everywhere.
This page documents the exact fields the importer understands, where each value lands, and what is deliberately ignored. Everything here is derived from ChallengeYamlModel and ChallengeImportService in the server — not from the upstream gzcli docs, which differ in places.
The file may be named either challenge.yaml or challenge.yml. The importer treats the directory that contains it as the package root: paths like provide: and ./src/Dockerfile are resolved relative to that directory.
Key conventions
- Keys are camelCase. The deserializer is built with
CamelCaseNamingConvention. A key likecontainer_image(snake_case) is silently dropped — usecontainerImage. This applies to nested blocks too (memoryLimit,cpuCount,checkerImage,allowEgress, …). - Unknown keys are ignored, not errors. The parser runs with
IgnoreUnmatchedProperties(), so a typo or an upstream gzcli field we don't map simply has no effect. There is no validation warning for an unrecognized key — double-check spelling against the tables below. - Some template keys have no effect.
value:andvisible:are not fields the importer maps — like any unrecognized key they are silently dropped byIgnoreUnmatchedProperties(). They exist in templates only so the file round-trips with gzcli; scoring and visibility stay admin-controlled. - A missing
containerImagetriggers an auto-build. For any container-type challenge, ifcontainer.containerImageis omitted (or is a gzcli template placeholder like{{.slug}}:latest), the platform looks for./src/Dockerfile, then./Dockerfile, and builds it. See Auto-build behavior.
Top-level keys
These keys sit at the root of challenge.yml.
value: is not a mapped field — it is silently ignored like any unknown key, so it never affects OriginalScore. Scoring is admin-controlled in the UI or the admin scoring API. Putting value: 500 in the file does nothing — see Scoring. Likewise visible: is ignored; only an admin flips IsEnabled.
The type enum
type is parsed case-insensitively into the ChallengeType enum. Accepted values:
AttackDefense and KingOfTheHill both report IsContainer() == true and run on the shared A&D engine (UsesAdEngine() == true). That is why both reuse the container: block for their service image and honor the ad: block.
For A&D and KotH the importer does not require a flags: list or a flagTemplate: — those types are exempt from the flag-source submission check. A&D plants a fresh per-team flag into the container each tick; KotH has no flag at all (the platform reads /koth/king).
The container: block
Used by all container types, including AttackDefense and KingOfTheHill. Every key is optional; an omitted key leaves the existing value untouched (so a sparse block only overrides what it names). Container fields are applied only when type is a container type.
Auto-build behavior
When the challenge is a container type, the importer resolves a build intent from containerImage:
- Local path (
./src,./Dockerfile,/…, anything ending in/Dockerfile) → resolve the build context and build. If the referenced Dockerfile doesn't exist, the challenge is flaggedMissingDockerfile. - Template placeholder (
containerImagecontains{{, e.g.{{.slug}}:latest) or omitted → look for./src/Dockerfile, then./Dockerfile. If found, build it; the platform fills in the image tag. If a placeholder was given but no Dockerfile exists, it's aMissingDockerfileerror. - Registry ref (
nginx:alpine,ghcr.io/foo:tag) → pulled as-is, no build.
The conventional layout is a ./src/Dockerfile next to challenge.yml. With that in place you can omit containerImage entirely and the platform builds the image for you. See Templates for the full package layout.
Only trusted imports auto-build immediately. Admin uploads and repo-binding syncs run with auto-approve and build right away. A player-submitted package's attacker-controlled Dockerfile is not built on the shared host until an admin approves it — the challenge stays pending in the review queue.
The ad: block
The ad: block carries the per-challenge Attack & Defense / King of the Hill knobs. It is consulted only when type is AttackDefense or KingOfTheHill (UsesAdEngine()), and it is sparse-friendly: an omitted key keeps the current value. The service image and ports come from the shared container: block — ad: only carries the engine-specific switches.
checkerImage must be a registry reference that has actually been pushed (e.g. ghcr.io/your-org/checker:latest), not a local-only tag. The cluster/daemon pulls it fresh, and local-only images are reaped by image-prune jobs — a pruned checker image makes every tick go InternalError. Build ./checker and push it before referencing it.
Event-wide A&D policy is NOT set here. Tick length, flag lifetime, warmup, reset cooldown, snapshot toggles, and the checker timing fractions (getflagWindowFraction, minGracePeriodSeconds) are configured once per event in the ad: block of the .gzevent manifest (or admin game settings), because rounds span the whole game and every service shares one tick. KotH hold scoring (KothHoldPointsPerTick, KothRefreshTicks) is currently fixed at defaults on the Game row. Putting any of these on a single challenge has no effect. See appsettings / config.
Complete example: Attack & Defense
Complete example: King of the Hill
KotH needs no flags: or flagTemplate:. The platform determines the king by reading the /koth/king marker file from the hill, and uses the checker purely as a health probe (Ok = working, Mumble = degraded, Offline = down).
See also
- Templates — the package layout,
./src/Dockerfile,./checker, andsolver/conventions. - Repo bindings — keeping challenges in sync from a GitHub repo.
- Scoring — how points, decay (
minScoreRate/difficulty), and blood bonuses actually work, given thatvalue:is ignored.