Files
portal/.gitea/workflows/deploy.yml
T
Bendik Aagaard LynghaugandClaude Sonnet 5 1c1cbfc13b
Deploy / deploy (push) Successful in 1m5s
deploy.yml: build and publish question_lint alongside the app binary
questions' CI never actually ran this - the binary existed in-repo but
nothing built or exposed it. Publish it to the same stable release path
as the app binary so questions' lint job can exec it directly (same
bare-metal runner/host, no artifact download needed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 21:16:34 +02:00

134 lines
6.3 KiB
YAML

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 <<EOF
NATS_URL=${{ secrets.PORTAL_NATS_URL }}
KANIDM_URL=${{ vars.PORTAL_KANIDM_URL }}
OAUTH2_CLIENT_ID=${{ vars.PORTAL_OAUTH2_CLIENT_ID }}
OAUTH2_CLIENT_SECRET=${{ secrets.PORTAL_OAUTH2_CLIENT_SECRET }}
PUBLIC_URL=${{ vars.PORTAL_PUBLIC_URL }}
COOKIE_SECURE=true
CONTENT_REPO=https://project.uhhm.no/uhhm/questions
CONTENT_BRANCH=main
SITE_NAME=${{ vars.PORTAL_SITE_NAME }}
LEPTOS_SITE_ADDR=0.0.0.0:3010
# Cargo.toml's site-root ("target/site") is a build-time path;
# the deploy layout copies the bundle to releases/<sha>/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