- Delete portal_mode_manager.gd, portal_door.gd, portal_door.tscn - Strip all Tekton Doors logic from main.gd, player.gd, lobby.gd, lobby_room.gd, lobby_manager.gd, camera_context_manager.gd, music_manager.gd, tekton.gd, enhanced_gridmap.gd, playerboard_manager.gd, special_tiles_manager.gd - Remove TK enum (TEKTON_DOORS=2), mode_config schema, arena area - Update tests: 3 modes instead of 4 - Strip HowToPlay tab from main.tscn
13 KiB
Patch & Release Workflow
Complete guide for shipping updates to Tekton players — hot patches (.pck) for content changes and full binary releases for engine/platform changes.
Overview
Two automated CI pipelines handle all distribution:
| Pipeline | Trigger | Output | Delivery |
|---|---|---|---|
Deploy Patch (deploy_patch.yml) |
Manual workflow dispatch | patch.pck + version.json → patches branch |
Gitea raw endpoint |
Release (ci.yml) |
Git tag v* push |
Windows/Linux/macOS .zip → Gitea Release |
git.klud.top releases |
Infrastructure
Gitea instance
- URL: https://git.klud.top
- API: http://52.74.133.55:3000/api/v1
- Runner: Local Docker container (
gitea-runner) viadocker-compose - Cache volume:
/home/dev/godot-cache→/cache(rw) inside runner containers - Secret:
TEKTON_RELEASE_TOKEN— Token from useradtpdnwith repo write access
Patch serving
Patches served directly from Gitea's built-in raw file endpoint — no external CDN:
- Manifest:
https://git.klud.top/danchie/tekton/raw/branch/patches/version.json - PCK:
https://git.klud.top/danchie/tekton/raw/branch/patches/patch.pck
Old raw.klud.top (gitea-pages container) retired — Gitea raw endpoint is faster, simpler, and always available.
Release page
- URL: https://git.klud.top/danchie/tekton/releases
- Assets auto-uploaded by CI on tag push
Part 1: Hot Patch (content-only updates)
Use when: script changes, UI tweaks, balance patches, asset replacements, config changes.
Step-by-step
1. Write changelog
Edit CHANGELOG_DRAFT.md — add player-facing notes under ## [NEXT]:
## [NEXT]
- Fixed playerboard desync in multiplayer.
- Adjusted Gauntlet difficulty scaling.
If [NEXT] is missing, add the header. Format is markdown list items without leading dash (the tool strips it). Each line becomes a bullet on the patch notes page.
2. Commit to experimental
git add CHANGELOG_DRAFT.md
git commit -m "docs: patch notes for next release"
git push origin experimental
3. Trigger patch deploy workflow
Navigate to the Actions tab:
https://git.klud.top/danchie/tekton/actions
Click Deploy Patch → Run workflow:
| Field | Example |
|---|---|
| Patch version | 2.4.3 |
| Release notes | fix: multiplayer desync, gauntlet balance |
OR via API:
curl -X POST "http://52.74.133.55:3000/api/v1/repos/danchie/tekton/actions/workflows/deploy_patch.yml/dispatches" \
-H "Authorization: token $TEKTON_RELEASE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ref":"experimental","inputs":{"version":"2.4.3","notes":"fix: multiplayer desync, gauntlet balance"}}'
What the CI does (deploy_patch.yml)
- Checkout —
git clone --depth 1fromexperimentalbranch (shallow = fast). - Setup Godot — Uses cached
/cache/godot_4.7binary. Downloads only if missing (140MB, cached forever). - Generate version.json — Runs
tools/generate_version_json.py --skip-changelog. Reads version fromproject.godot, bumps patch number, writesassets/data/version.jsonwith the new release entry includingpck_urlpointing to Gitea raw endpoint. - Export patch PCK —
godot --headless --export-pack "Windows Desktop" build/patch.pck. No export templates needed —--export-packonly packs resources, not binaries. Output ~10-15MB. - Push to patches branch — Force-pushes
patch.pck+version.jsonto thepatchesbranch of the repo.
Verification
# Check manifest
curl -s "https://git.klud.top/danchie/tekton/raw/branch/patches/version.json"
# Expected: latest_version matches your patch number
# Check pck exists
curl -s -o /dev/null -w "%{http_code} %{size_download}B" \
"https://git.klud.top/danchie/tekton/raw/branch/patches/patch.pck"
# Expected: HTTP 200, size ~10-15MB
How players receive patches
- Game boots →
GameUpdateManagerfetchesversion.jsonfrom Gitea raw endpoint. - Compares
latest_versionagainst local version. - If remote is newer → downloads
patch.pcktouser://patch.pck. - Mounts with
ProjectSettings.load_resource_pack("user://patch.pck"). - All files in
patch.pckoverride baseres://files in memory. - No files are overwritten on disk (safe rollback by deleting
patch.pck).
Part 2: Full Binary Release (platform updates)
Use when: engine upgrade, native plugin change, export template update, platform-specific build fix, or any change that needs a new .exe/.app.
Step-by-step
1. Ensure changelog is written
Same as patch step 1 — CHANGELOG_DRAFT.md must have ## [NEXT] entries. The CI auto-extracts them for the release body.
2. Commit and tag
# Commit all changes
git add -A
git commit -m "chore: bump to v2.4.3"
# Push to experimental
git push origin experimental
# Create and push tag
git tag v2.4.3 experimental
git push origin v2.4.3
IMPORTANT: Tag must match v + version format (e.g. v2.4.3). The CI is triggered by v* tags.
What the CI does (ci.yml)
- Install tools —
apt-get install curl unzip zip(zip was missing in early runs — make sure it's present). - Checkout — Full clone from tag (shallow not used — needs full history for changelog extraction, though
--depth 1works too). - Setup Godot with templates — Caches both Godot binary (140MB) and export templates (1.3GB) in
/cache/. Templates downloaded once per runner lifetime. - Export 3 platforms:
- Windows —
godot --headless --export-release "Windows Desktop"→ zipped withzip. - Linux/X11 — Same pattern.
- macOS — Export to
.zipdirectly (Godot's macOS export produces a zip). - Note: Steam DLLs copied into Windows build from
addons/godotsteam/. - Note:
|| trueon export commands masks Godot errors (e.g. GodotSteam plugin warnings). Real failures (missingzip) will surface.
- Windows —
- Extract changelog — Parses
CHANGELOG_DRAFT.mdfor the## [version]section matching the tag. Writes to$CHANGELOG_BODYenv var. - Create/Update Gitea Release — Checks if release exists for tag. Creates new one with changelog as body if missing. Updates draft release if re-run.
- Upload assets — Each
.zipuploaded as release asset via multipart POST. - Publish — Sets
draft:falseto make release public.
Verification
Check the release page:
https://git.klud.top/danchie/tekton/releases/tag/v2.4.3
Expected: 3 assets (Windows, Linux, macOS) with correct sizes, changelog body populated, release marked as published (not draft).
Cleaning duplicate assets
If a tag was force-pushed, multiple CI runs may upload duplicate assets to the same release:
# List assets
curl "https://git.klud.top/api/v1/repos/danchie/tekton/releases/tags/v2.4.3" \
-H "Authorization: token $TEKTON_RELEASE_TOKEN" | jq '.assets[] | "\(.id): \(.name) \(.size/1024/1024)MiB"'
# Delete old duplicates (keep latest 3: Windows, Linux, macOS)
RELEASE_ID=<id>
curl -X DELETE "http://52.74.133.55:3000/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets/$ASSET_ID" \
-H "Authorization: token $TEKTON_RELEASE_TOKEN"
Cancelling stuck or duplicate runs
Gitea API cannot cancel in-progress runs. Wait for completion, then delete:
# List runs for a tag
curl "http://52.74.133.55:3000/api/v1/repos/danchie/tekton/actions/runs?page=1&limit=10" \
-H "Authorization: token $TEKTON_RELEASE_TOKEN" | jq '.workflow_runs[] | "\(.id): \(.status) \(.conclusion) \(.display_title)"'
# Delete completed run
curl -X DELETE "http://52.74.133.55:3000/api/v1/repos/danchie/tekton/actions/runs/$RUN_ID" \
-H "Authorization: token $TEKTON_RELEASE_TOKEN"
Part 3: Agent-Automated Release
Agent (Hermes) can execute the full release flow from a single user request:
Scenario: "Ship v2.4.3"
Agent actions:
- Read
CHANGELOG_DRAFT.md— verify[NEXT]has entries. - Check
project.godotcurrent version. - Commit changelog to
experimental. - Create tag
v2.4.3→ push to triggerci.yml. - Wait for CI completion (poll every 30s, up to 30 min).
- If CI fails:
- Read job logs for failure reason.
- Fix the workflow file, commit, force-push tag.
- Clean up duplicate assets after re-run.
- Verify release page has 3 assets with correct sizes.
- If patch deploy also needed:
- Trigger
deploy_patch.ymldispatch. - Verify
patch.pckis served and version.json updated.
- Trigger
Scenario: "Quick hot patch"
Agent actions:
- Check if
[NEXT]has entries inCHANGELOG_DRAFT.md. - If empty, ask user for changelog notes.
- Commit
CHANGELOG_DRAFT.mdtoexperimental. - Dispatch
deploy_patch.ymlworkflow. - Verify patch files on Gitea raw endpoint.
Troubleshooting
zip: command not found in CI
Root cause: ubuntu-latest container doesn't have zip pre-installed. The install step must include zip:
- name: Install tools
run: apt-get update -qq && apt-get install -y -qq curl unzip zip
Godot export fails silently (|| true)
The || true on export commands means a failed Godot export still shows step as success. Check:
- Is
godot_4.7cached at/cache/? - Does the export preset name match exactly? E.g.
"Windows Desktop"must matchexport_presets.cfg. - Is
addons/godotsteam/libgodotsteam*present? Missing DLLs cause Godot to exit 1.
Runner container can't clone repo
Runner uses HTTP auth with god username and TEKTON_RELEASE_TOKEN as password. If token is revoked:
- Generate new token from Gitea → Settings → Applications.
- Update secret
TEKTON_RELEASE_TOKENin repo Settings → Actions → Secrets. - Restart runner:
docker compose -f /home/dev/gitea/docker-compose.yml restart runner.
Runner shows "permission denied" for Docker socket
User dev doesn't have Docker socket access. Commands that touch Docker must be run via sudo or by the root user on the VPS. The local agent can only:
- Restart runner via systemd:
systemctl --user restart docker-runner(if running as user service). - No Docker CLI commands from agent terminal.
Release has duplicate assets
Each CI run uploads assets as new entries. To clean:
- Get release ID from API.
- Delete old asset IDs keeping only latest (highest IDs) for each platform.
- Use jq or manual curl loop (see "Cleaning duplicate assets" above).
Tag force-push creates redundant CI runs
Each push to a tag triggers ci.yml. Force-pushing a tag to a new commit creates another run:
- Previous runs keep running (can't cancel via API).
- Wait for all to finish, then delete stale ones.
- The last run to publish sets the release state.
Best practice: Delete old release before force-pushing tag, or at minimum delete stale completed runs after.
Patch manifest not updating
generate_version_json.py --skip-changelog only bumps version and writes version.json. If the version didn't change (e.g. --skip-changelog with no [NEXT] entries), the script exits with code 0 but doesn't write anything. Verify assets/data/version.json has the new version after CI run.
gitea-pages (raw.klud.top) returns 404
gitea-pages container uses a Gitea token to read files. If the token is dead:
- Switch to Gitea native raw endpoint:
https://git.klud.top/danchie/tekton/raw/branch/patches/... - Update
MANIFEST_URLingenerate_version_json.pyandVERSION_MANIFEST_URLingame_update_manager.gd. - Retire gitea-pages container entirely (not needed, Gitea has built-in raw serving).
File Reference
| File | Purpose |
|---|---|
.gitea/workflows/deploy_patch.yml |
Patch deploy CI — generates pck + pushes to patches branch |
.gitea/workflows/ci.yml |
Full binary release CI — exports 3 platforms + creates release |
tools/generate_version_json.py |
Version bumping + changelog → version.json conversion |
CHANGELOG_DRAFT.md |
Human-readable changelog draft (source of truth for release notes) |
assets/data/version.json |
Machine-readable manifest served to players (auto-generated) |
scripts/managers/game_update_manager.gd |
Client-side update checker (reads version.json → downloads patch.pck) |
project.godot |
Godot project file (config/version = source of truth for version number) |
export_presets.cfg |
Export configuration for all platforms |
/home/dev/gitea/docker-compose.yml |
Runner container composition (cache volume mount: /home/dev/godot-cache:/cache) |
Key Gotchas
zipmust be in install step — missing zip kills Windows/Linux export. Added in run 141 — do not remove.- Tag format is
vX.Y.Z—ci.ymltrigger isv*. A tag withoutvprefix won't build. - Force-push tag = new CI run — Always expect a new run on force-push. Old run keeps running.
- Changelog extracted from tag version —
## [X.Y.Z]section inCHANGELOG_DRAFT.md. If section doesn't exist, release body is empty. - Patch deploy skips changelog clearing —
--skip-changelogmeansversion.jsonis written butCHANGELOG_DRAFT.mdis NOT modified. Only the fullci.ymlpipeline clears it. - Cache is per-runner-host, not per-run — Godot binary (140MB) and templates (1.3GB) download once on fresh runner container, then persist via
/cachevolume. Runningdocker compose down+upreuses cache if volume isn't deleted. || truemasks Godot export errors — If export fails silently, check the2>&1 | tail -5output in CI logs. Error messages like "Cannot call method 'queue_free' on a null value" from GodotSteam are non-fatal (cosmetic plugin warnings).