main
md 8.36 KB

name: release

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.

Releasing sigit.si

Server mechanics (SSH, Puma, hook internals, Postgres admin) live in the deployment skill. This skill is the release process around them: what a release is in this repo, the checks that gate it, and the order of operations.

The shape of a release

Features land on development through PRs. A release is:

  1. A release prep commit on development — version bump + changelog entry.
  2. The full pre-deploy check (below) against the development tip.
  3. A no-fast-forward merge of development into main.
  4. Deploy: push main to the prod remote.
  5. Post-deploy verification.

Two conventions to respect:

  • Promotions to main are explicit merge commits, named for the release: Merge development into main for the vX.Y.Z release. This is convention, not git config — don't fast-forward even though git would let you.
  • Merging to main and deploying are deliberate operator actions. Prepare everything, but get an explicit go-ahead before pushing main anywhere.

1. Release prep commit (on development)

Two files change; nothing else needs wiring.

lib/sigitsi/version.rb — bump Sigitsi::VERSION per semver (the file comment spells it out: MAJOR incompatible, MINOR features, PATCH fixes). The value surfaces in the site footer.

config/changelog/vX.Y.Z.md — one new file per release. The Changelog service (app/services/changelog.rb) auto-discovers files matching v<major>.<minor>.<patch>.md, sorts numerically by version (so 1.10.0 > 1.9.0 — file dates and mtimes don't matter), and renders them at /changelog. Malformed files are silently skipped, so verify your entry actually appears (e.g. bin/rails runner 'puts Changelog.latest&.version'). Format:

---
date: 2026-07-05
title: Short release name
headline: One sentence shown on the changelog index.
---

Markdown body. Look at existing entries in config/changelog/ for the voice:
plain prose, feature sections with ## headings, concrete over promotional.

Commit both together (Release vX.Y.Z: <one-line summary> works as a message), push to development (via PR or directly, matching however the user is currently working).

2. Full pre-deploy check

Run all of these against the exact tip being released. They mirror CI plus the production-only failure modes CI can't see. Don't skip the boring ones — each earned its place by failing for real at least once.

bundle install                      # lockfile sane, native gems build

# Schema drift: schema.rb must match the migration set exactly. Rebuild from
# migrations on an EMPTY db and diff. Two traps: (a) if schema.rb is present
# during db:prepare/migrate it can mark migrations "up" without running them
# (assume_migrated_upto), so move it aside first; (b) the dev database is
# shared across worktrees/branches — other branches' tables can leak into a
# dump. A from-scratch migrate on a dropped DB avoids both.
mv db/schema.rb /tmp/schema.committed.rb
RAILS_ENV=test bin/rails db:drop db:create db:migrate
diff /tmp/schema.committed.rb db/schema.rb   # empty diff or explain why not
git checkout db/schema.rb

# Full suite on a freshly loaded test DB (stale test DBs cause phantom
# uniqueness failures). Tailwind must be built or view specs fail on assets.
bundle exec rails tailwindcss:build
RAILS_ENV=test bin/rails db:drop db:create db:schema:load
bundle exec rspec                   # expect 0 failures

# The four CI gates, locally:
bin/brakeman --no-pager
bin/bundler-audit check --update
bin/importmap audit
bin/rubocop

# Production boot check. This is the one that catches site-down bugs: any
# initializer that raises in production (missing keys, required env) fails
# here, exactly as it would under the deploy hook. Supply dummies for values
# the initializers merely require to be present.
RAILS_ENV=production SECRET_KEY_BASE=dummy bin/rails assets:precompile

Config/environment audit

New code often needs new production config, and the deploy hook will not tell you it's missing — Puma just fails to boot, or a feature silently 500s.

# What did this release add?
git diff origin/main -- .env.example        # documented new vars
git diff origin/main --stat -- config/initializers/  # new boot-time requirements

# Is prod ready for them? (names/presence only — never print secret values)
ssh smb1-deploy 'sudo -u git bash -lc "printenv | grep -oE \"^SOME_NEW_VAR[A-Z_]*\""'

Notes that save time (each learned from a real incident):

  • The git user's ~/.profile is the env source; the post-receive hook starts with . ~/.profile, so vars set there do reach db:prepare, Puma, and bin/jobs.
  • Rails credentials must be edited locally and committed, never on the server. config/credentials.yml.enc is git-tracked and the deploy does git checkout -f main — a credentials:edit in the server work tree is silently wiped by the next deploy (this deleted the GitHub App credentials once). Edit with the local config/master.key, commit the .enc, and let the deploy distribute it.
  • The only trustworthy remote presence check is printenv VAR in the login shell: ssh ... 'sudo -u git bash -lc "printenv GITHUB_WEBHOOK_SECRET >/dev/null && echo present || echo absent"'. Compound [ -n "$VAR" ] checks through the ssh/sudo/bash quoting layers have produced false "set" readings, printenv | grep -c X matches SUDO_COMMAND (your own command line), and pgrep -f X over ssh matches its own invocation.

Production database state

# Pending migrations on prod (expected: the release's new ones, nothing odd)
ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && bin/rails db:migrate:status | tail"'

# Solid Queue: the queue DB must hold solid_queue_* tables (db:prepare loads
# db/queue_schema.rb; verify rather than assume — see deployment skill
# "Known issues")
ssh smb1-deploy 'sudo -u postgres psql sigitsi_production_queue -c "\dt"'

3. Promote development to main

Only after the user says go:

git checkout main && git pull origin main
git merge --no-ff development -m "Merge development into main for the vX.Y.Z release"
git push origin main

4. Deploy

The operator deploys with the interactive smb CLI from the repo root on main — it logs in, detects the Rails app, and pushes with the right key (~/.ssh/id_11@smbcloud), ending in "Deployment complete":

smb

A plain git push smbcloud main does the same thing but only if that key is what ssh offers; the Host api-1.smbcloud.xyz entry in ~/.ssh/config is not configured, so without the smb CLI expect Permission denied (publickey).

Either way it lands on the bare-repo post-receive hook, which has sharp edges (deployment skill has the full list): no set -e, migration output goes to the pushing terminal — read the push output, it is the only record of db:prepare's success — and bin/jobs start stacks a new Solid Queue supervisor on every deploy unless the hook has been amended with a pkill -f solid_queue || true line first.

5. Post-deploy verification

# Schema arrived
ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && bin/rails db:migrate:status | tail -5"'

# App serves (allow_browser rejects old/absent User-Agents with 403 — always
# curl with a modern UA or a healthy site looks broken)
curl -s -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0 (Chrome/130)" https://sigit.si/
curl -s -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0 (Chrome/130)" https://sigit.si/changelog

# New release visible
curl -s -A "Mozilla/5.0 (Chrome/130)" https://sigit.si/changelog | grep -o "vX.Y.Z"

# Jobs supervisor: exactly one, and the log is quiet
ssh smb1-deploy 'ps aux | grep -E "bin/jobs|solid.queue" | grep -v grep'
ssh smb1-deploy 'sudo -u git tail -20 /home/git/apps/sigitsi/output-jobs.log'

Feature-specific smoke tests belong here too — hit the endpoints the release added (webhooks, new pages) and check their runbooks under docs/ for what "working" looks like.