diff --git a/.gitea/scripts/deploy.py b/.gitea/scripts/deploy.py deleted file mode 100644 index f8eeebd..0000000 --- a/.gitea/scripts/deploy.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Deploy a build with Komodo. Run by ci.yml after the images are pushed. - - deploy.py - -A vX.Y.Z tag deploys X.Y.Z to prod: the version is committed to t5de/t5de-infra (stacks//prod.env), then -Komodo deploys -prod from it. Pushes to main deploy -staging, which runs the :main image. - -Env: KOMODO_API_KEY and KOMODO_API_SECRET (a Komodo service user that can only deploy), INFRA_TOKEN (writes -t5de/t5de-infra; releases only), KOMODO_URL (default http://10.10.10.239:9120). -""" - -import base64 -import json -import os -import re -import sys -import time -import urllib.error -import urllib.request - -KOMODO_URL = os.environ.get("KOMODO_URL") or "http://10.10.10.239:9120" -GITEA_URL = os.environ.get("GITHUB_SERVER_URL", "https://git.t5de.dev").rstrip("/") -INFRA_REPO = "t5de/t5de-infra" -RELEASE_TAG = re.compile(r"v(\d+\.\d+\.\d+)") - - -def call(method: str, url: str, body: dict | None = None, headers: dict | None = None) -> dict: - data = json.dumps(body).encode() if body is not None else None - req = urllib.request.Request(url, data=data, method=method, headers={"Content-Type": "application/json", **(headers or {})}) - try: - with urllib.request.urlopen(req, timeout=60) as resp: - return json.loads(resp.read() or b"{}") - except urllib.error.HTTPError as e: - sys.exit(f"{method} {url}: {e.code} {e.read().decode(errors='replace')}") - - -def komodo(kind: str, type_: str, params: dict) -> dict: - keys = {"X-Api-Key": os.environ["KOMODO_API_KEY"], "X-Api-Secret": os.environ["KOMODO_API_SECRET"]} - return call("POST", f"{KOMODO_URL}/{kind}", {"type": type_, "params": params}, keys) - - -def pin_version(infra_dir: str, variable: str, version: str) -> None: - """Set = in the infra repo's prod.env, committing only if it changed.""" - path = f"stacks/{infra_dir}/prod.env" - url = f"{GITEA_URL}/api/v1/repos/{INFRA_REPO}/contents/{path}" - auth = {"Authorization": f"token {os.environ['INFRA_TOKEN']}"} - current = call("GET", f"{url}?ref=main", headers=auth) - text = base64.b64decode(current["content"]).decode() - updated, n = re.subn(rf"(?m)^{variable}=.*$", f"{variable}={version}", text) - if n != 1: - sys.exit(f"{path}: expected one {variable}= line, found {n}") - if updated == text: - print(f"{path} already has {variable}={version}") - return - release_url = f"{GITEA_URL}/{os.environ.get('GITHUB_REPOSITORY', '')}/releases/tag/v{version}" - call("PUT", url, { - "branch": "main", - "sha": current["sha"], - "content": base64.b64encode(updated.encode()).decode(), - "message": f"{infra_dir} {version} to prod\n\nFrom {release_url}", - }, auth) - print(f"Committed {variable}={version} to {INFRA_REPO}/{path}") - - -def deploy(stack: str) -> None: - update = komodo("execute", "DeployStack", {"stack": stack}) - update_id = update["_id"]["$oid"] - print(f"Deploying {stack} (Komodo update {update_id})") - deadline = time.time() + 15 * 60 - while time.time() < deadline: - update = komodo("read", "GetUpdate", {"id": update_id}) - if update.get("status") == "Complete": - for log in update.get("logs", []): - print(f"--- {log.get('stage')}: {'ok' if log.get('success') else 'FAILED'}") - if not log.get("success"): - print(log.get("stdout", ""), log.get("stderr", ""), sep="\n") - if not update.get("success"): - sys.exit(f"{stack}: deploy failed") - print(f"{stack}: deployed") - return - time.sleep(5) - sys.exit(f"{stack}: deploy didn't finish in 15 minutes") - - -def main() -> None: - stack, infra_dir, variable = sys.argv[1:4] - ref_name = os.environ.get("GITHUB_REF_NAME", "") - release = os.environ.get("GITHUB_REF_TYPE") == "tag" and RELEASE_TAG.fullmatch(ref_name) - if release: - pin_version(infra_dir, variable, release.group(1)) - deploy(f"{stack}-prod") - else: - deploy(f"{stack}-staging") - - -if __name__ == "__main__": - main() diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 3126720..a25d450 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,11 +1,9 @@ -# Type-checks, tests (the API against PGlite, an in-process Postgres) and builds the hub, then deploys it: main to -# staging, vX.Y.Z tags to prod. +# Type-checks, tests (the API against PGlite, an in-process Postgres) and builds the hub. name: CI on: push: branches: [main] - tags: ["v*"] pull_request: workflow_dispatch: @@ -34,46 +32,3 @@ jobs: - name: Build run: bun run build - - # Images for Komodo, then the deploy: main → :main and :sha-, deployed to staging; vX.Y.Z → :X.Y.Z and - # :latest, pinned in t5de/t5de-infra and deployed to prod (.gitea/scripts/deploy.py). - deploy: - needs: test - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Image tags - id: tags - run: | - if [ "$GITHUB_REF_TYPE" = tag ]; then - version=$(echo "$GITHUB_REF_NAME" | sed -nE 's/^v([0-9]+\.[0-9]+\.[0-9]+)$/\1/p') - [ -n "$version" ] || { echo "Not a vX.Y.Z tag: $GITHUB_REF_NAME"; exit 1; } - echo "tags=$version latest" >> "$GITHUB_OUTPUT" - else - echo "tags=main sha-${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT" - fi - - - name: Build and push - env: - TAGS: ${{ steps.tags.outputs.tags }} - REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN || secrets.GITHUB_TOKEN }} - run: | - echo "$REGISTRY_TOKEN" | docker login git.t5de.dev -u "$GITHUB_ACTOR" --password-stdin - for app in api web; do - image=git.t5de.dev/t5de/t5de-hub-$app - args=(-f apps/$app/Dockerfile - --label "org.opencontainers.image.source=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" - --label "org.opencontainers.image.revision=$GITHUB_SHA") - for tag in $TAGS; do args+=(-t "$image:$tag"); done - docker build "${args[@]}" . - for tag in $TAGS; do docker push "$image:$tag"; done - done - - - name: Deploy - env: - KOMODO_API_KEY: ${{ secrets.KOMODO_API_KEY }} - KOMODO_API_SECRET: ${{ secrets.KOMODO_API_SECRET }} - INFRA_TOKEN: ${{ secrets.INFRA_TOKEN }} - run: python3 .gitea/scripts/deploy.py hub t5de-hub HUB_VERSION