Add a release skill: version, changelog, pre-deploy checks, no-ff promotion

Seto Elkahfi committed Jul 5, 2026 at 23:46 UTC c51370ea65da5cd32e98a2b09b46c137f4cb0864
1 file changed +180
.agents/skills/release/SKILL.md
+180
new file mode 100644 index 0000000..354dbd5 --- /dev/null +++ b/.agents/skills/release/SKILL.md @@ -0,0 +1,180 @@ +--- +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](../deployment/SKILL.md). 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: + +```markdown +--- +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. + +```bash +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. + +```bash +# 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: + +- 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 are the alternative for multi-line + secrets (private keys) — see the runbooks under `docs/`. +- Beware shell false positives when checking remotely: `printenv | grep -c X` + can match `SUDO_COMMAND` (your own command line), and `pgrep -f X` over ssh + matches its own invocation. Anchor patterns (`^X=`) and exclude the + grep/ssh process before trusting a count. + +### Production database state + +```bash +# 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: + +```bash +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 + +```bash +git push smbcloud main # the bare-repo post-receive hook does the rest +``` + +Remember the hook's 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 + +```bash +# 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.