name: Deploy on: push: branches: [main] jobs: deploy: runs-on: bare env: # The bare runner's own systemd service intentionally has a minimal # PATH/HOME (no rustup default toolchain in reach) - point it at the # shared toolchain install directly rather than assuming an ambient # dev shell environment. CARGO_HOME: /var/local/cargo RUSTUP_HOME: /var/local/rustup PATH: /var/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/bin # Shared with interactive dev builds (see ~/.config/fish/config.fish) # so a crate compiled once, by either a manual build or CI, is # cached for the other - real cache hits, not just a warm toolchain. # SCCACHE_SERVER_PORT deliberately differs from the interactive # dev shell's (4227): sccache's server is discovered by a fixed # TCP port shared by every local user, so if both contexts used # the same port, whichever one's server happened to be running # would silently "win" and serve build requests it doesn't have # filesystem permission for. Separate ports keep each context's # own server answering its own requests; the cache directory # (not the server process) is what's actually shared. SCCACHE_DIR: /var/local/sccache SCCACHE_SERVER_PORT: "4228" CARGO_TARGET_DIR: /var/local/cargo-target # cargo-leptos's own downloaded tools (wasm-bindgen, wasm-opt) cache # under $XDG_CACHE_HOME - left at its default that resolves inside # this unit's systemd StateDirectory, files it creates there end up # owned by the kernel's overflow "nobody" uid instead of the # runner's own dynamic uid (a StateDirectory quirk, not something # in our control), so a later run can't execute what an earlier run # downloaded. Redirecting it to our own known-good shared dir avoids # that entirely. XDG_CACHE_HOME: /var/local/leptos-cache steps: - 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. - name: Build run: SITE_NAME=${{ vars.PORTAL_SITE_NAME }} cargo leptos build --release # cargo-leptos only builds the leptos bin-target ("portal") - the # question-lint utility binary needs its own plain cargo build. - name: Build question-lint run: cargo build --release --bin question_lint --features ssr - name: Ship release 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" # 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 # 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