Skip to content
Back to skills

Release

BSecurity

Release and deploy very-happy: publish the very-happy-cli npm package from v* tags, deploy happy-web-v2 and happy-server to the active production host, update the daemon hosts (mac-office, dev-sg), verify production, and roll back. Use when asked to release, publish, deploy, ship, roll back, or update production.

  • 5 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 2, 2026
developmentjavascriptjavashellbashnodedockerawsgitdatabase

Works with

  • terminal
  • cli

Security analysis

B88/100
  • mediumUses curl or wget to download content
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 25, 2026

npx -y skills add Mereithhh/very-happy --skill release --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Release?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Release
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/mereithhh-release/badge)](https://www.skillsdirectory.com/skills/mereithhh-release)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: release
description: >-
  Release and deploy very-happy: publish the very-happy-cli npm package from v* tags, deploy happy-web-v2 and happy-server to the active production host, update the daemon hosts (mac-office, dev-sg), verify production, and roll back. Use when asked to release, publish, deploy, ship, roll back, or update production.
---

# very-happy release

Production is `veryhappy.dev`: server + Web V2 on vh-sg (AWS Singapore), published CLI on
npm, daemons on mac-office (launchd) and dev-sg (systemd user unit).
`docs/operations.md` is the topology/runbook source;
`docs/PROCESS.md` is the gate/process source.

The only canonical source and release repository is the public GitHub repository
`Mereithhh/very-happy`. A private archive is read-only history: never push a
release commit or tag to it, and never trigger a deployment from it.

## Establish the target

Release targets are:

- `server`: publish and deploy the complete server image, including Web V2.
- `web`: deploy the same complete image; Web is not a host-mounted artifact.
- `cli`: publish `very-happy-cli` from a `vX.Y.Z` tag.
- `daemon`: install the published CLI on the daemon hosts: mac-office via
  `vh-update` + launchd re-adopt, dev-sg per `ops/dev-sg/README.md`.
- `all`: normally server → web → CLI → daemon, modified only by a spec's
  compatibility matrix.

Before any external mutation, show the intended commit/version, dirty-worktree
state, target and rollback point. Do not deploy unrelated local edits.

## Mandatory preflight

```bash
git status --short
git branch --show-current
git log -1 --oneline
RELEASE_ORIGIN=$(git remote get-url origin)
case "$RELEASE_ORIGIN" in
  https://github.com/Mereithhh/very-happy.git|git@github.com:Mereithhh/very-happy.git) ;;
  *) echo "refusing release from non-canonical origin: $RELEASE_ORIGIN" >&2; exit 1 ;;
esac
test "$(gh repo view Mereithhh/very-happy --json visibility --jq .visibility)" = PUBLIC
```

Run the exact gates in `AGENTS.md`. For CLI also run the built artifact:

```bash
HAPPY_HOME_DIR=$(mktemp -d) node packages/happy-cli/dist/index.mjs --version
```

Never release from a failing gate unless the user explicitly authorizes an
emergency exception after seeing the failure and rollback plan.

### Changelog coverage (run locally BEFORE tagging or dispatching)

Every release must carry its `CHANGELOG_RELEASES` entry
(`packages/happy-web-v2/src/app/changelogRelease.ts` + text keys). CI enforces
it — `deploy-hwsg.yml` and `publish.yml` both fail without it — but a failed
`v*` tag burns a version number, so run the same script first:

```bash
# Server/Web: diff the target SHA against the release currently live
# (read from the public entry asset name, no credentials needed).
node scripts/changelog/check-release.mjs --mode web --live https://veryhappy.dev --sha <sha>
# CLI: user-facing commits under packages/happy-cli|happy-wire since the
# previous v* tag require an entry with cliVersion '<X.Y.Z>'.
node scripts/changelog/check-release.mjs --mode cli --version X.Y.Z --sha <sha>
```

Keep `id` and `cliVersion` on their own lines in each release entry: the current
release scanner matches those fields at line starts. Verify both release modes
before the merge that will be tagged.

`FAIL` prints the conventional-commit draft: write the entry (id, title,
summary, items; `cliVersion` when a CLI ships), merge it, re-lock the SHA.
Escape hatches are explicit and logged — deploy input `changelog=skip` with a
mandatory `changelog_skip_reason`, or for CLI an annotated tag whose message
contains `[changelog-skip: <reason>]`. Use them only for changes with no
user-visible effect; the rollback path (live SHA not an ancestor of the
target) always needs the explicit skip.

## Deploy server/Web

Preferred path:

```bash
git push origin main
# Wait at least 20 seconds for GitHub's ref to settle.
gh workflow run deploy-hwsg.yml --ref main -f target=all -f rollout=switch
gh run list --workflow=deploy-hwsg.yml --limit 3
gh run view <run-id> --json headSha,status,conclusion,url
```

`headSha` must equal the intended pushed commit. The workflow is deliberately
manual and deploys only checked-out repository state. Release from merged
`main`; do not assume pushing a feature branch changes the workflow's checkout.

The workflow publishes `ghcr.io/mereithhh/very-happy-server:<commit-sha>`, resolves
its manifest digest, and the remote host pulls that immutable digest. Source,
migrations, Prisma schema/generated Client and Web move together. Choose the
rollout phase explicitly:

```bash
# One final interrupting bootstrap; only when /opt/happy/release/state.env is absent
gh workflow run deploy-hwsg.yml --ref main -f target=all -f rollout=groundwork
# Initial candidate proof; does not switch traffic or promote latest
gh workflow run deploy-hwsg.yml --ref main -f target=all -f rollout=shadow
# Initial switch of the same shadowed digest, then every normal later release
gh workflow run deploy-hwsg.yml --ref main -f target=all -f rollout=switch
```

Groundwork and switch promote only the verified active digest to `latest`;
shadow never does. A switch reloads Caddy, which reconnects every WebSocket at
once; B-494 bounds the socket.io recovery that follows (operations §socket.io
Redis stream and recovery bounds). Do not reload Caddy outside a release casually. The remote state machine requires Redis, an explicit Prisma
connection limit, Caddy ≥2.10.2 and host headroom. Candidate readiness includes
DB, Redis, adapter warmup and exact Web asset. A bidirectional cross-slot canary
is mandatory before Caddy changes. Never bypass these gates by restoring the old
single-container helper for a normal release.

If the packaged migration tree changed, review it for expand compatibility and
set `VH_RELEASE_MIGRATIONS_REVIEWED=<target commit>` in production before the
candidate run. The acknowledgement is commit-bound; never reuse a stale value.
Migration lock/statement timeouts fail candidate without changing active traffic.

Automatic rollback exists only before the switch commits: before drain, stop
candidate only; after drain, cancel the old slot's drain state; after an include
write, restore/reload the old Caddy include (only if the old slot answers
`/health`) and retain both slots. Once public verification passes after the Caddy
reload, the switch is committed (`state.env` written, B-483); draining the old
slot is best-effort and an undrained old slot is left running off the upstream
with a printed stop command. A workflow reported failed or cancelled after that
point did not roll back: establish reality per operations §A switch that dies
after the Caddy reload. Never delete the retained candidate while it may own
connections.

Environment changes: `docker compose restart` does not reread `env_file`. Deploy
the active merged `main` with `rollout=switch`, so candidate reads the new env
while old remains available.

The legacy `hw-sg` SSH alias is not the control origin and must never be used
for production deployment. If `vh-sg` is absent or does not resolve to the
current `veryhappy.dev` origin, stop and establish the exact target first.

After publishing a CLI that changes handover behavior, update the daemon hosts.
A normal blue-green Server/Web switch does not require restarting daemons: it
opens candidate, waits for every `rpc-registered` acknowledgement, then closes
old.

## Publish CLI

The tag is the version source; do not hand-edit package.json for the normal CI
path:

```bash
git tag vX.Y.Z
git push origin vX.Y.Z
gh run list --workflow=publish.yml --limit 3
```

The tag also launches hosted cross-platform smoke jobs. npm publication and
smoke run concurrently, so npm availability is not sufficient release evidence.
The publish workflow releases all six `very-happy-tools-<arch>-<os>` packages
before the main CLI, with one exact shared version. It is safe to rerun after a
partial publish; never publish the main package manually ahead of its platform
artifacts. Verify the main package, the platform artifact for the deployment
machine, and the smoke run:

```bash
npm view very-happy-cli@X.Y.Z version
npm view very-happy-tools-arm64-darwin@X.Y.Z version
gh run list --workflow=cli-smoke-test.yml --commit=<tag-sha>
```

The relay's recommended version **advances by itself** (B-348): `publish.yml`
publishes the main package under `next` and its `promote` job moves the `latest`
dist-tag only after the same repository/tag/SHA push smoke run has all six
Linux/macOS/Windows × Node 20/24 jobs completed successfully in its current
attempt. A green workflow with skipped or missing jobs is rejected; tag pushes
run Windows as well as macOS. The relay follows
`latest` via `CLI_VERSION_REGISTRY_LOOKUP=true`, cached one minute (failed refresh backs off five minutes). The promote job waits
up to ten minutes for npm and the public relay to advertise the exact release;
timeout or a mismatching explicit pin fails verification without undoing npm or
overwriting the pin. Rerun after resolving the cause. So there is no
per-release env edit. `CLI_RECOMMENDED_VERSION` must be unset for the registry
to drive recommendations; reserve that pin for an explicit hold or rollback.
Production's `CLI_AUTO_UPDATE_VERSION=latest` (B-503) follows the same promoted
tag, so promotion is also the moment idle machines are allowed to install it.
Confirm it landed:

```sh
curl -fsS https://veryhappy.dev/v1/version/cli   # source:"registry", recommendedVersion = autoUpdateVersion = X.Y.Z, autoUpdatePolicy:"latest"
npm view very-happy-cli version                  # the promoted latest
npm view very-happy-cli dist-tags                # `next` may be ahead if smoke failed
```

`latest` still sitting on the previous version means the `promote` job did not
promote — read its log before doing anything by hand; the usual cause is a red
smoke run, and that is the gate working.

**Recommending a release and installing it for people are two variables**
(B-351) **with one default since B-503: both follow the promoted `latest`.**
The Owner's standing decision (2026-09-25) is that the fleet always runs the
newest promoted CLI, so a release needs no second env edit: once `promote`
moves `latest`, `/v1/version/cli` reports `autoUpdateVersion = X.Y.Z` within
a minute and idle daemons install it on their next policy check (hourly).
`npm publish` alone still reaches nobody — `next` is never followed, and a red
smoke run leaves `latest` (and therefore the fleet) where it was.

Holding or rolling back the automatic install is the only time the variable
is touched by hand:

```sh
scripts/release/advance-auto-update.sh X.Y.Z            # pin: hold the fleet at / roll back to X.Y.Z
scripts/release/advance-auto-update.sh latest           # return to the default
# add --dry-run to only print the plan
```

For an exact version it refuses unless npm `latest` already is `X.Y.Z` (never
pin an unpromoted tarball); it backs up `/opt/happy/.env` on vh-sg, dispatches
`deploy-hwsg.yml rollout=switch` on `main` (a `restart` would not reread
`env_file`), waits for that exact headSha, and fails unless `/v1/version/cli`
then reports the expected `autoUpdateVersion` / `autoUpdatePolicy`. It prints
the backup path — rollback is restoring it and running the same deploy. A pin
older than what machines already run does not downgrade them (the daemon never
installs backwards); it stops them advancing. The script needs an `ssh vh-sg`
alias; from a workstation without one, run it on dev-sg or do its steps by hand
through `ssh dev-sg 'ssh vh-sg "…"'`.

To **hold or roll back the recommendation** without touching npm, pin it (the
pin always beats the lookup), then deploy so the candidate reads the new env
(`restart` does not reread `env_file`):

```sh
ssh vh-sg
sed -i 's|^CLI_RECOMMENDED_VERSION=.*|CLI_RECOMMENDED_VERSION=X.Y.Z|' /opt/happy/.env
gh workflow run deploy-hwsg.yml --ref main -f target=all -f rollout=switch
curl -fsS https://veryhappy.dev/v1/version/cli   # must not be source:"unavailable"
```

The publish workflow's `promote` job checks this for you after it moves the
dist-tag, and prints a loud notice in the run summary when the fleet still will
not be offered the release — a pin holding it back, or a `latest` that never
moved because smoke was red. It does not fail the run, because holding a release
back is legitimate, but you will not silently forget again.

`CLI_MINIMUM_VERSION` is separate: it makes the update banner non-dismissible.
Raise it only when older clients are actually incompatible. With registry lookup
working, removing `CLI_RECOMMENDED_VERSION` does not silence recommendations;
pinning it also caps `CLI_AUTO_UPDATE_VERSION=latest` (the brake stops installs,
not only banners). Removing `CLI_AUTO_UPDATE_VERSION` disables unattended
installation only.

Daemon hosts still need their supervised handover: with `latest` in force they
install by themselves when idle, but confirm the running version and the
supervisor afterwards (or update them at once). Update mac-office with `vh-update` (repository fallback:
`bash scripts/update-daemon.sh`) and confirm the running daemon version,
not only npm metadata. On mac-office, handover starts the replacement outside
launchd. Complete the existing re-adoption procedure in
[docs/operations.md](../../../docs/operations.md) (search “Re-adopt”), then verify
launchd is `running`, the daemon has the expected version, and a read-only RPC
works. Do not use `kickstart -k` against a live same-version daemon: the launcher
can yield and leave neither process supervised. This host-specific re-adoption
is not the generic user update command. dev-sg (systemd user unit) is
`sudo npm i -g …` → `systemctl --user restart very-happy-daemon` (B-505: the
unit's `KillMode=process` restarts the daemon only — session wrappers, their
turns and tmux terminals survive and are re-adopted; `daemon stop` + `start` or
a shell `daemon start` are no longer the path, and the CLI refuses the latter
while the daemon is unit-owned). Check `very-happy daemon list` (`turnActive`
for every session) if you want to know what is mid-turn first; see
[ops/dev-sg/README.md](../../../ops/dev-sg/README.md).

Never use `npm publish`, bare `npx`, `--ignore-scripts`,
or move/force an existing tag.

`vh-update` replaces the daemon but intentionally leaves already-running agent
session wrappers alive. A wrapper and its active SDK Query keep the CLI code
loaded when that wrapper started. Verify CLI session features with a session
started after the upgrade; do not claim an older live session hot-loaded the new
package. Stopping/resuming a business session is a separate interrupting action,
not an implicit release step.

## Production verification

```bash
curl -fsS https://veryhappy.dev/health
VH_MAIN=$(curl -fsS https://veryhappy.dev/ | grep -oE '/assets/[^" ]+\.js' | head -1)
curl -fsSI "https://veryhappy.dev${VH_MAIN}" | grep -i '^content-type:.*javascript'
```

A green health check and a matching SHA only prove the right image is serving.
`AGENTS.md` also requires proof that THIS release's change is in it — grep the
shipped assets for a string only the new code contains (a CSS rule, a storage
key, a socket event name). Do not hand-roll that curl; it has been wrong three
times in three releases, and every one of those failure modes reads as the
opposite of the truth:

```bash
node scripts/dev/check-shipped.mjs --needle '.tg-subagent-open' --needle 'vh:subagent-open'
```

It reads the live SHA off the served entry, walks the chunk graph transitively,
rejects the SPA's HTML fallback (a made-up /assets path returns 200, not 404),
and exits non-zero on a miss. The traps it exists to absorb are written up in
its header. When a needle is missing, check ancestry before concluding a
regression — another session deploying on top of you is the common case:

```bash
git merge-base --is-ancestor <your-sha> <live-sha> && echo "yours is in"
```

For browser acceptance, record the loaded entry/CSS and controlling service
worker before refreshing. Verify takeover via `controllerchange` and the actual
loaded entry; reload alone does not prove a version migration. Complete relevant items in
`docs/verify-queue.md`.

## Rollback

- Web/server: use the phase-aware rollback above; do not update daemons for a server-only rollback.
- CLI/daemon: install the fixed previous version with the reviewed script allowlist, use `daemon start` handover, then hand the daemon back to launchd (mac-office) or systemd (dev-sg).
- Database: migrations must be forward-compatible; never improvise a destructive
  down migration during an incident.

Report what was published/deployed, exact versions/SHAs, verification evidence,
and remaining manual acceptance. A successful build alone is not a release.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.