Skip to content

Operations and releases

Production operation has two separate jobs: deploy released images safely and publish new releases correctly.

The supported production update path is the rolling updater, not ad-hoc docker compose pull across mutable tags.

Preview the next coherent API release:

Terminal window
python scripts/update_rolling.py --env-file .env --dry-run

Apply it:

Terminal window
python scripts/update_rolling.py --env-file .env

The updater discovers the newest coherent CalVer release through the configured stable repositories, verifies that all selected services report the same product release, pulls their exact release tags, and atomically writes immutable image digests to .env only after every selected image resolves successfully.

DLP image variables are untouched unless explicitly included:

Terminal window
python scripts/update_rolling.py --env-file .env --dlp --dry-run
python scripts/update_rolling.py --env-file .env --dlp

Then apply/recreate the stack as appropriate:

Terminal window
docker compose --env-file .env config --quiet
docker compose --env-file .env up --detach
docker compose --env-file .env ps

Gateway health:

Terminal window
curl --fail-with-body --silent --show-error http://127.0.0.1:8080/health

Target logs rather than dumping the entire rendered configuration:

Terminal window
docker compose --env-file .env logs --tail=100 server
docker compose --env-file .env logs --tail=100 threads twitter

Avoid logging credentials, cookie contents, authorization headers, DLP request bodies, or full rendered Compose environment.

Do not use:

Terminal window
docker restart gluetun

A raw restart can lose/reuse runtime state in ways that do not reapply the intended Compose environment and has caused VPN authentication problems. Recreate through Compose instead:

Terminal window
docker compose --env-file .env up --detach --force-recreate gluetun

The normal scraper/gateway services share its network namespace, so plan a short interruption while it is recreated.

VERSION in the parent API repository is the product release source of truth and uses:

YY.MM.ITERATION

Examples within one month increment the final integer; the first release in a new month resets the iteration to 1. Python packages use the PEP 440 equivalent without zero-padded numeric segments (26.08.726.8.7).

Run from a clean parent checkout with authenticated git, uv, and gh:

Terminal window
python scripts/version.py bump --publish -n "Describe the release here"

For multiline notes:

Terminal window
python scripts/version.py bump --publish -F RELEASE_NOTES.md

For an explicit version:

Terminal window
python scripts/version.py set YY.MM.N --publish -n "Release notes"

The current release tool performs the full version synchronization:

  1. Requires clean parent/submodule worktrees.
  2. Synchronizes and initializes submodules.
  3. Fetches each submodule’s remote default branch and safely advances stale parent pins; divergent history aborts.
  4. Updates parent VERSION, Python submodule project.version, and matching uv.lock files.
  5. Commits version metadata in affected submodules.
  6. Pushes submodule HEADs so every parent pointer is remotely reachable.
  7. Commits/pushes the parent VERSION and updated submodule pointers.
  8. With --publish, creates the matching annotated tag and delegates to scripts/publish_release.py to push it and create the GitHub Release through gh.

Use --local-only only when you intentionally want the old metadata-only behavior without synchronization/commits/pushes:

Terminal window
python scripts/version.py bump --local-only

If the version commit is complete and the matching tag already exists locally on the current HEAD:

Terminal window
python scripts/publish_release.py -n "Describe the release here"

The publisher requires:

  • clean worktree;
  • submodules matching the parent pins;
  • scripts/version.py check --tag v<VERSION> to pass;
  • local tag to exist and resolve to current HEAD.

It pushes the branch/tag and creates or updates the GitHub Release.

--retag exists only to repair an unpushed local tag that was created before the final release commit. It refuses to move a tag already present on origin.

Never force-move a published release tag as routine release practice. If an already-published release needs a correction, prefer a new patch iteration unless you have explicitly established that the remote tag produced no release artifacts and are intentionally repairing it.

Official images use:

ghcr.io/pinchana/pinchana-api/<service>:<tag>
  • Push to main publishes the development snapshot channel latest.
  • Exact release tag vYY.MM.N publishes immutable YY.MM.N, rolling monthly YY.MM, and stable.
  • Release CI validates that the Git tag matches VERSION.
  • The legacy stable Git branch is not a release source.

Production should discover through stable only via update_rolling.py, then persist immutable digests.

Do not combine an ordinary API update with an unplanned DLP enablement. DLP has its own preflight/canary sequence. Updating DLP image digests with --dlp is safe while DLP_ENABLED=false; advertising the capability is a separate operational decision.

See DLP operations and security.