promote-environment
Run the SPEAKMAN.AI environment promotion gate (local→DEV, DEV→STG, STG→PROD) and produce a pass/block report on whether a codebase is ready to promote. Use when the user wants to promote between envi
SKILL.md.claude/skills/promote-environment/SKILL.md
GitHubpromote-environment — SPEAKMAN.AI Environment Promotion Gate
You are running the SPEAKMAN.AI environment promotion gate. Your job is to review the codebase against the checklist for the requested promotion level and produce a pass or block report. You do not fix issues — you surface them clearly so the developer knows exactly what must be resolved before the promotion proceeds.
Step 0 — Collect Inputs
Ask the user:
"Which promotion are you running?
- local → DEV (code quality gate)
- DEV → STG (security + reliability gate)
- STG → PROD (hardening gate)
And what is the path to the project root?"
Store:
- LEVEL:
local-dev,dev-stg, orstg-prod - PROJECT_ROOT: absolute path to the repository root
Each higher level inherits all checks from levels below it. STG → PROD runs all checks from all three levels.
Level 3 only — UAT results file: At Level 3, also look for the UAT results file. Check in this order:
{PROJECT_ROOT}/uat/uat_test_cases.md- If not found, ask: "Where is the completed UAT test cases file? (leave blank if UAT has not been run)"
Store as UAT_FILE (absolute path or null).
Step 1 — Read the Codebase
Before running any checks, read:
- Root
package.json(or equivalent manifest) to identify services and tech stack - All source files across every service — do not skip any layer
- All test files — presence and structure matter, not just source
- All environment config files (
.env.example, config objects, etc.) — never read.envdirectly READMEor equivalent if present
Do not guess at file content — read every file before drawing conclusions.
Level 3 only — parse UAT results:
If UAT_FILE is set, read it and extract the following. The file is Markdown produced by
/generate-uat-tests — parse these patterns:
UC-IDs — from
###headings whose text ends with— UC-{ID}(e.g.### Login — UC-01). Collect every UC-ID present in the file as UAT_UC_IDS.Test cases — from
####headings matchingTC-{NNN} ·. Each####block belongs to the most recently seen###UC-ID heading above it.Status per test case — from the Execution Record table immediately below each
####heading. The table has columnsActual Result | Status | Tester | Date | Notes. Read the data row (the row after the|---|separator). TheStatuscell containsPASS,FAIL,BLOCKED, or is blank (not run).Business outcomes — if a
## Business Outcomessection exists, read its table. Each row maps an outcome description to a list of UC-IDs. Store as UAT_OUTCOMES.Scope delta — if a
## Scope Deltasection exists, note how many untraced features and unimplemented UC-IDs are listed.
Build UAT_RESULTS:
{
"total": N,
"pass": N,
"fail": N,
"blocked": N,
"not_run": N, // blank Status cell
"uc_ids": {
"UC-01": { "tests": [{"id":"TC-001","status":"PASS"}, ...] },
"UC-02": { "tests": [{"id":"TC-003","status":"FAIL"}, ...] },
...
},
"uc_coverage": { // UC-IDs with at least one PASS
"passing": [...],
"failing": [...], // has tests but none are PASS
"blocked": [...], // all tests BLOCKED, none PASS
"not_run": [...] // all tests blank
},
"scope_delta": { "untraced": N, "unimplemented": N }
}
If UAT_FILE is null, set UAT_RESULTS = null.
Step 2 — Run the Checklist
Work through every check at or below the requested level. For each check record:
- Status: PASS / FAIL / WARN / N/A
- Evidence: the specific file and line that proves the status (for FAIL and WARN, quote the offending code)
- Action: what must be done to resolve it (for FAIL items only)
Level 1 — local → DEV: Code Quality
Correctness
- All test suites pass (report the test command and expected output — do not run it, flag it as "must verify")
- No hardcoded service URLs in source code — every inter-service URL comes from an environment variable
- Boolean request fields are validated as strict type before use — no
Boolean()coercion on user input - Unhandled error responses (status 500) return a static message — raw
error.messageis never forwarded to the client - Unhandled errors are logged server-side before responding (console.error or equivalent)
- String fields that accept user input have a maximum length enforced — unbounded strings are a FAIL
- Numeric fields that accept user input have minimum and maximum bounds enforced
- Every domain event published in the code has a wired subscriber, OR is documented as a forward extension point with no consumer — a published event whose consumer exists (an event-driven use case in the use case catalog) but has no subscriber is a FAIL, because the downstream effect silently never runs
Completeness
- A health check endpoint exists (
GET /healthor equivalent) with no auth requirement - Every API operation defined in the API contract has a corresponding UI control (if a frontend exists)
- Create forms include inputs for every optional field the API accepts, not just required fields
- Destructive actions (delete, irreversible status changes) require explicit user confirmation before the API call
Environment hygiene
-
.envfiles are listed in.gitignoreand are not committed to the repository - An
.env.examplefile exists for every service that reads environment variables - No secrets, API keys, or tokens appear in source code or committed config files
Test coverage
- Domain / unit tests exist and cover every invariant and validation rule
- Integration / infrastructure tests use a real database or equivalent (no mocked persistence layer)
- Interface / API tests cover every route: happy path, auth failure, and at least one validation error per endpoint
- E2E tests exist for every primary use case
- At least one E2E test exercises each primary user journey end-to-end through the API — the full ordered sequence of use cases for an actor, not just isolated single use cases
- E2E / journey tests do not seed the database at seams between use cases — any value one use case produces and another consumes must be obtained from the producing API response; direct seeding at a seam can hide a response-contract gap and is a FAIL
Level 2 — DEV → STG: Security + Reliability
(Runs all Level 1 checks plus the following)
Security
- Rate limiting is applied to all authentication endpoints (login, register, password reset, OTP)
- Rate limiting is applied to any endpoint that reveals the existence of a resource (anti-enumeration)
- Authentication error messages are identical for "not found" and "wrong credential" cases
- JWT or session tokens are verified on every protected endpoint — no endpoint is accidentally unprotected
- Request body size limits are explicitly configured — default framework limits must be confirmed or tightened
- Dependency audit is clean — no known high or critical vulnerabilities in production dependencies
Reliability
- Every outbound HTTP call to an external or downstream service has a timeout configured
- Network failures to downstream services return a structured error response (e.g. 502) — no unhandled promise rejections
- Database connections are closed or returned to a pool correctly — no connection leak on error paths
- The application handles SIGTERM gracefully — in-flight requests are allowed to complete before shutdown
Input validation
- All fields from the FIELD DEFINITIONS CATALOG (or equivalent spec) have their constraints enforced at the API boundary — minLength, maxLength, pattern, allowedValues
- Fields defined as strict types (boolean, integer, UUID) reject incorrect types with a 422 response — no silent coercion
- Pagination parameters are present on all list endpoints — unbounded list responses are a FAIL at this level
Observability
- All unhandled errors are logged with enough context to diagnose the failure (request path, error code, stack if available)
- Health check endpoint is present and returns a meaningful status (not just 200 OK — include version or uptime if possible)
Level 3 — STG → PROD: Hardening
(Runs all Level 1 and Level 2 checks plus the following)
Infrastructure
- Database is configured for production workloads — connection pooling, appropriate journal mode (WAL for SQLite, pgBouncer for Postgres, etc.), query timeouts
- Indexes exist on every column used in a
WHERE,ORDER BY, orJOINclause in production queries - Database credentials are injected via environment variables — no credentials in migration scripts or config files
- File-based storage is not used for persistent data — a proper database or object store is required
Security hardening
- HTTPS is enforced — HTTP requests are redirected or rejected
- Security headers are set (Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security)
- CORS origin is restricted to the production frontend domain — wildcard
*is a FAIL - JWT secrets and signing keys meet minimum entropy requirements (256-bit / 32 bytes random)
- Token expiry is configured and short enough for the security profile of the application
Observability
- Structured logging is in place — logs are machine-parseable (JSON) and routed to an aggregator
- Error tracking is configured (Sentry, Datadog, CloudWatch, or equivalent)
- Key business events are logged (user registration, authentication, mutations) for audit purposes
Deployment readiness
- All environment variables are documented in
.env.examplewith descriptions - The application starts cleanly with no warnings or deprecation notices
- Build output (if applicable) produces no errors or warnings
-
READMEcontains accurate run-locally and deploy instructions
Functionality (Level 3 only — requires completed UAT file)
- UAT test cases file exists and has been located
- All test cases have been executed — no blank Status cells remain
- UC coverage is 100% — every UC-ID in the file has at least one PASS result
- Test pass rate is 100% — no FAIL status in any test case
- Scope delta is documented — untraced features and unimplemented UCs are noted
Scoring rules:
- UAT_FILE null → F (UAT not run — FAIL; cannot promote to PROD without human UAT sign-off)
- any
not_run> 0 → FAIL (incomplete UAT; all cases must be executed) - any UC-ID in
uc_coverage.failing→ FAIL (a requirement is demonstrably broken) - any UC-ID in
uc_coverage.not_run→ FAIL (a requirement was never tested) - any UC-ID in
uc_coverage.blocked→ WARN (environment constraint; document and accept) - all UC-IDs in
uc_coverage.passing→ PASS
Step 3 — Produce the Report
Output the report in this exact format:
PROMOTION GATE: {FROM} → {TO}
PROJECT: {PROJECT_ROOT}
DATE: {today}
RESULT: PASS | BLOCKED
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FAILURES ({count}) — must resolve before promotion
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[F-01] {check name}
File: {file}:{line}
Found: {quoted evidence}
Fix: {specific action required}
[F-02] ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WARNINGS ({count}) — should resolve, will not block
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[W-01] {check name}
File: {file}:{line}
Note: {what was found and why it's a concern}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PASSED ({count})
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ {check name}
✓ ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
N/A ({count}) — not applicable to this stack
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- {check name}: {reason not applicable}
Level 3 only — append the Functionality Report section after PASSED / N/A:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FUNCTIONALITY REPORT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
UAT file : {UAT_FILE path, or "NOT FOUND — run /generate-uat-tests first"}
Tests total : {total} (Pass: {pass} Fail: {fail} Blocked: {blocked} Not run: {not_run})
UC coverage : {passing count} / {total uc count} ({pct}%)
Test pass rate : {pass} / {pass+fail} executed ({pct}%) [excludes Blocked]
Scope delta : {untraced} untraced features · {unimplemented} unimplemented UC-IDs
{If any UC-IDs are failing or not run, list them:}
UC-IDs with no passing test:
✗ UC-{ID} — {feature name} [{FAIL / Not run / Blocked}]
...
{If UAT_OUTCOMES is present:}
Business Outcome Coverage:
{For each outcome row:}
✓ {outcome} (UC-{IDs} — all PASS)
✗ {outcome} (UC-{IDs} — {one or more not passing})
~ {outcome} (UC-{IDs} — BLOCKED)
FUNCTIONALITY SCORE: {pass}/{total} tests · {uc_passing}/{uc_total} UC-IDs · {PASS | FAIL | PARTIAL}
The FUNCTIONALITY SCORE line contributes to the overall RESULT:
- Any Functionality FAIL → overall RESULT is BLOCKED (add F-entries in the FAILURES section above)
- Any Functionality WARN (blocked UCs) → add W-entries in the WARNINGS section above
- All Functionality PASS → does not change overall RESULT (other checks still apply)
If RESULT is BLOCKED, end with:
"Resolve all FAILURES above and re-run
/promote-environment {level}before proceeding."
If RESULT is PASS, end with:
"All checks passed. This codebase is cleared for promotion to {TARGET}."
Rules
- Never fix issues — only report them. The developer decides how to resolve.
- Quote evidence for every FAIL — "this file looks fine" is not evidence; a file path and line number is.
- N/A requires a reason — if a check doesn't apply, say why (e.g. "no frontend in this project", "not using a relational database").
- Do not skip checks — every check in the level must appear in the report as PASS, FAIL, WARN, or N/A. Omitting a check is not allowed.
- Warnings do not block — they are concerns the developer should be aware of but can choose to defer.
- Higher levels inherit lower levels — a STG → PROD run must include all Level 1 and Level 2 checks.