Files
tekton/wiki/patch_release_workflow.md
T
god 114748a54f experimental: remove Tekton Doors entirely
- 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
2026-07-06 00:18:59 +08:00

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.jsonpatches 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) via docker-compose
  • Cache volume: /home/dev/godot-cache/cache (rw) inside runner containers
  • Secret: TEKTON_RELEASE_TOKEN — Token from user adtpdn with 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


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

  1. Checkoutgit clone --depth 1 from experimental branch (shallow = fast).
  2. Setup Godot — Uses cached /cache/godot_4.7 binary. Downloads only if missing (140MB, cached forever).
  3. Generate version.json — Runs tools/generate_version_json.py --skip-changelog. Reads version from project.godot, bumps patch number, writes assets/data/version.json with the new release entry including pck_url pointing to Gitea raw endpoint.
  4. Export patch PCKgodot --headless --export-pack "Windows Desktop" build/patch.pck. No export templates needed — --export-pack only packs resources, not binaries. Output ~10-15MB.
  5. Push to patches branch — Force-pushes patch.pck + version.json to the patches branch 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

  1. Game boots → GameUpdateManager fetches version.json from Gitea raw endpoint.
  2. Compares latest_version against local version.
  3. If remote is newer → downloads patch.pck to user://patch.pck.
  4. Mounts with ProjectSettings.load_resource_pack("user://patch.pck").
  5. All files in patch.pck override base res:// files in memory.
  6. 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)

  1. Install toolsapt-get install curl unzip zip (zip was missing in early runs — make sure it's present).
  2. Checkout — Full clone from tag (shallow not used — needs full history for changelog extraction, though --depth 1 works too).
  3. Setup Godot with templates — Caches both Godot binary (140MB) and export templates (1.3GB) in /cache/. Templates downloaded once per runner lifetime.
  4. Export 3 platforms:
    • Windowsgodot --headless --export-release "Windows Desktop" → zipped with zip.
    • Linux/X11 — Same pattern.
    • macOS — Export to .zip directly (Godot's macOS export produces a zip).
    • Note: Steam DLLs copied into Windows build from addons/godotsteam/.
    • Note: || true on export commands masks Godot errors (e.g. GodotSteam plugin warnings). Real failures (missing zip) will surface.
  5. Extract changelog — Parses CHANGELOG_DRAFT.md for the ## [version] section matching the tag. Writes to $CHANGELOG_BODY env var.
  6. 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.
  7. Upload assets — Each .zip uploaded as release asset via multipart POST.
  8. Publish — Sets draft:false to 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:

  1. Read CHANGELOG_DRAFT.md — verify [NEXT] has entries.
  2. Check project.godot current version.
  3. Commit changelog to experimental.
  4. Create tag v2.4.3 → push to trigger ci.yml.
  5. Wait for CI completion (poll every 30s, up to 30 min).
  6. If CI fails:
    • Read job logs for failure reason.
    • Fix the workflow file, commit, force-push tag.
    • Clean up duplicate assets after re-run.
  7. Verify release page has 3 assets with correct sizes.
  8. If patch deploy also needed:
    • Trigger deploy_patch.yml dispatch.
    • Verify patch.pck is served and version.json updated.

Scenario: "Quick hot patch"

Agent actions:

  1. Check if [NEXT] has entries in CHANGELOG_DRAFT.md.
  2. If empty, ask user for changelog notes.
  3. Commit CHANGELOG_DRAFT.md to experimental.
  4. Dispatch deploy_patch.yml workflow.
  5. 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.7 cached at /cache/?
  • Does the export preset name match exactly? E.g. "Windows Desktop" must match export_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:

  1. Generate new token from Gitea → Settings → Applications.
  2. Update secret TEKTON_RELEASE_TOKEN in repo Settings → Actions → Secrets.
  3. 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_URL in generate_version_json.py and VERSION_MANIFEST_URL in game_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

  • zip must be in install step — missing zip kills Windows/Linux export. Added in run 141 — do not remove.
  • Tag format is vX.Y.Zci.yml trigger is v*. A tag without v prefix 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 in CHANGELOG_DRAFT.md. If section doesn't exist, release body is empty.
  • Patch deploy skips changelog clearing--skip-changelog means version.json is written but CHANGELOG_DRAFT.md is NOT modified. Only the full ci.yml pipeline 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 /cache volume. Running docker compose down + up reuses cache if volume isn't deleted.
  • || true masks Godot export errors — If export fails silently, check the 2>&1 | tail -5 output in CI logs. Error messages like "Cannot call method 'queue_free' on a null value" from GodotSteam are non-fatal (cosmetic plugin warnings).