Amend: GITEA_ is a reserved Actions secret-name prefix
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s

This commit is contained in:
Bendik Aagaard Lynghaug
2026-08-24 21:26:26 +02:00
parent 9b7162f2ca
commit bb117c1a1b
3 changed files with 128 additions and 8 deletions
+108
View File
@@ -0,0 +1,108 @@
name: Deploy instance
# This content repo owns its portal instance: which portal version runs,
# the service env, the systemd unit, and the Caddy route. The portal repo
# only publishes versioned release artifacts (uhhm/portal's Publish
# workflow); PORTAL_RELEASE below pins the one this site runs.
#
# Rolling out a new portal version = bumping PORTAL_RELEASE (a commit,
# so every rollout is auditable and revertable). Content-only changes
# never come through here - lint-and-reload hot-swaps those into the
# running instance over NATS.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- .gitea/workflows/deploy.yml
env:
PORTAL_RELEASE: build-2b593ba
INSTANCE: uhhm-portal
jobs:
deploy:
runs-on: bare
steps:
# Instance config comes from THIS repo's Actions variables/secrets
# (Settings -> Actions) - not the portal repo's. Guard before
# touching anything on the host: a missing secret must fail the
# run, not silently write an empty value into the live env file.
- name: Check instance secrets are configured
run: |
set -eu
[ -n "${{ secrets.NATS_URL }}" ] || { echo "missing secret NATS_URL"; exit 1; }
[ -n "${{ secrets.OAUTH2_CLIENT_SECRET }}" ] || { echo "missing secret OAUTH2_CLIENT_SECRET"; exit 1; }
[ -n "${{ secrets.AUTOMATION_READ_TOKEN }}" ] || { echo "missing secret AUTOMATION_READ_TOKEN"; exit 1; }
[ -n "${{ secrets.PORTAL_GITEA_API_TOKEN }}" ] || { echo "missing secret PORTAL_GITEA_API_TOKEN"; exit 1; }
# uhhm/portal is public, so the asset download is anonymous. The
# app@ template's ExecStart is /srv/app/%i/current/%i - the shipped
# binary is named "portal", so link the instance name to it.
- name: Ship pinned portal release
run: |
set -euo pipefail
api="${{ github.server_url }}/api/v1/repos/uhhm/portal"
url=$(curl -sf "$api/releases/tags/$PORTAL_RELEASE" | jq -r '.assets[0].browser_download_url')
rel="/srv/app/$INSTANCE/releases/$PORTAL_RELEASE"
rm -rf "$rel"
mkdir -p "$rel"
curl -sfL "$url" | tar -xz -C "$rel"
ln -sfn portal "$rel/$INSTANCE"
- name: Write service env
run: |
cat > /etc/app/$INSTANCE.env <<EOF
NATS_URL=${{ secrets.NATS_URL }}
KANIDM_URL=${{ vars.KANIDM_URL }}
OAUTH2_CLIENT_ID=${{ vars.OAUTH2_CLIENT_ID }}
OAUTH2_CLIENT_SECRET=${{ secrets.OAUTH2_CLIENT_SECRET }}
PUBLIC_URL=${{ vars.PUBLIC_URL }}
COOKIE_SECURE=true
# This repo is its own instance's content source.
CONTENT_REPO=${{ github.server_url }}/${{ github.repository }}
CONTENT_BRANCH=main
SITE_NAME=${{ vars.SITE_NAME }}
LEPTOS_SITE_ADDR=0.0.0.0:3010
# The release tarball carries the site bundle at site/ (no
# target/ prefix), so override Cargo.toml's build-time path.
LEPTOS_SITE_ROOT=site
AUTOMATION_READ_TOKEN=${{ secrets.AUTOMATION_READ_TOKEN }}
# Read-only (read:user,read:repository,read:organization),
# used only by the Gitea resource sources (portal
# src/resource.rs) - content loading stays anonymous.
GITEA_API_TOKEN=${{ secrets.PORTAL_GITEA_API_TOKEN }}
EOF
# Activate only after the release and env are fully written, so a
# failed download or missing config never takes the site down.
#
# No sudo: the runner's unit sets NoNewPrivileges=yes - systemctl
# talks to PID1 over D-Bus, authorized by the polkit rule scoped
# to deploy-runner + the app@* unit pattern.
- name: Activate and restart
run: |
ln -sfn "/srv/app/$INSTANCE/releases/$PORTAL_RELEASE" /srv/app/$INSTANCE/current
systemctl restart app@$INSTANCE.service
# 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 - redirecting www to the naked domain keeps
# every visit on one canonical host. The runner is in the docker
# group, so no sudo here either.
- name: Update Caddy routing
run: |
cat > /etc/caddy/services.d/$INSTANCE.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
+5 -5
View File
@@ -15,11 +15,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Same bare-metal runner/host as portal's own deploy job, which # Same bare-metal runner/host as this repo's own Deploy workflow,
# publishes this binary to a stable path on every deploy - runs # which ships question_lint alongside the portal binary in every
# portal's real transition-table/shape validation directly, not a # pinned release - so content is validated by the exact portal
# hand-maintained yq/jq subset of the same rules (lint.sh, now # version this instance runs, not a hand-maintained yq/jq subset
# superseded and removed). # of the same rules.
- name: Lint questions - name: Lint questions
run: /srv/app/uhhm-portal/current/question_lint --path questions run: /srv/app/uhhm-portal/current/question_lint --path questions
+15 -3
View File
@@ -161,14 +161,26 @@ Question id (doubles as the URL path), name, description,
accept (file), resource + id_field (select options), accept (file), resource + id_field (select options),
bind {field, param, resource} (load this field's bind {field, param, resource} (load this field's
value from a resource whenever the named sibling value from a resource whenever the named sibling
changes - {param} templates into a url source's path) changes - {param} templates into a url source's path),
ResourceSpec source (kv | gitea_starred | gitea_org_repos | url), relay (gesture only - ws(s):// redoal-relay URL)
ResourceSpec source (kv | gitea_starred | gitea_org_repos |
gitea_releases | url),
key, public, requires_group, transitions[{from, to, label}], key, public, requires_group, transitions[{from, to, label}],
jq (reshape filter) jq (reshape filter)
``` ```
Requirement `type`: `text` (default), `textarea`, `email`, `tel`, Requirement `type`: `text` (default), `textarea`, `email`, `tel`,
`select`, `file`, `prosekit` (rich text), or any HTML input type. `select`, `file`, `prosekit` (rich text), `gesture` (draw a stroke on
a canvas; submits `{points, key}` - with `relay` set it announces the
stroke to a redoal-relay and shows echoes of similar strokes; mark it
`optional: true`, hidden inputs skip HTML required-validation), or any
HTML input type.
A repo may also carry an optional `site.yaml` at its root (sibling of
`aggregates.yaml`) declaring instance branding: `title`, `wordmark`
(image URL), and `hero {kind: yes | gesture | plain, relay}` for the
landing page. Absent file = the historical uhhm look (YES canvas,
/wordmark.svg).
`self_transition` is the anonymous, single-record counterpart to a `self_transition` is the anonymous, single-record counterpart to a
review resource's transitions — fireable by whoever holds one specific review resource's transitions — fireable by whoever holds one specific