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.