Compare commits
3
Commits
0def91f8ab
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8858336ecc | ||
|
|
2b593ba53f | ||
|
|
e0094f2e97 |
@@ -1,187 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
# ── 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
|
|
||||||
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 <<EOF
|
|
||||||
NATS_URL=${{ secrets.PORTAL_NATS_URL }}
|
|
||||||
KANIDM_URL=${{ vars.PORTAL_KANIDM_URL }}
|
|
||||||
OAUTH2_CLIENT_ID=${{ vars.REDOAL_OAUTH2_CLIENT_ID }}
|
|
||||||
OAUTH2_CLIENT_SECRET=${{ secrets.REDOAL_OAUTH2_CLIENT_SECRET }}
|
|
||||||
PUBLIC_URL=https://redoal.com
|
|
||||||
COOKIE_SECURE=true
|
|
||||||
CONTENT_REPO=https://project.uhhm.no/redoal/questions
|
|
||||||
CONTENT_BRANCH=main
|
|
||||||
LEPTOS_SITE_ADDR=0.0.0.0:3020
|
|
||||||
LEPTOS_SITE_ROOT=site
|
|
||||||
# Same read-only token as uhhm's instance - here it also
|
|
||||||
# backs the gitea_releases source for the private
|
|
||||||
# redoal/redoal repo, so the owning user needs read access
|
|
||||||
# there.
|
|
||||||
GITEA_API_TOKEN=${{ secrets.PORTAL_GITEA_API_TOKEN }}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Restart redoal service
|
|
||||||
run: systemctl restart app@redoal-portal.service
|
|
||||||
|
|
||||||
- name: Update redoal Caddy routing
|
|
||||||
run: |
|
|
||||||
cat > /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
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
name: Publish release
|
||||||
|
|
||||||
|
# Portal does not deploy itself. Cutting a version is deliberate:
|
||||||
|
# `cargo release <level>` bumps Cargo.toml, commits, tags v<semver>,
|
||||||
|
# and pushes; this workflow reacts to the tag and publishes the
|
||||||
|
# artifact as a Gitea release. Each content repo (uhhm/questions,
|
||||||
|
# redoal/questions) pins PORTAL_RELEASE to one of these tags in its
|
||||||
|
# own deploy workflow - bumping the pin there is what rolls a version
|
||||||
|
# out to a site. Plain main pushes only run test.yml.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
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. 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
|
||||||
|
|
||||||
|
# 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: Package
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
tag="${{ github.ref_name }}"
|
||||||
|
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 "$stage/site"
|
||||||
|
tar -C "$stage" -czf "portal-$tag.tar.gz" portal question_lint site
|
||||||
|
rm -rf "$stage"
|
||||||
|
|
||||||
|
# The run's own ephemeral token has write access to this repo -
|
||||||
|
# no long-lived PAT to manage. The tag already exists (cargo
|
||||||
|
# release pushed it), so the release attaches to it; a re-run
|
||||||
|
# finds the existing release instead of failing.
|
||||||
|
- name: Publish release
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
tag="${{ github.ref_name }}"
|
||||||
|
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","name":"%s"}' \
|
||||||
|
"$tag" "$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"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
# Publishing only happens on v* tags (publish.yml), so this is what
|
||||||
|
# keeps plain main pushes honest between releases.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: bare
|
||||||
|
env:
|
||||||
|
# Same shared-toolchain/cache story as publish.yml.
|
||||||
|
CARGO_HOME: /var/local/cargo
|
||||||
|
RUSTUP_HOME: /var/local/rustup
|
||||||
|
PATH: /var/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/bin
|
||||||
|
SCCACHE_DIR: /var/local/sccache
|
||||||
|
SCCACHE_SERVER_PORT: "4228"
|
||||||
|
CARGO_TARGET_DIR: /var/local/cargo-target
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --features ssr
|
||||||
@@ -131,3 +131,12 @@ bin-default-features = false
|
|||||||
lib-features = ["hydrate"]
|
lib-features = ["hydrate"]
|
||||||
lib-default-features = false
|
lib-default-features = false
|
||||||
lib-profile-release = "wasm-release"
|
lib-profile-release = "wasm-release"
|
||||||
|
|
||||||
|
# cargo release <level> is how a portal version is cut: bump, commit,
|
||||||
|
# tag v{{version}}, push. CI (publish.yml) reacts to the tag and
|
||||||
|
# publishes the release artifact; nothing on crates.io.
|
||||||
|
[package.metadata.release]
|
||||||
|
publish = false
|
||||||
|
push = true
|
||||||
|
tag-name = "v{{version}}"
|
||||||
|
pre-release-commit-message = "Release {{version}}"
|
||||||
|
|||||||
Reference in New Issue
Block a user