diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml new file mode 100644 index 0000000..92e7b09 --- /dev/null +++ b/.gitea/workflows/tests.yml @@ -0,0 +1,69 @@ +# CluPilot CI — runs on Gitea Actions (GitHub-Actions syntax, own runner). +# +# Deliberately not mirrored to GitHub: this repository describes how our +# infrastructure is provisioned, and the same workflow runs at home. If we ever +# move, this file goes along unchanged. +name: tests + +on: + push: + branches: [main, 'feat/**'] + pull_request: + +jobs: + pest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: mbstring, pdo_sqlite, sodium, redis, bcmath, gd, zip + coverage: none + + - name: Install PHP dependencies + run: composer install --no-interaction --prefer-dist --no-progress + + - name: Prepare environment + run: | + cp .env.example .env + php artisan key:generate + + - name: Tests + # phpunit.xml pins its own sqlite/array drivers, so no services needed. + run: ./vendor/bin/pest --colors=always + + assets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install JS dependencies + run: npm ci --no-fund --no-audit + + # A build failure here is what used to surface as "the design is broken" + # only after deploying — catch it before it ships. + - name: Build assets + run: npm run build + + release: + # Only a green run produces the tag the console offers as an update. + needs: [pest, assets] + if: gitea.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Tag this commit as tested + run: | + tag="tested-$(date -u +%Y%m%d-%H%M)-$(git rev-parse --short HEAD)" + git tag "$tag" + git push origin "$tag" diff --git a/deploy/update.sh b/deploy/update.sh index cbe9ee0..fc4002f 100755 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -116,9 +116,11 @@ in_app npm run build # Before the restarts, not after: a service that starts while the old cache is # still on disk loads it and keeps those values for the life of its process. -log "Clearing caches" -in_app php artisan config:clear >/dev/null -in_app php artisan view:clear >/dev/null +log "Rebuilding caches" +# optimize:clear first, then optimize: a half-warm cache from before the update +# is what produces a page styled with assets that no longer exist. +in_app php artisan optimize:clear >/dev/null +in_app php artisan optimize >/dev/null log "Restarting services" docker compose up -d diff --git a/docker-compose.yml b/docker-compose.yml index 959dd0c..59c523b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -93,6 +93,24 @@ services: - redis - mariadb + # Gitea Actions runner. Opt-in via profile, because a development machine does + # not need to burn cycles on CI: docker compose --profile ci up -d runner + runner: + image: gitea/act_runner:latest + restart: unless-stopped + profiles: [ci] + environment: + GITEA_INSTANCE_URL: ${GITEA_INSTANCE_URL:-https://git.bave.dev} + GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_TOKEN:-} + GITEA_RUNNER_NAME: ${GITEA_RUNNER_NAME:-clupilot-local} + # ubuntu-latest maps to a real image rather than the host, so a workflow + # behaves the same here as anywhere else. + GITEA_RUNNER_LABELS: ubuntu-latest:docker://catthehacker/ubuntu:act-22.04 + volumes: + - runner-data:/data + # The runner starts job containers as siblings, which needs the socket. + - /var/run/docker.sock:/var/run/docker.sock + mariadb: image: mariadb:11.4 restart: unless-stopped @@ -138,5 +156,6 @@ services: volumes: db-data: + runner-data: redis-data: wireguard: diff --git a/lang/de/updating.php b/lang/de/updating.php new file mode 100644 index 0000000..e510c94 --- /dev/null +++ b/lang/de/updating.php @@ -0,0 +1,8 @@ + 'Aktualisierung läuft', + 'title' => 'Gleich wieder da.', + 'body' => 'Wir spielen gerade ein Update ein. Ihre Nextcloud läuft davon unberührt weiter — nur dieses Portal ist für einen Moment nicht erreichbar.', + 'auto' => 'Diese Seite lädt sich von selbst neu.', +]; diff --git a/lang/en/updating.php b/lang/en/updating.php new file mode 100644 index 0000000..11d2b6c --- /dev/null +++ b/lang/en/updating.php @@ -0,0 +1,8 @@ + 'Update in progress', + 'title' => 'Back in a moment.', + 'body' => 'We are applying an update. Your Nextcloud keeps running — only this portal is briefly unavailable.', + 'auto' => 'This page reloads itself.', +]; diff --git a/resources/js/app.js b/resources/js/app.js index bc27da2..e686b35 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -72,6 +72,27 @@ function applyLineGradients(cfg) { } } +// A page that was open across a deploy carries a Livewire snapshot and a CSRF +// token the new code no longer accepts. The user sees "419 Page Expired" and a +// dead interface; the session itself is still fine, so a reload fixes it. +// +// Hooked at the network layer rather than through Livewire's request hook: the +// hook's failure callback is not invoked for this case in every version, and a +// silent no-op here is exactly the bug we are trying to remove. +const originalFetch = window.fetch.bind(window); +window.fetch = async (...args) => { + const response = await originalFetch(...args); + + if (response.status === 419) { + const url = typeof args[0] === 'string' ? args[0] : args[0]?.url ?? ''; + if (url.includes('/livewire/')) { + window.location.reload(); + } + } + + return response; +}; + document.addEventListener('alpine:init', () => { // ── VPN config: copy / download ────────────────────────────────────── // navigator.clipboard only exists in a secure context, so over plain http diff --git a/resources/views/errors/503.blade.php b/resources/views/errors/503.blade.php new file mode 100644 index 0000000..02b74b7 --- /dev/null +++ b/resources/views/errors/503.blade.php @@ -0,0 +1,30 @@ + + + + + + + {{-- Comes back on its own: an update takes a minute, and nobody should have + to guess whether to keep hitting reload. --}} + + {{ config('app.name') }} + + @vite(['resources/css/app.css']) + + +
+
+ +
+ +
+ + {{ __('updating.badge') }} +
+ +

{{ __('updating.title') }}

+

{{ __('updating.body') }}

+

{{ __('updating.auto') }}

+
+ + diff --git a/vite.config.js b/vite.config.js index cffd88e..bedcb33 100644 --- a/vite.config.js +++ b/vite.config.js @@ -18,6 +18,12 @@ export default defineConfig({ refresh: true, }), ], + build: { + // Not emptied on rebuild: a browser holding the previous page would + // otherwise 404 on its stylesheet the moment we deploy, which is exactly + // the "design is completely broken" symptom. Stale files are cheap. + emptyOutDir: false, + }, server: { host: '0.0.0.0', port: 5173,