diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 62989692..5f537da7 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -6,6 +6,10 @@ on: branches: - main # Trigger on pushes to the main branch +concurrency: + group: wiki-build-${{ github.ref }} + cancel-in-progress: false + jobs: build-and-push: runs-on: ubuntu-latest @@ -52,33 +56,21 @@ jobs: password: ${{ secrets.AUTH_TOKEN }} - name: Build and push Docker images + id: image uses: docker/build-push-action@v6.9.0 with: push: true context: . - dockerfile: Dockerfile - tags: gitea.rwcloud.ch/homelab/wiki:latest - - # TODO: Deploy muss neu geschrieben werden - # deploy: - # needs: build-and-push - # runs-on: ubuntu-latest - # steps: - # - name: executing remote ssh commands using ssh key - # uses: appleboy/ssh-action@v1.2.0 - # with: - # host: ${{ secrets.SSH_HOST }} - # username: ${{ secrets.SSH_USERNAME }} - # key: ${{ secrets.SSH_KEY }} - # port: ${{ secrets.SSH_PORT }} - # script: | - # cd ~/ansible - # git pull - # source ./.venv/bin/activate - # pip install -r requirements.txt - # ansible-playbook -i inventory/hosts.yml --vault-password-file ./.vault_pass playbooks/proxmox/deploy_docker_services.yml --tags oauth2-wiki,wiki-uninstall - # ansible-playbook -i inventory/hosts.yml --vault-password-file ./.vault_pass playbooks/proxmox/deploy_docker_services.yml --tags oauth2-wiki,wiki-install - # deactivate - - + file: Dockerfile + labels: | + org.opencontainers.image.revision=${{ github.sha }} + tags: | + gitea.rwcloud.ch/homelab/wiki:sha-${{ github.sha }} + gitea.rwcloud.ch/homelab/wiki:latest + - name: Promote Wiki image to Portainer GitOps + if: github.ref_name == 'main' + env: + GITEA_DEPLOY_TOKEN: ${{ secrets.AUTH_TOKEN }} + WIKI_IMAGE: gitea.rwcloud.ch/homelab/wiki:sha-${{ github.sha }}@${{ steps.image.outputs.digest }} + run: python3 scripts/promote_wiki.py diff --git a/Dockerfile b/Dockerfile index e50c78ed..c4006c03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM caddy:alpine +FROM caddy:2.11.6-alpine COPY ./site /usr/share/caddy COPY Caddyfile /etc/caddy/Caddyfile diff --git a/docs/devops/tools/gitea/wiki-deployment.md b/docs/devops/tools/gitea/wiki-deployment.md new file mode 100644 index 00000000..502577a4 --- /dev/null +++ b/docs/devops/tools/gitea/wiki-deployment.md @@ -0,0 +1,31 @@ +# Wiki-Build und Deployment über Portainer + +Das Wiki läuft als Stack `wiki` auf `rwcloudsrv112` (100.72.156.2), erreichbar +unter https://wiki.rwcloud.ch. Caddy und Platzierung werden in +`homelab/portainer-hosted` verwaltet. Der alte SSH-/Ansible-Deploy entfällt. + +Ein Push auf `main` startet `.gitea/workflows/ci.yml`: Abhängigkeiten installieren, +Multirepo-Dokumentation per vorhandenem `GIT_SSH_KEY` laden, `mkdocs build`, +Container bauen und in `gitea.rwcloud.ch/homelab/wiki` publizieren. Der Build +veröffentlicht `sha-` und `latest`, mit Quell-SHA als OCI-Label. +Erst nach erfolgreichem Push aktualisiert `scripts/promote_wiki.py` ausschließlich +`WIKI_IMAGE` der Wiki-Zeile in `hosts/deployments.yaml` auf `main` des +Deployment-Repos. Die Referenz enthält Tag und festen Registry-Digest. +Dieser Commit startet dort `deploy`; Portainer aktualisiert den Wiki-Stack. + +Der vorhandene Secret `AUTH_TOKEN` benötigt read/write packages und Schreibrecht +auf `homelab/portainer-hosted`. Registry-Login bleibt beim bisherigen Benutzer +`build.user`. Portainer-Zugangsdaten bleiben ausschließlich im Deployment-Repo. +Überholte Builds werden anhand der aktuellen Main-SHA vor Promotion übersprungen. +Die Dateiversion schützt vor konkurrierenden Matrixänderungen. + +Kontrolle: zuerst Wiki-Build-Run, dann automatisch erzeugten Matrix-Commit und +Deployment-Run prüfen; anschließend https://wiki.rwcloud.ch öffnen und eine +Unterseite/Suche testen. Ein grüner Build allein beweist noch kein Deployment. +Bei Buildfehler bleibt das bisher festgelegte Image. Bei fehlendem Schreibrecht +schlägt die Promotion sichtbar fehl. Kein Zurückschreiben ins Wiki-Quellrepo. + +Rollback: früheren Wiki-Digest aus der Git-Historie in der Matrix eintragen und +committen. Das Wiki ist statisch; es hat keine Geschäftsdatenbank oder Datenvolumes. +Caddy-Konfiguration und Zertifikatsverteilung sind im Deployment-Runbook +`docs/runbooks/rwcloudsrv112.md` beschrieben. diff --git a/mkdocs.yml b/mkdocs.yml index 31239012..37fef8d4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,6 +62,7 @@ nav: - Gitea: - Gitea Actions: devops/tools/gitea/gitea_actions.md - Nützliches: devops/tools/gitea/gitea_basics.md + - Wiki Deployment: devops/tools/gitea/wiki-deployment.md - Backup und Restore: devops/tools/gitea/backup_restore.md - GitHub: - Github Codespace: devops/tools/github/codespace.md diff --git a/scripts/promote_wiki.py b/scripts/promote_wiki.py new file mode 100644 index 00000000..4ac509ed --- /dev/null +++ b/scripts/promote_wiki.py @@ -0,0 +1,67 @@ +"""Promote a successful Wiki image build to the Portainer placement matrix.""" +import base64 +import json +import os +import re +import urllib.error +import urllib.request + +API = "https://gitea.rwcloud.ch/api/v1" +TARGET = "/repos/homelab/portainer-hosted/contents/hosts/deployments.yaml" + + +def update_matrix(content, image): + if not re.fullmatch(r"gitea\.rwcloud\.ch/homelab/wiki:sha-[0-9a-f]{40}@sha256:[0-9a-f]{64}", image): + raise ValueError("Invalid immutable Wiki image reference") + section = re.search(r"(?m)^wiki:\n(?:[ \t].*\n|\n)*", content) + if section is None: + raise ValueError("Wiki stack missing from placement matrix") + updated, count = re.subn(r"(?m)^( WIKI_IMAGE: ).+$", lambda m: m[1] + image, section.group()) + if count != 1: + raise ValueError("Expected exactly one WIKI_IMAGE") + return content[:section.start()] + updated + content[section.end():] + + +def request(method, path, token, payload=None): + req = urllib.request.Request( + API + path, method=method, + data=json.dumps(payload).encode() if payload is not None else None, + headers={"Authorization": "token " + token, "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=60) as response: + return json.load(response) + + +def main(): + token = os.environ["GITEA_DEPLOY_TOKEN"] + source = os.environ["GITHUB_SHA"] + image = os.environ["WIKI_IMAGE"] + for attempt in range(3): + # A slower, older build must never replace a newer main revision. + latest = request("GET", "/repos/rewa81/mkdocs-wiki/branches/main", token) + if latest["commit"]["id"] != source: + print("Newer main revision exists; skipping obsolete build promotion.") + return + current = request("GET", TARGET + "?ref=main", token) + content = base64.b64decode(current["content"]).decode() + updated = update_matrix(content, image) + if updated == content: + print("Wiki image already selected; no deployment commit needed.") + return + try: + result = request("PUT", TARGET, token, { + "branch": "main", "sha": current["sha"], + "content": base64.b64encode(updated.encode()).decode(), + "message": f"deploy(wiki): mkdocs-wiki main {source}", + }) + except urllib.error.HTTPError as exc: + if exc.code == 409 and attempt < 2: + continue + raise + print("Wiki deployment requested by commit " + result["commit"]["sha"]) + return + raise RuntimeError("Placement matrix changed concurrently; promotion failed") + + +if __name__ == "__main__": + main()