@massu/core 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/README.md +23 -11
- package/commands/massu-deploy.python-docker.md +170 -0
- package/commands/massu-deploy.python-fly.md +189 -0
- package/commands/massu-deploy.python-launchd.md +144 -0
- package/commands/massu-deploy.python-systemd.md +163 -0
- package/commands/massu-scaffold-page.swift.md +10 -10
- package/commands/massu-scaffold-router.python-django.md +153 -0
- package/commands/massu-scaffold-router.python-fastapi.md +145 -0
- package/dist/cli.js +9914 -4133
- package/dist/hooks/auto-learning-pipeline.js +45 -2
- package/dist/hooks/classify-failure.js +45 -2
- package/dist/hooks/cost-tracker.js +45 -2
- package/dist/hooks/fix-detector.js +45 -2
- package/dist/hooks/incident-pipeline.js +45 -2
- package/dist/hooks/post-edit-context.js +45 -2
- package/dist/hooks/post-tool-use.js +45 -2
- package/dist/hooks/pre-compact.js +45 -2
- package/dist/hooks/pre-delete-check.js +45 -2
- package/dist/hooks/quality-event.js +45 -2
- package/dist/hooks/rule-enforcement-pipeline.js +45 -2
- package/dist/hooks/session-end.js +45 -2
- package/dist/hooks/session-start.js +4790 -406
- package/dist/hooks/user-prompt.js +45 -2
- package/package.json +13 -4
- package/src/cli.ts +22 -2
- package/src/commands/config-refresh.ts +91 -23
- package/src/commands/init.ts +131 -24
- package/src/commands/install-commands.ts +142 -26
- package/src/commands/refresh-log.ts +37 -0
- package/src/commands/template-engine.ts +260 -0
- package/src/commands/watch.ts +430 -0
- package/src/config.ts +71 -0
- package/src/detect/adapters/nextjs-trpc.ts +166 -0
- package/src/detect/adapters/parse-guard.ts +133 -0
- package/src/detect/adapters/python-django.ts +208 -0
- package/src/detect/adapters/python-fastapi.ts +223 -0
- package/src/detect/adapters/query-helpers.ts +170 -0
- package/src/detect/adapters/runner.ts +252 -0
- package/src/detect/adapters/swift-swiftui.ts +171 -0
- package/src/detect/adapters/tree-sitter-loader.ts +467 -0
- package/src/detect/adapters/types.ts +173 -0
- package/src/detect/codebase-introspector.ts +190 -0
- package/src/detect/index.ts +28 -2
- package/src/detect/migrate.ts +4 -4
- package/src/detect/regex-fallback.ts +449 -0
- package/src/hooks/session-start.ts +94 -3
- package/src/lib/gitToplevel.ts +22 -0
- package/src/lib/installLock.ts +179 -0
- package/src/lib/pidLiveness.ts +67 -0
- package/src/lsp/auto-detect.ts +98 -0
- package/src/lsp/client.ts +776 -0
- package/src/lsp/enrich.ts +127 -0
- package/src/lsp/types.ts +221 -0
- package/src/watch/daemon.ts +385 -0
- package/src/watch/lockfile-detector.ts +65 -0
- package/src/watch/paths.ts +279 -0
- package/src/watch/state.ts +178 -0
package/commands/README.md
CHANGED
|
@@ -14,13 +14,17 @@ This README covers:
|
|
|
14
14
|
## 1. Variant filename convention
|
|
15
15
|
|
|
16
16
|
```
|
|
17
|
-
<base>.md
|
|
18
|
-
<base>.python.md
|
|
19
|
-
<base>.
|
|
20
|
-
<base>.
|
|
21
|
-
<base>.
|
|
17
|
+
<base>.md # default — used when no variant matches
|
|
18
|
+
<base>.python.md # FastAPI / Django / generic Python (one-axis)
|
|
19
|
+
<base>.python-fastapi.md # Python + FastAPI (two-axis: lang + sub-framework)
|
|
20
|
+
<base>.python-django.md # Python + Django (two-axis: lang + sub-framework)
|
|
21
|
+
<base>.swift.md # SwiftUI / iOS / visionOS
|
|
22
|
+
<base>.rust.md # axum / actix / generic Rust
|
|
23
|
+
<base>.typescript.md # reserved for future use; currently no variants ship
|
|
22
24
|
```
|
|
23
25
|
|
|
26
|
+
The two-axis form (`<base>.<lang>-<framework>.md`) is tried BEFORE the one-axis (`<base>.<lang>.md`) form. It is selected when the consumer's config declares `framework.languages.<lang>.framework = "<sub-framework>"` (e.g. `fastapi` or `django`).
|
|
27
|
+
|
|
24
28
|
The variant convention applies **only at the top level** of `packages/core/commands/`. Files inside subdirectories (e.g., `_shared-references/`, `massu-loop/references/`) are copied recursively as-is — no variant resolution, no dot-skip filtering. Future authors can use dotted filenames in nested dirs without losing them to the variant filter.
|
|
25
29
|
|
|
26
30
|
The variant convention is **opt-in**: a template that does NOT ship any `<base>.<variant>.md` siblings is variant-agnostic and the unsuffixed `<base>.md` is used everywhere.
|
|
@@ -100,11 +104,22 @@ Prints the resolved template content (post-variant-resolution) to stdout. Used i
|
|
|
100
104
|
|
|
101
105
|
| Base | Variants |
|
|
102
106
|
|------|----------|
|
|
103
|
-
| `massu-scaffold-router` | `.python.md` (FastAPI) |
|
|
107
|
+
| `massu-scaffold-router` | `.python-fastapi.md` (FastAPI — two-axis), `.python-django.md` (Django — two-axis) |
|
|
104
108
|
| `massu-scaffold-page` | `.swift.md` (SwiftUI), regenerated default with embedded Next.js / FastAPI / SwiftUI / Rust examples |
|
|
105
|
-
| `massu-deploy` | `.python.md
|
|
109
|
+
| `massu-deploy` | `.python-launchd.md`, `.python-systemd.md`, `.python-docker.md`, `.python-fly.md` (all two-axis) |
|
|
110
|
+
|
|
111
|
+
All other top-level templates ship as variant-agnostic defaults (one `<base>.md`).
|
|
112
|
+
|
|
113
|
+
### Template variable substitution
|
|
114
|
+
|
|
115
|
+
As of `@massu/core@1.3.0`, template files may contain `{{variable.path}}` and `{{variable.path | default("fallback")}}` placeholders. These are rendered against the consumer's `massu.config.yaml` (all `framework.*`, `paths.*`, and `config.*` fields) plus the `detected.*` block written by the codebase introspector (e.g. `detected.python.auth_dep`, `detected.swift.api_client_class`).
|
|
116
|
+
|
|
117
|
+
Rules:
|
|
118
|
+
- Every `{{detected.*}}` reference MUST include a `| default("...")` fallback — introspection may return nothing.
|
|
119
|
+
- Use `\{{` to emit a literal `{{` in the rendered output (e.g. Go/Docker format strings).
|
|
120
|
+
- A render error on a single file causes that file to be skipped (stderr message only); the rest of the install continues.
|
|
106
121
|
|
|
107
|
-
|
|
122
|
+
Pass `--skip-commands` to `massu init` or `massu refresh` to suppress command installation entirely.
|
|
108
123
|
|
|
109
124
|
## 6. Adding a new variant
|
|
110
125
|
|
|
@@ -112,11 +127,8 @@ All other 57 top-level templates ship as variant-agnostic defaults (one `<base>.
|
|
|
112
127
|
2. The default `<base>.md` should remain generic (or be regenerated if it was previously stack-specific — see `massu-scaffold-page.md` for the pattern of an embedded multi-stack default).
|
|
113
128
|
3. Add a row to the table in section 5.
|
|
114
129
|
4. Add a test case to `packages/core/src/__tests__/install-commands.test.ts` that exercises the new variant against a fixture config.
|
|
115
|
-
5. Update `docs/internal/2026-04-26-template-variant-audit.md` (the audit doc) with the new label.
|
|
116
130
|
|
|
117
131
|
## 7. Reference
|
|
118
132
|
|
|
119
|
-
- Plan: `/Users/ekoultra/hedge/docs/plans/2026-04-26-massu-stack-aware-command-templates.md`
|
|
120
|
-
- Audit: `docs/internal/2026-04-26-template-variant-audit.md`
|
|
121
133
|
- Implementation: `packages/core/src/commands/install-commands.ts`
|
|
122
134
|
- Tests: `packages/core/src/__tests__/install-commands.test.ts` and `show-template.test.ts`
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: massu-deploy
|
|
3
|
+
description: "Deploy a containerized Python service via docker compose — force-recreate the service container, health-check poll, diff before/after, rollback if unhealthy"
|
|
4
|
+
allowed-tools: Bash(*), Read(*), Grep(*), Glob(*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Massu Deploy: Python Service — Docker Compose
|
|
8
|
+
|
|
9
|
+
Force-recreates the Docker container for a Python service using `docker compose`. Use this variant when your service is containerized and `massu.config.yaml` declares `config.python.service_label` matching the compose service name.
|
|
10
|
+
|
|
11
|
+
## Workflow Position
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/massu-push -> /massu-deploy (docker variant)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This command redeploys a running container. **If the service handles financial, transactional, or consequential state, real data is at risk** — pre-flight checks are mandatory.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## NON-NEGOTIABLE RULES
|
|
22
|
+
|
|
23
|
+
- **Never deploy with uncommitted changes** — push first via `/massu-push`
|
|
24
|
+
- **Never deploy with failing tests** — test suite must be green before this runs
|
|
25
|
+
- **Always restart-and-probe** — `docker compose up` ≠ the new image is healthy
|
|
26
|
+
- **Never stop containers without identifying them first** — confirm the compose service name before running any destructive command
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Pre-flight
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Branch + working tree clean
|
|
34
|
+
test -z "$(git status --porcelain)" || { echo "DIRTY — commit/stash first"; exit 1; }
|
|
35
|
+
|
|
36
|
+
# 2. Tests green
|
|
37
|
+
pytest -x 2>&1 | tail -10
|
|
38
|
+
|
|
39
|
+
# 3. Confirm the service is running
|
|
40
|
+
docker compose ps {{config.python.service_label | default("<service-label>")}}
|
|
41
|
+
|
|
42
|
+
# 4. Capture current health for diff
|
|
43
|
+
curl -sS http://localhost:8000/health | python3 -m json.tool \
|
|
44
|
+
> /tmp/{{config.python.service_label | default("service")}}-health-before.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Build (if image needs rebuilding)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Rebuild the image for this service only (skip if using a pre-built registry image)
|
|
53
|
+
docker compose build {{config.python.service_label | default("<service-label>")}}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Approval Gate
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
===============================================================================
|
|
62
|
+
APPROVAL REQUIRED — DOCKER COMPOSE REDEPLOY
|
|
63
|
+
===============================================================================
|
|
64
|
+
|
|
65
|
+
Compose service : {{config.python.service_label | default("<service-label>")}}
|
|
66
|
+
Supervisor : docker compose
|
|
67
|
+
Pre-flight : PASS
|
|
68
|
+
|
|
69
|
+
This will:
|
|
70
|
+
1. docker compose up -d --force-recreate {{config.python.service_label | default("<service-label>")}}
|
|
71
|
+
2. Poll /health every 2s against the container (max 60s) until 200
|
|
72
|
+
3. Smoke /health + any critical endpoint
|
|
73
|
+
4. Diff before/after health JSON
|
|
74
|
+
5. On failure: print rollback command
|
|
75
|
+
|
|
76
|
+
Reply "approve" or "abort".
|
|
77
|
+
===============================================================================
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Redeploy
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
docker compose up -d --force-recreate {{config.python.service_label | default("<service-label>")}}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`--force-recreate` ensures the container is replaced even if the image tag didn't change. Omit if you only want `up` to no-op on unchanged images.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Poll Container Health
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
for i in $(seq 1 30); do
|
|
96
|
+
sleep 2
|
|
97
|
+
status=$(curl -sS -o /dev/null -w "%{http_code}" http://localhost:8000/health || echo "000")
|
|
98
|
+
[ "$status" = "200" ] && { echo "READY after ${i} polls"; break; }
|
|
99
|
+
echo "poll ${i}: ${status}"
|
|
100
|
+
done
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
If the compose file declares a `HEALTHCHECK` directive, you can also poll the Docker health state:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
for i in $(seq 1 30); do
|
|
107
|
+
sleep 2
|
|
108
|
+
state=$(docker inspect --format='\{{.State.Health.Status}}' \
|
|
109
|
+
"$(docker compose ps -q {{config.python.service_label | default("<service-label>")}})") 2>/dev/null || state="unknown"
|
|
110
|
+
[ "$state" = "healthy" ] && { echo "Container healthy after ${i} polls"; break; }
|
|
111
|
+
echo "poll ${i}: $state"
|
|
112
|
+
done
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Smoke + Diff
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
curl -sS http://localhost:8000/health | python3 -m json.tool \
|
|
121
|
+
> /tmp/{{config.python.service_label | default("service")}}-health-after.json
|
|
122
|
+
|
|
123
|
+
diff \
|
|
124
|
+
/tmp/{{config.python.service_label | default("service")}}-health-before.json \
|
|
125
|
+
/tmp/{{config.python.service_label | default("service")}}-health-after.json \
|
|
126
|
+
| head -50
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Tail Container Logs
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Last 100 lines, filtered for errors
|
|
135
|
+
docker compose logs {{config.python.service_label | default("<service-label>")}} \
|
|
136
|
+
--tail=100 --no-log-prefix | grep -iE "error|warn" | head -20
|
|
137
|
+
|
|
138
|
+
# Follow live (Ctrl-C to stop)
|
|
139
|
+
docker compose logs -f {{config.python.service_label | default("<service-label>")}} --no-log-prefix
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Rollback
|
|
145
|
+
|
|
146
|
+
If `/health` does not return 200 within 60s, or any smoke check fails:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Option A: revert code + redeploy
|
|
150
|
+
git revert HEAD --no-edit
|
|
151
|
+
docker compose build {{config.python.service_label | default("<service-label>")}}
|
|
152
|
+
docker compose up -d --force-recreate {{config.python.service_label | default("<service-label>")}}
|
|
153
|
+
|
|
154
|
+
# Option B: roll back to the previous image tag (if using a registry)
|
|
155
|
+
# docker compose pull {{config.python.service_label | default("<service-label>")}}:<prev-tag>
|
|
156
|
+
# docker compose up -d --force-recreate {{config.python.service_label | default("<service-label>")}}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Then page yourself or your on-call — an unhealthy production container is an incident.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Audit Log
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
echo "$(date -u +%FT%TZ) deploy surface=docker sha=$(git rev-parse HEAD) service={{config.python.service_label | default("<service-label>")}} actor=$(whoami)" \
|
|
167
|
+
>> data/audit/deploys.log
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Done. Report: compose service name, image digest or sha, restart time, health status, any warnings.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: massu-deploy
|
|
3
|
+
description: "Deploy a Python service to Fly.io — flyctl deploy, status check, log tail, rollback if unhealthy"
|
|
4
|
+
allowed-tools: Bash(*), Read(*), Grep(*), Glob(*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Massu Deploy: Python Service — Fly.io
|
|
8
|
+
|
|
9
|
+
Deploys a Python service to Fly.io using `flyctl deploy`. Use this variant when your project targets Fly.io and has a `fly.toml` in the repository root. The app name comes from `fly.toml` (or `config.python.service_label` as a fallback).
|
|
10
|
+
|
|
11
|
+
## Workflow Position
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/massu-push -> /massu-deploy (fly variant)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This command deploys to a live Fly.io app. **If the app handles financial, transactional, or otherwise consequential state, real data is at risk** — pre-flight checks are mandatory.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## NON-NEGOTIABLE RULES
|
|
22
|
+
|
|
23
|
+
- **Never deploy with uncommitted changes** — push first via `/massu-push`
|
|
24
|
+
- **Never deploy with failing tests** — test suite must be green before this runs
|
|
25
|
+
- **Always check status after deploy** — `flyctl deploy` success ≠ the new release is healthy
|
|
26
|
+
- **Never force a deploy to fix a broken deploy** — diagnose first, then decide
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Pre-flight
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Branch + working tree clean
|
|
34
|
+
test -z "$(git status --porcelain)" || { echo "DIRTY — commit/stash first"; exit 1; }
|
|
35
|
+
|
|
36
|
+
# 2. Tests green
|
|
37
|
+
pytest -x 2>&1 | tail -10
|
|
38
|
+
|
|
39
|
+
# 3. Confirm flyctl is authenticated and the app exists
|
|
40
|
+
flyctl status --app {{config.python.service_label | default("<app-name>")}} 2>&1 | head -20
|
|
41
|
+
|
|
42
|
+
# 4. Note the current release number for rollback reference
|
|
43
|
+
flyctl releases --app {{config.python.service_label | default("<app-name>")}} --limit 3
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## `fly.toml` — Key Fields to Verify
|
|
49
|
+
|
|
50
|
+
Before deploying, confirm these sections exist in `fly.toml`:
|
|
51
|
+
|
|
52
|
+
```toml
|
|
53
|
+
app = "{{config.python.service_label | default("<app-name>")}}"
|
|
54
|
+
primary_region = "<region>" # e.g. "ord", "lax", "iad"
|
|
55
|
+
|
|
56
|
+
[build]
|
|
57
|
+
# Dockerfile or builder block
|
|
58
|
+
|
|
59
|
+
[http_service]
|
|
60
|
+
internal_port = 8000
|
|
61
|
+
force_https = true
|
|
62
|
+
auto_stop_machines = true
|
|
63
|
+
auto_start_machines = true
|
|
64
|
+
|
|
65
|
+
[[vm]]
|
|
66
|
+
memory = "512mb"
|
|
67
|
+
cpu_kind = "shared"
|
|
68
|
+
cpus = 1
|
|
69
|
+
|
|
70
|
+
[checks]
|
|
71
|
+
[checks.health]
|
|
72
|
+
grace_period = "10s"
|
|
73
|
+
interval = "30s"
|
|
74
|
+
method = "GET"
|
|
75
|
+
path = "/health"
|
|
76
|
+
protocol = "https"
|
|
77
|
+
timeout = "10s"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Missing a `[checks.health]` block means Fly.io won't automatically detect an unhealthy release.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Approval Gate
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
===============================================================================
|
|
88
|
+
APPROVAL REQUIRED — FLY.IO DEPLOY
|
|
89
|
+
===============================================================================
|
|
90
|
+
|
|
91
|
+
App name : {{config.python.service_label | default("<app-name>")}}
|
|
92
|
+
Platform : Fly.io
|
|
93
|
+
Pre-flight : PASS
|
|
94
|
+
|
|
95
|
+
This will:
|
|
96
|
+
1. flyctl deploy --app {{config.python.service_label | default("<app-name>")}}
|
|
97
|
+
2. Wait for Fly.io health checks to pass (built-in to flyctl)
|
|
98
|
+
3. flyctl status to confirm all instances are running
|
|
99
|
+
4. Smoke /health endpoint
|
|
100
|
+
5. On failure: print rollback command
|
|
101
|
+
|
|
102
|
+
Reply "approve" or "abort".
|
|
103
|
+
===============================================================================
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Deploy
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
flyctl deploy --app {{config.python.service_label | default("<app-name>")}}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`flyctl deploy` builds the image (or reuses a cached layer), pushes to the Fly.io registry, creates a new release, and waits for health checks. Pass `--strategy rolling` or `--strategy bluegreen` if your app supports it.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Post-Deploy Status
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Confirm all machines are healthy
|
|
122
|
+
flyctl status --app {{config.python.service_label | default("<app-name>")}}
|
|
123
|
+
|
|
124
|
+
# Show the new release
|
|
125
|
+
flyctl releases --app {{config.python.service_label | default("<app-name>")}} --limit 1
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Smoke
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Hit the public health endpoint (replace with your actual hostname)
|
|
134
|
+
APP_HOST=$(flyctl info --app {{config.python.service_label | default("<app-name>")}} --hostname 2>/dev/null | head -1 || echo "{{config.python.service_label | default("<app-name>")}}.fly.dev")
|
|
135
|
+
curl -sS "https://${APP_HOST}/health" | python3 -m json.tool
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Tail Logs
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Live log stream (Ctrl-C to stop)
|
|
144
|
+
flyctl logs --app {{config.python.service_label | default("<app-name>")}}
|
|
145
|
+
|
|
146
|
+
# Filter for errors in recent output
|
|
147
|
+
flyctl logs --app {{config.python.service_label | default("<app-name>")}} 2>&1 | grep -iE "error|warn|exception" | head -30
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Rollback
|
|
153
|
+
|
|
154
|
+
If health checks fail or the smoke check returns non-200:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Roll back to the previous release
|
|
158
|
+
flyctl releases --app {{config.python.service_label | default("<app-name>")}} --limit 5
|
|
159
|
+
flyctl deploy --image <previous-image-ref> --app {{config.python.service_label | default("<app-name>")}}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Or use the Fly dashboard's "Rollback" button if the release number is known.
|
|
163
|
+
|
|
164
|
+
Then page yourself or your on-call — an unhealthy Fly.io release is an incident.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Secrets Management
|
|
169
|
+
|
|
170
|
+
Do NOT hard-code secrets in `fly.toml`. Use Fly secrets:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Set a secret (prompts for value if omitted)
|
|
174
|
+
flyctl secrets set MY_SECRET_KEY=<value> --app {{config.python.service_label | default("<app-name>")}}
|
|
175
|
+
|
|
176
|
+
# List current secret names (values are redacted)
|
|
177
|
+
flyctl secrets list --app {{config.python.service_label | default("<app-name>")}}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Audit Log
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
echo "$(date -u +%FT%TZ) deploy surface=fly sha=$(git rev-parse HEAD) app={{config.python.service_label | default("<app-name>")}} actor=$(whoami)" \
|
|
186
|
+
>> data/audit/deploys.log
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Done. Report: app name, release number, deploy time, health status, any warnings.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: massu-deploy
|
|
3
|
+
description: "Deploy a Python service supervised by launchd (macOS) — restart the launchd agent, poll health, diff before/after, rollback if unhealthy"
|
|
4
|
+
allowed-tools: Bash(*), Read(*), Grep(*), Glob(*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Massu Deploy: Python Service — launchd (macOS)
|
|
8
|
+
|
|
9
|
+
Restarts a Python service running under `launchd` on macOS. Use this variant when your `massu.config.yaml` declares `config.python.service_label` and your host is macOS.
|
|
10
|
+
|
|
11
|
+
## Workflow Position
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/massu-push -> /massu-deploy (launchd variant)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This command restarts a production service. **If the service handles financial, transactional, or otherwise consequential state, real data is at risk** — pre-flight checks are mandatory.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## NON-NEGOTIABLE RULES
|
|
22
|
+
|
|
23
|
+
- **Never deploy with uncommitted changes** — push first via `/massu-push`
|
|
24
|
+
- **Never deploy with failing tests** — test suite must be green before this runs
|
|
25
|
+
- **Always restart-and-probe** — file-saved ≠ process-running-the-fix
|
|
26
|
+
- **Never kill processes without identifying them first** — confirm the label before sending any signal
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Pre-flight
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Branch + working tree clean
|
|
34
|
+
test -z "$(git status --porcelain)" || { echo "DIRTY — commit/stash first"; exit 1; }
|
|
35
|
+
|
|
36
|
+
# 2. Tests green
|
|
37
|
+
pytest -x 2>&1 | tail -10
|
|
38
|
+
|
|
39
|
+
# 3. Confirm the launchd agent is registered
|
|
40
|
+
launchctl list | grep {{config.python.service_label | default("<service-label>")}}
|
|
41
|
+
|
|
42
|
+
# 4. Capture current health for diff
|
|
43
|
+
curl -sS http://localhost:8000/health | python3 -m json.tool \
|
|
44
|
+
> /tmp/{{config.python.service_label | default("service")}}-health-before.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Approval Gate
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
===============================================================================
|
|
53
|
+
APPROVAL REQUIRED — LAUNCHD RESTART
|
|
54
|
+
===============================================================================
|
|
55
|
+
|
|
56
|
+
Service label : {{config.python.service_label | default("<service-label>")}}
|
|
57
|
+
Supervisor : launchd (macOS)
|
|
58
|
+
Pre-flight : PASS
|
|
59
|
+
|
|
60
|
+
This will:
|
|
61
|
+
1. launchctl kickstart -k gui/$(id -u)/{{config.python.service_label | default("<service-label>")}}
|
|
62
|
+
2. Poll /health every 2s (max 60s) until 200
|
|
63
|
+
3. Smoke /health + any critical endpoint
|
|
64
|
+
4. Diff before/after health JSON
|
|
65
|
+
5. On failure: print rollback command
|
|
66
|
+
|
|
67
|
+
Reply "approve" or "abort".
|
|
68
|
+
===============================================================================
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Restart
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
launchctl kickstart -k gui/$(id -u)/{{config.python.service_label | default("<service-label>")}}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The `-k` flag kills the existing process before relaunching — equivalent to a clean restart. If you need to stop without restart, use `launchctl kill SIGTERM gui/$(id -u)/{{config.python.service_label | default("<service-label>")}}` instead.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Poll Health
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
for i in $(seq 1 30); do
|
|
87
|
+
sleep 2
|
|
88
|
+
status=$(curl -sS -o /dev/null -w "%{http_code}" http://localhost:8000/health || echo "000")
|
|
89
|
+
[ "$status" = "200" ] && { echo "READY after ${i} polls"; break; }
|
|
90
|
+
echo "poll ${i}: ${status}"
|
|
91
|
+
done
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Smoke + Diff
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
curl -sS http://localhost:8000/health | python3 -m json.tool \
|
|
100
|
+
> /tmp/{{config.python.service_label | default("service")}}-health-after.json
|
|
101
|
+
|
|
102
|
+
diff \
|
|
103
|
+
/tmp/{{config.python.service_label | default("service")}}-health-before.json \
|
|
104
|
+
/tmp/{{config.python.service_label | default("service")}}-health-after.json \
|
|
105
|
+
| head -50
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Tail Startup Logs
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Unified log (most reliable for launchd-managed services)
|
|
114
|
+
log show \
|
|
115
|
+
--predicate 'subsystem == "{{config.python.service_label | default("<service-label>")}}"' \
|
|
116
|
+
--last 2m --info 2>/dev/null | grep -iE "error|warn" | head -20
|
|
117
|
+
|
|
118
|
+
# Alternative: stderr from the plist's StandardErrorPath
|
|
119
|
+
# cat ~/Library/Logs/{{config.python.service_label | default("<service-label>")}}/stderr.log | tail -40
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Rollback
|
|
125
|
+
|
|
126
|
+
If `/health` does not return 200 within 60s, or any smoke check fails:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git revert HEAD --no-edit
|
|
130
|
+
launchctl kickstart -k gui/$(id -u)/{{config.python.service_label | default("<service-label>")}}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Then page yourself or your on-call — an unhealthy production service is an incident.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Audit Log
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
echo "$(date -u +%FT%TZ) deploy surface=launchd sha=$(git rev-parse HEAD) label={{config.python.service_label | default("<service-label>")}} actor=$(whoami)" \
|
|
141
|
+
>> data/audit/deploys.log
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Done. Report: label, sha, restart time, health status, any warnings.
|