From 2b593ba53f496658b111c3ee1d8542b785c77116 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Mon, 24 Aug 2026 21:21:06 +0200 Subject: [PATCH] Deploy becomes publish: portal ships as a versioned release artifact The portal repo no longer deploys instances. CI builds once, packages portal + question_lint + site/ into a tarball, and publishes it as a Gitea release tagged build- using the run's ephemeral token. Content repos (uhhm/questions, redoal/questions) now own their instance deploys - env, unit restart, Caddy route - pinned to a release tag. Co-Authored-By: Claude Fable 5 --- .gitea/workflows/deploy.yml | 169 +++++++++--------------------------- 1 file changed, 40 insertions(+), 129 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 4ed97d9..26d1978 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,11 +1,17 @@ -name: Deploy +name: Publish release + +# Portal no longer deploys itself. Each content repo (uhhm/questions, +# redoal/questions) owns its instance - domain, port, env, Caddy route - +# and its deploy workflow downloads a pinned release published here. +# Rolling a new portal version out to a site = bumping PORTAL_RELEASE +# in that site's .gitea/workflows/deploy.yml (an auditable commit). on: push: branches: [main] jobs: - deploy: + publish: runs-on: bare env: # The bare runner's own systemd service intentionally has a minimal @@ -42,8 +48,9 @@ jobs: - uses: actions/checkout@v4 # SITE_NAME is read via option_env! (src/app.rs) - compile-time, - # not a runtime env var - so it has to be set here, not just in - # the "Write service env" step below. + # not a runtime env var. It is only the last-resort fallback name: + # each instance's site.yaml (content-driven branding) overrides it, + # so one artifact serves every site. - name: Build run: SITE_NAME=${{ vars.PORTAL_SITE_NAME }} cargo leptos build --release @@ -52,136 +59,40 @@ jobs: - name: Build question-lint run: cargo build --release --bin question_lint --features ssr - - name: Ship release + - name: Package run: | set -euo pipefail - rel="/srv/app/uhhm-portal/releases/${{ github.sha }}" - mkdir -p "$rel" - cp "$CARGO_TARGET_DIR/release/portal" "$rel/uhhm-portal" - # questions' own CI execs this directly by path (same bare-metal - # runner/host as this job, no artifact download needed) instead - # of running its own hand-maintained yq/jq subset of these rules. - cp "$CARGO_TARGET_DIR/release/question_lint" "$rel/question_lint" + tag="build-$(echo ${{ github.sha }} | cut -c1-7)" + echo "TAG=$tag" >> "$GITHUB_ENV" + stage=$(mktemp -d) + cp "$CARGO_TARGET_DIR/release/portal" "$stage/portal" + cp "$CARGO_TARGET_DIR/release/question_lint" "$stage/question_lint" # site-root ("target/site" in Cargo.toml) is project-relative, # not affected by CARGO_TARGET_DIR - only the plain `cargo build` # outputs (release/, front/) move with that override. - cp -r target/site "$rel/site" - ln -sfn "$rel" /srv/app/uhhm-portal/current + cp -r target/site "$stage/site" + tar -C "$stage" -czf "portal-$tag.tar.gz" portal question_lint site + rm -rf "$stage" - # Sourced from this repo's own Settings -> Actions Variables/Secrets, - # not typed onto the host by hand - see the infrastructure repo's - # deploy-runner plan for the exact names/values to configure once. - - name: Write service env - run: | - cat > /etc/app/uhhm-portal.env </site - # (no target/ prefix) - override so the running binary looks - # in the right place for /pkg/*. - LEPTOS_SITE_ROOT=site - AUTOMATION_READ_TOKEN=${{ secrets.PORTAL_AUTOMATION_READ_TOKEN }} - # Read-only (read:user,read:repository,read:organization), - # used only by the GiteaStarred/GiteaOrgRepos resource sources - # (src/resource.rs) - the repo-contents/repo-info calls - # content loading already makes stay anonymous. - GITEA_API_TOKEN=${{ secrets.PORTAL_GITEA_API_TOKEN }} - EOF - - # No sudo: the runner's own unit sets NoNewPrivileges=yes, which - # blocks setuid escalation outright (sudo can't work at all under - # it, regardless of sudoers config) - systemctl talks to PID1 over - # D-Bus instead, authorized by a polkit rule scoped to deploy-runner - # + this unit pattern (see /etc/polkit-1/rules.d/10-deploy-runner.rules - # on the host). - - name: Restart service - run: systemctl restart app@uhhm-portal.service - - # No sudo here either - the runner is already in the `docker` group, - # so it can talk to the Docker socket directly. - # Apex and www are separate cookie scopes (no shared Domain - # attribute on the session cookie), but Kanidm's redirect_uri is - # fixed to PUBLIC_URL - a login started on the other host set its - # session cookie there, then landed on PUBLIC_URL's callback with - # an empty session ("no login in progress"). Redirecting www to - # the naked domain keeps every visit on one canonical host - # instead - PUBLIC_URL (repo variable) is set to https://{$DOMAIN} - # to match. - - name: Update Caddy routing - run: | - cat > /etc/caddy/services.d/uhhm-portal.caddy <<'EOF' - www.{$DOMAIN} { - redir https://{$DOMAIN}{uri} permanent - } - - {$DOMAIN} { - reverse_proxy host.docker.internal:3010 - log { - output file /var/log/caddy/www.log - } - } - EOF - docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile - - # ── redoal.com - second instance, same build ───────────────── - # One binary serves both faces: branding is content-driven - # (site.yaml in each CONTENT_REPO), so only the env differs. - # The compile-time SITE_NAME baked above is just this instance's - # fallback - redoal/questions' site.yaml overrides it. - - - name: Ship redoal release + # The run's own ephemeral token has write access to this repo - + # no long-lived PAT to manage. Re-running a build for the same sha + # finds the existing release instead of failing on the tag. + - name: Publish release run: | set -euo pipefail - rel="/srv/app/redoal-portal/releases/${{ github.sha }}" - mkdir -p "$rel" - cp "$CARGO_TARGET_DIR/release/portal" "$rel/redoal-portal" - cp -r target/site "$rel/site" - ln -sfn "$rel" /srv/app/redoal-portal/current - - - name: Write redoal service env - run: | - cat > /etc/app/redoal-portal.env < /etc/caddy/services.d/redoal-portal.caddy <<'EOF' - www.redoal.com { - redir https://redoal.com{uri} permanent - } - - redoal.com { - reverse_proxy host.docker.internal:3020 - log { - output file /var/log/caddy/redoal.log - } - } - EOF - docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile + api="${{ github.server_url }}/api/v1/repos/${{ github.repository }}" + auth="Authorization: token ${{ secrets.GITHUB_TOKEN }}" + subject=$(git log -1 --format=%s) + body=$(printf '{"tag_name":"%s","target_commitish":"%s","name":"%s"}' \ + "$TAG" "${{ github.sha }}" "$TAG: $(echo "$subject" | sed 's/"/\\"/g')") + id=$(curl -sf -X POST -H "$auth" -H 'Content-Type: application/json' \ + -d "$body" "$api/releases" | jq .id) \ + || id=$(curl -sf -H "$auth" "$api/releases/tags/$TAG" | jq .id) + # Replace the asset if a re-run already uploaded one. + for aid in $(curl -sf -H "$auth" "$api/releases/$id/assets" | jq '.[].id'); do + curl -sf -X DELETE -H "$auth" "$api/releases/$id/assets/$aid" + done + curl -sf -X POST -H "$auth" \ + -F "attachment=@portal-$TAG.tar.gz" \ + "$api/releases/$id/assets?name=portal-$TAG.tar.gz" > /dev/null + echo "published $TAG"