# 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`) 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 - **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]`: ```markdown ## [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`** ```bash 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: ```bash 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. **Checkout** — `git 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 PCK** — `godot --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 ```bash # 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** ```bash # 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 tools** — `apt-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:** - **Windows** — `godot --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: ```bash # 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= 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: ```bash # 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`: ```yaml - 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.Z`** — `ci.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).