From 5b17b52e3f56d37e9633626131eeb32dbdcfc5db Mon Sep 17 00:00:00 2001 From: Labyricorn Deployment Date: Tue, 11 Aug 2026 20:44:14 -0700 Subject: [PATCH] Automate production deployment on main pushes --- .gitea/workflows/deploy.yml | 27 ++++++++++++++ README.md | 70 ++++++++++++++++++++++++++++++------- 2 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 .gitea/workflows/deploy.yml diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..8b23262 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,27 @@ +name: Deploy production + +on: + push: + branches: + - main + +concurrency: + group: labyricorn-production + cancel-in-progress: false + +jobs: + deploy: + runs-on: labyricorn-deploy + timeout-minutes: 10 + steps: + - name: Build and activate current main + run: sudo -n /usr/local/bin/deploy-labyricorn + + - name: Verify the deployed revision and origin + run: | + deployed_revision="$(cat /srv/labyricorn/current/.labyricorn-commit)" + test "$deployed_revision" = "$GITHUB_SHA" + curl --fail --silent --show-error \ + -H 'Host: www.labyricorn.com' \ + -H 'X-Forwarded-Proto: https' \ + http://127.0.0.1/ >/dev/null diff --git a/README.md b/README.md index 27e4387..e442408 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,9 @@ ignored and must not be committed. ## Routine content workflow -The preferred workflow is to edit in a normal Git checkout, review the changes, -push them to `main`, and then deploy on the production host. +Edit in a normal Git checkout, review the changes, and push them to `main`. +A successful push triggers the repository's Gitea Action, which builds and +atomically activates that exact revision on the production host. ```bash git switch main @@ -87,7 +88,8 @@ git commit -m "Describe the site change" git push origin main ``` -Then deploy on `ubuntu2-47`: +Watch the run under **Actions** in Gitea. Manual deployment remains available +for recovery or controlled operations: ```bash sudo deploy-labyricorn @@ -512,17 +514,61 @@ The first origin request should redirect; the simulated Cloudflare HTTPS request should return the site. A different result indicates a proxy/header or nginx configuration problem. -## Future automatic deployment +## Automatic deployment -Manual deployment is complete and remains the supported path. Automatic -deployment is not configured. +Pushes to `main` automatically deploy through Gitea Actions. The workflow is +versioned with the site at `.gitea/workflows/deploy.yml` and has one job: -The preferred future design is a Gitea Action triggered only by pushes to -`main`, invoking `deploy-labyricorn` through a dedicated, narrowly restricted -SSH identity. A webhook receiver is acceptable only if it authenticates a -secret, verifies the repository and branch, never executes payload text, and -runs with minimal privileges. Never expose a general shell endpoint or an -unauthenticated HTTP deployment URL. +1. Run on the repository-scoped runner labelled `labyricorn-deploy`. +2. Execute the existing root-owned `deploy-labyricorn` command. +3. Confirm `/srv/labyricorn/current/.labyricorn-commit` equals the pushed commit. +4. Request the site through nginx on the local origin and fail if it is unhealthy. + +The action intentionally does not check out the repository into its own +workspace. The deployment script fetches `origin/main` using the dedicated +`labyricorn-deploy` account, validates a clean checkout, builds a new release, +and atomically changes the `current` symlink. Concurrent action runs share the +`labyricorn-production` concurrency group, and the deployment script also uses +a filesystem lock. + +### Runner installation + +The production host runs official Gitea Runner 3.0.0 directly on Linux; Docker +is not installed or required for this workflow. + +| Item | Value | +|---|---| +| systemd unit | `gitea-runner.service` | +| service account | `gitea-runner` | +| runner name | `labyricorn-production` | +| registration scope | `Labyricorn/labyricorn-site` repository | +| execution label | `labyricorn-deploy:host` | +| runner binary | `/usr/local/bin/gitea-runner` | +| runner configuration | `/etc/gitea-runner/config.yaml` | +| registration state | `/var/lib/gitea-runner/.runner` | +| job workspace | `/var/lib/gitea-runner/work` | +| sudo policy | `/etc/sudoers.d/gitea-runner-labyricorn` | + +The runner account has no general administrative access. Its sudo policy allows +only this exact command without a password: + +```text +/usr/local/bin/deploy-labyricorn +``` + +Useful checks: + +```bash +systemctl status gitea-runner +journalctl -u gitea-runner -n 100 --no-pager +sudo -l -U gitea-runner +``` + +If the runner must be replaced, delete or disable it in **Repository Settings → +Actions → Runners**, generate a fresh repository registration token, and +register the replacement. Registration tokens and `.runner` contents are +credentials: never commit or print them. A push can still be deployed manually +with `sudo deploy-labyricorn` while the runner is unavailable. ## Change-control checklist