diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..94ebf47 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -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 < /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 diff --git a/.gitea/workflows/lint-and-reload.yml b/.gitea/workflows/lint-and-reload.yml index 07f03fc..00f2d21 100644 --- a/.gitea/workflows/lint-and-reload.yml +++ b/.gitea/workflows/lint-and-reload.yml @@ -15,11 +15,11 @@ jobs: steps: - uses: actions/checkout@v4 - # Same bare-metal runner/host as portal's own deploy job, which - # publishes this binary to a stable path on every deploy - runs - # portal's real transition-table/shape validation directly, not a - # hand-maintained yq/jq subset of the same rules (lint.sh, now - # superseded and removed). + # Same bare-metal runner/host as this repo's own Deploy workflow, + # which ships question_lint alongside the portal binary in every + # pinned release - so content is validated by the exact portal + # version this instance runs, not a hand-maintained yq/jq subset + # of the same rules. - name: Lint questions run: /srv/app/uhhm-portal/current/question_lint --path questions diff --git a/README.md b/README.md index 6315094..8334465 100644 --- a/README.md +++ b/README.md @@ -161,14 +161,26 @@ Question id (doubles as the URL path), name, description, accept (file), resource + id_field (select options), bind {field, param, resource} (load this field's value from a resource whenever the named sibling - changes - {param} templates into a url source's path) - ResourceSpec source (kv | gitea_starred | gitea_org_repos | url), + changes - {param} templates into a url source's path), + 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}], jq (reshape filter) ``` 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 review resource's transitions — fireable by whoever holds one specific