# Platform CI: push to `main` → production, via Jenkins Deploying the **platform itself** (the `paas` CLI + panel in this repo) used to be a manual rsync + `sudo ./bootstrap.sh` on the production VM. It is now a Jenkins job that lints every push to `main` and rolls it out through one narrow SSH entry point on the server. ``` git push origin main │ (LAN Jenkins polls every 2 min — it is not reachable from the │ internet, so there is no inbound GitHub webhook by design) ▼ Jenkins job `paas-platform` ── ci/Jenkinsfile.platform 1. checkout 2. lint (bash -n, shellcheck, node --check) 3. tar czf - . | ssh deploy@192.168.13.10 deploy "" ▼ production VM, deploy user, CI key with forced command ── infra/paas-ci-ssh └─ sudo -n /usr/local/sbin/paas-self-deploy deploy … ── infra/paas-self-deploy extract → lint again → copy checkout to *.prev → rsync new tree in → ./bootstrap.sh with the instance's own settings → verify ``` `bootstrap.sh` is idempotent and is re-run exactly as `paas-init` would run it: panel domain from `/srv/paas/panel/.env`, flavour + PHP versions from `/srv/paas/paas.conf` (a Laravel server stays Laravel, a Node server stays Node), ACME e-mail from the Caddyfile. Apps, `paas.db`, the panel `.env`, certificates and keys are never touched. The CI key cannot do anything except `deploy`, `verify`, `status`, `rollback` (see `infra/paas-ci-ssh`). ## One-time setup 1. **Jenkins → GitHub read access.** The LAN Jenkins already has the GitHub token credential `younex1` (used by the other jobs); the job clones `https://github.com/younex1/deploy-younex-web.git` with it. Nothing to do unless that credential is removed (`GIT_CRED_ID` below). 2. **Create credential + job** from the workstation (needs a Jenkins API token: *your user → Security → API token*): ```bash JENKINS_USER= JENKINS_TOKEN= ./ci/jenkins-setup.sh ``` This generates `~/.ssh/jenkins-platform`, stores the private half in Jenkins as credential `paas-platform-deploy`, and creates the job `paas-platform` from `ci/jenkins-platform-job.xml`. Re-running updates the job in place. (The first setup on 2026-09-16 was done as root on the Jenkins host with a one-shot `init.groovy.d` script instead; the keypair lives in `/root/jenkins-platform` there. Both paths produce the same credential id and job.) 3. **Authorize the key on the server** (the command is printed by step 2): ```bash ssh younes@192.168.13.10 'cd dotnews-paas && sudo ./infra/setup-ci-deploy.sh ""' ``` That installs `paas-self-deploy`, the forced-command wrapper and the sudoers line (all three are also installed by every `bootstrap.sh`, so they are part of the golden template — inert until a key is authorized). 4. Push to `main`, or *Build Now*. The first run also takes the server's host key (`StrictHostKeyChecking=accept-new`). ## Day to day | | | |---|---| | Deploy | `git push origin main` — live within ~3 minutes; build log in Jenkins, server log in `/var/log/paas-self-deploy.log` | | What is live | `ssh younes@192.168.13.10 'sudo paas-self-deploy status'` (commit, time, message; `.deployed` in the checkout) | | Health | `sudo paas-self-deploy verify` — caddy/varnish active, pm2 `paas-panel` online, panel `/login` 200, installed panel == checkout | | Rollback | `sudo paas-self-deploy rollback` — swaps `/home/younes/dotnews-paas` with `…prev` and re-runs bootstrap | | Manual deploy | `tar czf - -C . \| ssh younes@… 'sudo paas-self-deploy deploy "msg"'` (same path, no Jenkins) | Lint failures on the server abort before anything is changed. A failed bootstrap or verify fails the Jenkins build and leaves the new tree in place for inspection; `rollback` restores the previous one. ## Other instances Every server from the golden template has the same entry point. To let the same Jenkins deploy a second instance, add its host to a copy of the `Deploy` stage (or a second job with `PAAS_HOST` changed) and run `infra/setup-ci-deploy.sh` there with the same public key.