main
md 196 lines 8.36 KB
Rendered Raw
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.