| 1 | --- |
| 2 | name: release |
| 3 | description: How to cut a sigit.si release — bump the version, write the changelog entry, run the full pre-deploy check, promote development to main (no-ff), deploy, and verify. Use this whenever the user asks to release, ship, deploy, promote development/main, bump the version, add a changelog entry, or asks "is this safe to deploy?" — even if they only mention one of those pieces. |
| 4 | --- |
| 5 | |
| 6 | # Releasing sigit.si |
| 7 | |
| 8 | > Server mechanics (SSH, Puma, hook internals, Postgres admin) live in the |
| 9 | > [deployment skill](../deployment/SKILL.md). This skill is the release |
| 10 | > process around them: what a release *is* in this repo, the checks that gate |
| 11 | > it, and the order of operations. |
| 12 | |
| 13 | ## The shape of a release |
| 14 | |
| 15 | Features land on `development` through PRs. A release is: |
| 16 | |
| 17 | 1. A **release prep commit** on `development` — version bump + changelog entry. |
| 18 | 2. The **full pre-deploy check** (below) against the `development` tip. |
| 19 | 3. A **no-fast-forward merge** of `development` into `main`. |
| 20 | 4. **Deploy**: push `main` to the prod remote. |
| 21 | 5. **Post-deploy verification**. |
| 22 | |
| 23 | Two conventions to respect: |
| 24 | |
| 25 | - **Promotions to main are explicit merge commits**, named for the release: |
| 26 | `Merge development into main for the vX.Y.Z release`. This is convention, |
| 27 | not git config — don't fast-forward even though git would let you. |
| 28 | - **Merging to main and deploying are deliberate operator actions.** Prepare |
| 29 | everything, but get an explicit go-ahead before pushing `main` anywhere. |
| 30 | |
| 31 | ## 1. Release prep commit (on development) |
| 32 | |
| 33 | Two files change; nothing else needs wiring. |
| 34 | |
| 35 | **`lib/sigitsi/version.rb`** — bump `Sigitsi::VERSION` per semver (the file |
| 36 | comment spells it out: MAJOR incompatible, MINOR features, PATCH fixes). The |
| 37 | value surfaces in the site footer. |
| 38 | |
| 39 | **`config/changelog/vX.Y.Z.md`** — one new file per release. The `Changelog` |
| 40 | service (`app/services/changelog.rb`) auto-discovers files matching |
| 41 | `v<major>.<minor>.<patch>.md`, sorts numerically by version (so 1.10.0 > |
| 42 | 1.9.0 — file dates and mtimes don't matter), and renders them at `/changelog`. |
| 43 | Malformed files are silently skipped, so verify your entry actually appears |
| 44 | (e.g. `bin/rails runner 'puts Changelog.latest&.version'`). Format: |
| 45 | |
| 46 | ```markdown |
| 47 | --- |
| 48 | date: 2026-07-05 |
| 49 | title: Short release name |
| 50 | headline: One sentence shown on the changelog index. |
| 51 | --- |
| 52 | |
| 53 | Markdown body. Look at existing entries in config/changelog/ for the voice: |
| 54 | plain prose, feature sections with ## headings, concrete over promotional. |
| 55 | ``` |
| 56 | |
| 57 | Commit both together (`Release vX.Y.Z: <one-line summary>` works as a |
| 58 | message), push to `development` (via PR or directly, matching however the |
| 59 | user is currently working). |
| 60 | |
| 61 | ## 2. Full pre-deploy check |
| 62 | |
| 63 | Run all of these against the exact tip being released. They mirror CI plus |
| 64 | the production-only failure modes CI can't see. Don't skip the boring ones — |
| 65 | each earned its place by failing for real at least once. |
| 66 | |
| 67 | ```bash |
| 68 | bundle install # lockfile sane, native gems build |
| 69 | |
| 70 | # Schema drift: schema.rb must match the migration set exactly. Rebuild from |
| 71 | # migrations on an EMPTY db and diff. Two traps: (a) if schema.rb is present |
| 72 | # during db:prepare/migrate it can mark migrations "up" without running them |
| 73 | # (assume_migrated_upto), so move it aside first; (b) the dev database is |
| 74 | # shared across worktrees/branches — other branches' tables can leak into a |
| 75 | # dump. A from-scratch migrate on a dropped DB avoids both. |
| 76 | mv db/schema.rb /tmp/schema.committed.rb |
| 77 | RAILS_ENV=test bin/rails db:drop db:create db:migrate |
| 78 | diff /tmp/schema.committed.rb db/schema.rb # empty diff or explain why not |
| 79 | git checkout db/schema.rb |
| 80 | |
| 81 | # Full suite on a freshly loaded test DB (stale test DBs cause phantom |
| 82 | # uniqueness failures). Tailwind must be built or view specs fail on assets. |
| 83 | bundle exec rails tailwindcss:build |
| 84 | RAILS_ENV=test bin/rails db:drop db:create db:schema:load |
| 85 | bundle exec rspec # expect 0 failures |
| 86 | |
| 87 | # The four CI gates, locally: |
| 88 | bin/brakeman --no-pager |
| 89 | bin/bundler-audit check --update |
| 90 | bin/importmap audit |
| 91 | bin/rubocop |
| 92 | |
| 93 | # Production boot check. This is the one that catches site-down bugs: any |
| 94 | # initializer that raises in production (missing keys, required env) fails |
| 95 | # here, exactly as it would under the deploy hook. Supply dummies for values |
| 96 | # the initializers merely require to be present. |
| 97 | RAILS_ENV=production SECRET_KEY_BASE=dummy bin/rails assets:precompile |
| 98 | ``` |
| 99 | |
| 100 | ### Config/environment audit |
| 101 | |
| 102 | New code often needs new production config, and the deploy hook will not |
| 103 | tell you it's missing — Puma just fails to boot, or a feature silently 500s. |
| 104 | |
| 105 | ```bash |
| 106 | # What did this release add? |
| 107 | git diff origin/main -- .env.example # documented new vars |
| 108 | git diff origin/main --stat -- config/initializers/ # new boot-time requirements |
| 109 | |
| 110 | # Is prod ready for them? (names/presence only — never print secret values) |
| 111 | ssh smb1-deploy 'sudo -u git bash -lc "printenv | grep -oE \"^SOME_NEW_VAR[A-Z_]*\""' |
| 112 | ``` |
| 113 | |
| 114 | Notes that save time (each learned from a real incident): |
| 115 | |
| 116 | - The `git` user's `~/.profile` is the env source; the post-receive hook |
| 117 | starts with `. ~/.profile`, so vars set there do reach `db:prepare`, Puma, |
| 118 | and `bin/jobs`. |
| 119 | - **Rails credentials must be edited locally and committed, never on the |
| 120 | server.** `config/credentials.yml.enc` is git-tracked and the deploy does |
| 121 | `git checkout -f main` — a `credentials:edit` in the server work tree is |
| 122 | silently wiped by the next deploy (this deleted the GitHub App credentials |
| 123 | once). Edit with the local `config/master.key`, commit the `.enc`, and let |
| 124 | the deploy distribute it. |
| 125 | - The only trustworthy remote presence check is `printenv VAR` in the login |
| 126 | shell: `ssh ... 'sudo -u git bash -lc "printenv GITHUB_WEBHOOK_SECRET |
| 127 | >/dev/null && echo present || echo absent"'`. Compound `[ -n "$VAR" ]` |
| 128 | checks through the ssh/sudo/bash quoting layers have produced false |
| 129 | "set" readings, `printenv | grep -c X` matches `SUDO_COMMAND` (your own |
| 130 | command line), and `pgrep -f X` over ssh matches its own invocation. |
| 131 | |
| 132 | ### Production database state |
| 133 | |
| 134 | ```bash |
| 135 | # Pending migrations on prod (expected: the release's new ones, nothing odd) |
| 136 | ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && bin/rails db:migrate:status | tail"' |
| 137 | |
| 138 | # Solid Queue: the queue DB must hold solid_queue_* tables (db:prepare loads |
| 139 | # db/queue_schema.rb; verify rather than assume — see deployment skill |
| 140 | # "Known issues") |
| 141 | ssh smb1-deploy 'sudo -u postgres psql sigitsi_production_queue -c "\dt"' |
| 142 | ``` |
| 143 | |
| 144 | ## 3. Promote development to main |
| 145 | |
| 146 | Only after the user says go: |
| 147 | |
| 148 | ```bash |
| 149 | git checkout main && git pull origin main |
| 150 | git merge --no-ff development -m "Merge development into main for the vX.Y.Z release" |
| 151 | git push origin main |
| 152 | ``` |
| 153 | |
| 154 | ## 4. Deploy |
| 155 | |
| 156 | The operator deploys with the interactive `smb` CLI from the repo root on |
| 157 | `main` — it logs in, detects the Rails app, and pushes with the right key |
| 158 | (`~/.ssh/id_11@smbcloud`), ending in "Deployment complete": |
| 159 | |
| 160 | ```bash |
| 161 | smb |
| 162 | ``` |
| 163 | |
| 164 | A plain `git push smbcloud main` does the same thing but only if that key is |
| 165 | what ssh offers; the `Host api-1.smbcloud.xyz` entry in `~/.ssh/config` is |
| 166 | not configured, so without the smb CLI expect `Permission denied (publickey)`. |
| 167 | |
| 168 | Either way it lands on the bare-repo post-receive hook, which has sharp |
| 169 | edges (deployment skill has the full list): no `set -e`, migration output |
| 170 | goes to the pushing terminal — **read the push output**, it is the only |
| 171 | record of `db:prepare`'s success — and `bin/jobs start` stacks a new Solid |
| 172 | Queue supervisor on every deploy unless the hook has been amended with a |
| 173 | `pkill -f solid_queue || true` line first. |
| 174 | |
| 175 | ## 5. Post-deploy verification |
| 176 | |
| 177 | ```bash |
| 178 | # Schema arrived |
| 179 | ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && bin/rails db:migrate:status | tail -5"' |
| 180 | |
| 181 | # App serves (allow_browser rejects old/absent User-Agents with 403 — always |
| 182 | # curl with a modern UA or a healthy site looks broken) |
| 183 | curl -s -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0 (Chrome/130)" https://sigit.si/ |
| 184 | curl -s -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0 (Chrome/130)" https://sigit.si/changelog |
| 185 | |
| 186 | # New release visible |
| 187 | curl -s -A "Mozilla/5.0 (Chrome/130)" https://sigit.si/changelog | grep -o "vX.Y.Z" |
| 188 | |
| 189 | # Jobs supervisor: exactly one, and the log is quiet |
| 190 | ssh smb1-deploy 'ps aux | grep -E "bin/jobs|solid.queue" | grep -v grep' |
| 191 | ssh smb1-deploy 'sudo -u git tail -20 /home/git/apps/sigitsi/output-jobs.log' |
| 192 | ``` |
| 193 | |
| 194 | Feature-specific smoke tests belong here too — hit the endpoints the release |
| 195 | added (webhooks, new pages) and check their runbooks under `docs/` for |
| 196 | what "working" looks like. |