D28 — Deploy + Live Smoke Pack — Agent Prompt (REVIEW DRAFT Rev6)
D28 — Deploy + Live Smoke Pack — Agent Prompt (REVIEW DRAFT Rev6)
Date: 2026-05-10 | Rev: 6 | Phase 2E URL template fix (?collection=workflow_steps) + Forensic diagnostic discipline + Decision matrix relations split (PROMPT_URL_DRIFT vs SERVER_BREAKAGE) Status: DRAFT for GPT/User review — KHÔNG dispatch Agent target: claude-go (VPS SSH + git + docker on host) Scope: Tier 2+3 — Build production image + Deploy + Smoke Predecessor: D28 Tier 1 build_verify_only PASS (2026-05-10) + Stage 1 PREFLIGHT rev2 (DISCOVERY ONLY — NOT final baseline due to undeclared substitution at 0D + 0G; clean re-run required after rev5) Critical: Pack này LẦN ĐẦU thực sự deploy lên production — Stage 1 phải re-run CLEAN sau rev5 để có baseline ký approval
⚠️ User-facing impact warning
Stage 2 sẽ:
- Build production image mới
- Restart
incomex-nuxtcontainer (~30s downtime) - Brief 502 errors trong window restart
- Nginx có thể serve stale cache vài phút
Hard boundaries
NO_DIRECTUS_MUTATION=true
NO_PG_MUTATION=true
NO_PUBLISH_EVENT_OUTBOX=true
NO_CHANGE_TABLE_REGISTRY=true
NO_FIX_TBL_MODULES_LIST=true
NO_ADD_ENTITY_TYPE_COLUMN=true
NO_PACKAGE_INSTALL_ON_HOST=true
NO_LOCKFILE_CHANGE=true
NO_FILE_MUTATION_OUTSIDE_TEMP=true
NO_SECRET_IN_CODE_OR_LOG=true
NO_PRINT_ENV_TOKEN_URL=true
NO_PRINT_HTTP_BODY=true
NO_PRINT_TAIL_AFTER_SCAN_FAIL=true (rev3)
NO_AUTO_ROLLBACK=true
NO_TOUCH_DIRECTUS_SERVICE=true
NO_TOUCH_POSTGRES_SERVICE=true
NO_DEPLOY_WITHOUT_USER_APPROVAL=true
NO_SMOKE_EVENT_OUTBOX_ROUTE=true
NO_HARDCODE_IMAGE_TAG=true
NO_MUTATION_IN_STAGE_1=true
NO_OVERWRITE_BACKUP_TAG=true (rev3)
NO_OVERWRITE_BACKUP_FILE=true (rev3)
NO_HEAD_DOCKERFILE_RAW=true (rev3)
NO_PRINT_COMPOSE_DIFF_RAW=true (rev4)
NO_STAGE2_RECOMPUTE_BACKUP_TIMESTAMP=true (rev4)
NO_RELATIONS_BODY_GREP_BEFORE_SCAN=true (rev4)
NO_WORKFLOW_DISCOVERY_REQUIRED_FOR_STAGE1_PASS=true (rev4)
NO_UNDECLARED_SUBSTITUTION=true (rev5)
Log safety pattern (rev3 — branching, rev4 — extended)
Mọi command tạo log/diff/response → temp file → scan → branching:
SCAN_RESULT
├── PASS: print safe tail/summary (grep -v patterns) — DESTINATION VISIBLE
├── FAIL_FILENAME_FALSE_POSITIVE (e.g. forgot-password.*.mjs):
│ print REDACTED SUMMARY only (count + classification)
│ KHÔNG print tail (kể cả filtered)
└── FAIL_SECRET_LEAK_SUSPECTED:
STOP
KHÔNG print gì từ log/diff/body
Recommend offline review
CẤM print grep -v ... $LOGFILE | tail khi SCAN=FAIL bất kể classification. Tail/summary print chỉ khi SCAN=PASS.
Apply for (rev4 — full list):
- production image build log
- compose up log
- container logs
- workflow sample response
- compose diff (rev4)
- relations endpoint response body (rev4 — scan TRƯỚC body-shape grep)
Naming convention — TS variables (rev4)
Để tránh nhầm lẫn giữa Stage 1 backup timestamp và Stage 2 log file timestamps:
TS = captured ONCE in Stage 1 (Phase 0I), used for backup tag/path,
becomes proposed_backup_image_tag suffix + proposed_compose_backup_path suffix
Stage 2 NEVER recomputes TS — it loads STAGE1_* values instead.
TS_LOG = per-step log file timestamp in Stage 2 (Phase 1D, 1E diff, 1F, 1H, 2E)
Independent from TS. Only used for /tmp/d28-*.log uniqueness.
NEVER substituted into image tags or compose backup paths.
CẤM trộn 2 biến này trong Stage 2.
Service name rule + Substitution discipline (rev5)
SERVICE_NAME_RULE
Compose binding xác nhận từ Stage 1 PREFLIGHT supplement (2026-05-10):
docker-compose.yml lines 129-131:
nuxt:
image: nuxt-ssr-local:s174
container_name: incomex-nuxt
Quy ước:
- Compose service name =
nuxt. - Container name =
incomex-nuxt(chỉ là alias hiển thịdocker ps, không dùng trongdocker compose <verb>). - Mọi
docker compose <verb> <name>MUST dùng Compose service name =nuxt.docker compose ps nuxt✅docker compose up -d nuxt✅docker compose logs nuxt✅ (logs nhận service name)docker compose port nuxt 3000✅docker compose restart nuxt✅
docker compose <verb> incomex-nuxtSẼ FAIL vớino such service: incomex-nuxt❌- Raw
docker logs <container>,docker inspect <container>,docker ps— chấp nhận container name → có thể dùngincomex-nuxtnếu cần. Hiện rev5 KHÔNG dùng raw docker logs (vẫn dùngdocker compose logs nuxtở 1H để giữ context service-aware). - Raw
docker tag,docker build,docker images,docker image rm— nhận image tag, không liên quan service/container name.
NO_UNDECLARED_SUBSTITUTION (hard boundary rev5)
Agent KHÔNG được silent-substitute service name, container name, file path, image tag, env var, route path, hoặc bất kỳ argument nào trong prompt.
Workflow expected khi verbatim fail:
Nếu verbatim command fails với "no such service", "no such container", "file not found", "image not found", "no such image", hoặc tương tự infrastructure mismatch, Agent MUST:
- STOP execution ngay tại phase đó.
- Report drift trong report với fields:
preflight_status=BLOCKED # hoặc phase_status=BLOCKED tùy stagedrift_detected=truedrift_phase=<phase_id, e.g. 0D, 1F>drift_command=<verbatim command failed>drift_error=<exact error message>drift_resolution=PROMPT_REVISION_REQUIRED - KHÔNG attempt workaround, KHÔNG fall-through, KHÔNG silent-substitute.
Substitution CHỈ được phép khi prompt có rule explicit (ví dụ SERVICE_NAME_RULE rev5). Mọi áp dụng PHẢI declare trong report:
service_name_rule_applied=true
undeclared_substitution_used=false
Vì sao có rule này
Stage 1 PREFLIGHT rev2 (2026-05-10) phát hiện drift "incomex-nuxt vs nuxt" — Agent đã silent-substitute ở 0D + 0G (đã honest disclose ở supplement). Production deploy đầu tiên cần verbatim baseline, không substitution ngầm. Rev5 codify rule + ban substitution undeclared.
Forensic diagnostic discipline (rev6)
Khi verbatim smoke command FAIL, Agent có thể chạy 1 forensic diagnostic call để phân loại root cause (prompt drift vs server breakage), CHỈ KHI tuân thủ tất cả 5 điều kiện sau:
- Verbatim result preserved: Verbatim FAIL phải giữ nguyên trong official smoke count. Diagnostic KHÔNG được override smoke result.
- Labeled clearly: Diagnostic call phải được label "diagnostic" trong report, KHÔNG phải "smoke pass". Section riêng, separate khỏi smoke pass count.
- Read-only: Diagnostic chỉ được là HTTP GET hoặc read-only DB query. KHÔNG mutation, KHÔNG side effect.
- No body/secret/unsafe URL output: Diagnostic phải tuân thủ log safety pattern (rev3+rev4 — secret scan trước, no body print, temp file chmod 600 + cleanup).
- Informs classification, never overrides matrix: Diagnostic dùng để chọn giữa các classification trong decision matrix (PROMPT_URL_DRIFT vs SERVER_BREAKAGE), NHƯNG không tự ý tạo classification mới ngoài matrix.
Diagnostic call PHẢI được declare trong report:
diagnostic_executed=true|false
diagnostic_purpose=<root cause classification | other>
diagnostic_url_or_query=<exact URL/query, redacted if env-derived>
diagnostic_result=<HTTP status hoặc query result summary>
diagnostic_secret_scan=PASS|FAIL_*
diagnostic_did_not_override_smoke_count=true
diagnostic_classification_outcome=<PROMPT_URL_DRIFT|SERVER_BREAKAGE|INCONCLUSIVE>
CẤM:
- Multiple diagnostic calls cho 1 verbatim FAIL (1 diagnostic là đủ; nhiều = trigger D28_LEAK_INVESTIGATION_PACK style review).
- Diagnostic gây mutation (CRUD, file write outside temp).
- Diagnostic dùng credentials không có trong rev5 standard env.
- Diagnostic được dùng để bypass smoke FAIL → smoke PASS classification.
Vì sao có rule này: Stage 2 rev1 (2026-05-10) Agent đã thực hiện 1 declared diagnostic call để phân loại relations FAIL = PROMPT_URL_DRIFT (không phải SERVER_BREAKAGE). GPT chấp nhận hành vi đó vì verbatim preserved + declared. Rev6 codify để tránh mơ hồ tương lai.
Dispatch flag system
Stage 1 — Preflight only
RUN_STAGE=1_PREFLIGHT_ONLY
Optional flag (rev4):
OPTIONAL_WORKFLOW_DISCOVERY=true|false (default: false → workflow discovery SKIPPED_SAFETY)
Agent runs Phase 0 → STOP → upload Stage 1 report.
Stage 2 — Deploy + smoke
RUN_STAGE=2_DEPLOY_AND_SMOKE
APPROVE D28 DEPLOY: I authorize deploying commits d2db418 + 0947613 as image <NEW_TAG> to production with brief service interruption, using rollback image <BACKUP_TAG> and compose backup <COMPOSE_BACKUP_PATH>.
<NEW_TAG>, <BACKUP_TAG>, <COMPOSE_BACKUP_PATH> PHẢI khớp exact với Stage 1 report's proposed_new_image_tag, proposed_backup_image_tag, proposed_compose_backup_path từ Stage 1 re-run sau rev5 (NOT Stage 1 PREFLIGHT rev2 cũ).
Stage 2 agent đọc Stage 1 report trước và verify all 3 fields match. Bất kỳ mismatch → STOP APPROVAL_PHRASE_OR_BINDING_MISMATCH.
Stage 2 KHÔNG được tự sinh TS mới (rev4). Mọi backup tag/path PHẢI dùng exact values load từ Stage 1 report (xem Phase 1A step 7).
Rollback
APPROVE D28 ROLLBACK: I authorize restoring compose backup <COMPOSE_BACKUP_PATH> and bringing up incomex-nuxt with the previous image.
Allowed Docker operations per stage
STAGE 1 (READ-ONLY, no docker logs):
docker compose ps nuxt, docker compose config (rev5 — service name "nuxt")
docker inspect (for service info, redacted, raw verb chấp nhận container_name)
docker images (for backup tag conflict check)
KHÔNG: docker tag, build, run, up, restart, stop, start, exec, logs
STAGE 2 (after approval):
docker tag (Phase 1C backup, dùng STAGE1_BACKUP_IMAGE_TAG)
docker build (Phase 1D, dùng STAGE1_NEW_IMAGE_TAG)
docker compose up -d nuxt (Phase 1F — service name rev5)
docker compose logs nuxt (Phase 1H — service name rev5, with log safety pattern)
docker image rm (cleanup new image on abort)
FORBIDDEN throughout:
docker compose down, stop, restart of directus/postgres
Any data service mutation
Default tags (Stage 1 propose, Stage 2 verify exact and load)
TS=$(date +%s) # captured ONCE in Stage 1 only (Phase 0I), NEVER recomputed in Stage 2
proposed_new_image_tag=nuxt-ssr-local:d2db418
proposed_backup_image_tag=nuxt-ssr-local:pre-d28-rollback-<TS>
proposed_compose_backup_path=/opt/incomex/docker/docker-compose.yml.pre-d28-<TS>
Unique timestamp tránh overwrite từ pack runs trước. Stage 2 LOAD những giá trị này từ Stage 1 report (rev4 — Phase 1A step 7).
Lưu ý rev5: TS từ Stage 1 PREFLIGHT rev2 cũ (1778394987) KHÔNG được carry-forward sang Stage 2. Stage 1 phải re-run sau rev5 để sinh TS mới + clean baseline.
Phase 0 — Stage 1 preflight (NO MUTATION)
0A. HEAD commits verified
ssh contabo "cd /opt/incomex/docker/nuxt-repo && git log --oneline -5"
Verify d2db418 + 0947613 trong history. Missing → STOP.
0B. Build verify PASS report
Read KB report, verify all PASS fields. Bất kỳ ≠ PASS → STOP.
0C. Source tree clean
ssh contabo "cd /opt/incomex/docker/nuxt-repo && git status --porcelain"
Empty = PASS.
0D. Production service inspection (rev5 — service name "nuxt")
ssh contabo "cd /opt/incomex/docker && docker compose ps nuxt --format 'table {{.Service}}\t{{.Image}}\t{{.State}}'"
Capture current_production_image=, state must be running.
Nếu command fail với "no such service: nuxt" → STOP SERVICE_NAME_DRIFT_AT_0D (per NO_UNDECLARED_SUBSTITUTION). Report drift, KHÔNG fall-through. Substitution chỉ được dùng nếu rev6+ ban hành rule mới.
0E. Compose image line discovery
ssh contabo "grep -nE 'image:.*nuxt-ssr-local' /opt/incomex/docker/docker-compose.yml"
Capture line number + exact image string. Match count must = 1. Else STOP COMPOSE_IMAGE_PATCH_AMBIGUOUS.
0F. Production Dockerfile verify (filtered, no raw print)
ssh contabo "test -f /opt/incomex/docker/nuxt-repo/web/Dockerfile && echo DOCKERFILE_OK || echo DOCKERFILE_MISSING"
ssh contabo "grep -E '^(FROM|WORKDIR|COPY|RUN|CMD|ENTRYPOINT)' /opt/incomex/docker/nuxt-repo/web/Dockerfile | head -30"
KHÔNG head -50 raw (có thể leak ARG/ENV).
0G. Smoke base URL discovery (rev5 — service name "nuxt", mode only, no full URL)
ssh contabo "docker compose -f /opt/incomex/docker/docker-compose.yml port nuxt 3000 2>/dev/null"
ssh contabo "grep -E 'ports:|nginx|proxy' /opt/incomex/docker/docker-compose.yml | head -5"
Determine + report MODE only:
smoke_base_url_mode=LOCALHOST|PORT_MAPPING|PUBLIC_HOST_VIA_NGINX|BLOCKED
smoke_base_url_redacted=<scheme>://<host-redacted>:<port>
KHÔNG print full env-derived URL. BLOCKED → STOP. Nếu port command fail "no such service: nuxt" → STOP SERVICE_NAME_DRIFT_AT_0G.
0H. Health check mechanism
ssh contabo "grep -A3 'healthcheck:' /opt/incomex/docker/docker-compose.yml | head -10"
ssh contabo "grep -i '^HEALTHCHECK' /opt/incomex/docker/nuxt-repo/web/Dockerfile"
Document.
0I. Backup tag conflict check (NO mutation) — TS captured here
TS=$(date +%s) # captured ONCE here, used in 0J too, reported as proposed_backup_image_tag suffix
ssh contabo "docker images nuxt-ssr-local:pre-d28-rollback-$TS --format '{{.Repository}}:{{.Tag}}' | head -1"
Empty (tag không tồn tại) = PASS. Tag exists → STOP ROLLBACK_TAG_CONFLICT. Unique TS đảm bảo conflict cực hiếm.
Report proposed_backup_image_tag=nuxt-ssr-local:pre-d28-rollback-<TS>.
0J. Compose backup path conflict check (same TS as 0I)
ssh contabo "test -f /opt/incomex/docker/docker-compose.yml.pre-d28-$TS && echo CONFLICT || echo CLEAR"
CONFLICT → STOP. CLEAR = OK. Report proposed_compose_backup_path=/opt/incomex/docker/docker-compose.yml.pre-d28-<TS>.
0K. Workflow sample ID — DEFAULT SKIPPED_SAFETY (rev4)
Default behavior: Stage 1 KHÔNG cần workflow sample ID. Phase 2 workflow tab smoke sẽ SKIP.
workflow_sample_discovery=SKIPPED_SAFETY
workflow_sample_status=NONE
workflow_tab_smoke=SKIPPED_NO_SAMPLE_ID
Stage 1 PASS KHÔNG phụ thuộc bước này. Hard boundary NO_WORKFLOW_DISCOVERY_REQUIRED_FOR_STAGE1_PASS=true.
Optional discovery (only if dispatch flag OPTIONAL_WORKFLOW_DISCOVERY=true):
Nếu User explicitly enable, Agent thực hiện với pattern temp-file + scan + extract-id-only + no-body-print:
TS_LOG=$(date +%s) # log file TS, NOT backup TS
LOGFILE="/tmp/d28-workflow-sample-$TS_LOG.log"
SAMPLE_ID_FILE="/tmp/d28-sample-id-$TS_LOG.txt"
ssh contabo "set -a; source /opt/incomex/docker/.env 2>/dev/null; set +a; \
curl -s -H \"Authorization: Bearer \$DIRECTUS_ADMIN_TOKEN\" \
\"\$DIRECTUS_PUBLIC_URL/items/workflows?limit=1&fields=id\" \
> $LOGFILE 2>&1; echo CURL_EXIT=\$?"
ssh contabo "chmod 600 $LOGFILE"
# Scan FIRST
ssh contabo "grep -qi 'token\|secret\|bearer\|password\|authorization' $LOGFILE && echo SAMPLE_LOG_SCAN=FAIL || echo SAMPLE_LOG_SCAN=PASS"
# Branching:
# - SCAN=PASS → extract id only into $SAMPLE_ID_FILE; persist for Stage 2 if FOUND
# - SCAN=FAIL_FILENAME_FALSE_POSITIVE → SKIPPED_FALSE_POSITIVE, no extraction
# - SCAN=FAIL_SECRET_LEAK_SUSPECTED → STOP entire 0K, classify, no extraction
# Cleanup log (keep $SAMPLE_ID_FILE only if SCAN=PASS for Stage 2 use)
ssh contabo "rm -f $LOGFILE"
Report:
workflow_sample_discovery=PERFORMED|SKIPPED_SAFETY|SKIPPED_FALSE_POSITIVE|STOPPED_LEAK
workflow_sample_status=FOUND|NONE|SKIPPED
workflow_tab_smoke=PLANNED|SKIPPED_NO_SAMPLE_ID
KHÔNG print id, KHÔNG print URL, KHÔNG print body. Stage 1 PASS bất kể giá trị này.
0L. Stage 1 report
Path: knowledge/dev/laws/dieu28-trien-khai/reports/d28-deploy-and-live-smoke-stage1-preflight-report.md
phase=PREFLIGHT_ONLY
run_stage=1_PREFLIGHT_ONLY
## Verifications
head_commits_verified=true|false
build_verify_pass_confirmed=true|false
source_tree_clean=true|false
current_production_image=<from 0D>
proposed_new_image_tag=nuxt-ssr-local:d2db418
proposed_backup_image_tag=nuxt-ssr-local:pre-d28-rollback-<TS>
proposed_compose_backup_path=/opt/incomex/docker/docker-compose.yml.pre-d28-<TS>
production_service_running=true|false
production_dockerfile_path=
compose_image_line_number=
compose_image_match_count=1
smoke_base_url_mode=
health_check_mechanism=
workflow_sample_discovery=PERFORMED|SKIPPED_SAFETY|SKIPPED_FALSE_POSITIVE|STOPPED_LEAK
workflow_sample_status=FOUND|NONE|SKIPPED
workflow_tab_smoke=PLANNED|SKIPPED_NO_SAMPLE_ID
backup_tag_conflict_check=CLEAR|CONFLICT
compose_backup_path_conflict_check=CLEAR|CONFLICT
## Service name binding (rev5)
compose_service_name=nuxt
container_name=incomex-nuxt
service_name_rule_applied=true
undeclared_substitution_used=false
drift_detected=false
drift_phase=N/A
drift_command=N/A
drift_error=N/A
## Mutations performed in Stage 1
deploy_executed=false
smoke_executed=false
image_tag_created=false
backup_image_tag_created=false
compose_modified=false
container_restarted=false
file_writes_outside_temp=0
## Status
preflight_status=PASS|FAIL|BLOCKED
status=AWAITING_DEPLOY_APPROVAL
## Stage 2 dispatch requirements
required_dispatch_flag=RUN_STAGE=2_DEPLOY_AND_SMOKE
required_approval_phrase=APPROVE D28 DEPLOY: I authorize deploying commits d2db418 + 0947613 as image nuxt-ssr-local:d2db418 to production with brief service interruption, using rollback image nuxt-ssr-local:pre-d28-rollback-<TS> and compose backup /opt/incomex/docker/docker-compose.yml.pre-d28-<TS>.
Phase 1 — Stage 2 deploy execution
1A. Verify dispatch + approval phrase + Stage 1 report binding + Load STAGE1_* values (rev4) + verify rev5 cleanliness
Steps:
- Verify
RUN_STAGE=2_DEPLOY_AND_SMOKEtrong dispatch - Read Stage 1 report from KB:
knowledge/dev/laws/dieu28-trien-khai/reports/d28-deploy-and-live-smoke-stage1-preflight-report.md - Verify Stage 1
preflight_status=PASSvàstatus=AWAITING_DEPLOY_APPROVAL - Verify rev5 cleanliness (rev5):
service_name_rule_applied=trueundeclared_substitution_used=falsedrift_detected=false- Mismatch any → STOP
STAGE1_NOT_REV5_CLEAN
- Extract Stage 1's
proposed_new_image_tag,proposed_backup_image_tag,proposed_compose_backup_path,current_production_image - Verify approval phrase contains EXACT 3 values (NEW_TAG, BACKUP_TAG, COMPOSE_BACKUP_PATH) match parsed Stage 1 values
- Mismatch any → STOP
APPROVAL_PHRASE_OR_BINDING_MISMATCH - Load STAGE1_ variables (rev4) — Stage 2 NEVER recomputes:*
STAGE1_NEW_IMAGE_TAG="<from Stage 1 report 'proposed_new_image_tag'>"
STAGE1_BACKUP_IMAGE_TAG="<from Stage 1 report 'proposed_backup_image_tag'>"
STAGE1_COMPOSE_BACKUP_PATH="<from Stage 1 report 'proposed_compose_backup_path'>"
STAGE1_CURRENT_PRODUCTION_IMAGE="<from Stage 1 report 'current_production_image'>"
All Phase 1B–1H + 3B operations PHẢI dùng $STAGE1_* variables. CẤM regenerate TS=$(date +%s) trong Stage 2 cho mục đích backup tag/path. TS_LOG cho log file là độc lập, không thay thế cho TS backup.
Report:
stage1_values_loaded=true
stage1_rev5_clean_verified=true
stage2_used_stage1_backup_tag=true
stage2_used_stage1_compose_backup_path=true
stage2_used_stage1_new_image_tag=true
stage2_used_stage1_current_production_image=true
stage2_recomputed_backup_timestamp=false
1B. Stage 2 drift re-check (rev3, rev5 — service name "nuxt")
Re-verify all Stage 1 conditions still hold:
# Source tree clean
ssh contabo "cd /opt/incomex/docker/nuxt-repo && git status --porcelain"
# HEAD includes commits
ssh contabo "cd /opt/incomex/docker/nuxt-repo && git log --oneline -5 | grep -E '(d2db418|0947613)'"
# Current image still matches Stage 1's STAGE1_CURRENT_PRODUCTION_IMAGE (service name "nuxt")
ssh contabo "docker compose -f /opt/incomex/docker/docker-compose.yml ps nuxt --format '{{.Image}}'"
# Compose line still matches
ssh contabo "grep -nE 'image:.*nuxt-ssr-local' /opt/incomex/docker/docker-compose.yml"
# Build verify report still PASS — agent KB call (NOT shell command):
# call agent-data:get_document on the build-verify report path
# verify build_verify_status=PASS still present
Bất kỳ drift → STOP PREFLIGHT_DRIFT_AFTER_STAGE1. Recommend re-run Stage 1.
1C. Backup current image (FIRST mutation Stage 2, dùng STAGE1 values)
ssh contabo "docker tag $STAGE1_CURRENT_PRODUCTION_IMAGE $STAGE1_BACKUP_IMAGE_TAG"
ssh contabo "docker images $STAGE1_BACKUP_IMAGE_TAG --format '{{.Repository}}:{{.Tag}}'"
Output empty → STOP. Report backup_image_tag_created=true, backup_image_tag=$STAGE1_BACKUP_IMAGE_TAG.
1D. Build production image (log-safe branching, dùng STAGE1 tag)
TS_LOG=$(date +%s) # log file timestamp — NOT for tag
LOGFILE="/tmp/d28-deploy-build-$TS_LOG.log"
ssh contabo "cd /opt/incomex/docker/nuxt-repo && \
docker build -f web/Dockerfile -t $STAGE1_NEW_IMAGE_TAG web/ \
> $LOGFILE 2>&1; echo BUILD_EXIT=\$?"
ssh contabo "chmod 600 $LOGFILE"
# Scan boolean
ssh contabo "grep -qi 'token\|secret\|bearer\|password\|authorization' $LOGFILE && echo SCAN=FAIL || echo SCAN=PASS"
Branching:
- SCAN=PASS →
tail -15filtered safe tail printable - SCAN=FAIL + filename pattern (forgot-password.*.mjs etc.) → classify FILENAME_FALSE_POSITIVE, report count + summary, NO tail print
- SCAN=FAIL + actual leak suspected → STOP, NO content print
ssh contabo "rm -f $LOGFILE"
Build fail BEFORE restart → STOP. Cleanup new image:
ssh contabo "docker image rm $STAGE1_NEW_IMAGE_TAG"
Report:
new_image_created=true|false
new_image_tag=$STAGE1_NEW_IMAGE_TAG
new_image_removed_on_abort=true|false|N/A
old_image_preserved=true
build_log_secret_scan=PASS|FAIL_FILENAME_FALSE_POSITIVE|FAIL_LEAK
build_log_tail_printed=true|false
1E. Compose patch (dùng STAGE1 path, diff log-safe — rev4)
# Backup compose to STAGE1_COMPOSE_BACKUP_PATH (rev4 — NEVER recompute path)
ssh contabo "cp /opt/incomex/docker/docker-compose.yml $STAGE1_COMPOSE_BACKUP_PATH"
# Verify backup file created
ssh contabo "test -f $STAGE1_COMPOSE_BACKUP_PATH && echo BACKUP_OK || echo BACKUP_FAIL"
# Patch using captured exact image (NOT hardcoded), idempotent on exact line
ssh contabo "sed -i \"s|image: $STAGE1_CURRENT_PRODUCTION_IMAGE\$|image: $STAGE1_NEW_IMAGE_TAG|\" /opt/incomex/docker/docker-compose.yml"
# ===== rev4 — Diff verification, LOG-SAFE =====
TS_LOG=$(date +%s)
DIFF_FILE="/tmp/d28-compose-diff-$TS_LOG.txt"
# Write diff to temp file (NEVER stream raw to stdout)
ssh contabo "diff $STAGE1_COMPOSE_BACKUP_PATH /opt/incomex/docker/docker-compose.yml > $DIFF_FILE 2>&1 || true"
ssh contabo "chmod 600 $DIFF_FILE"
# Secret scan BEFORE any inspection
ssh contabo "grep -qi 'token\|secret\|bearer\|password\|authorization' $DIFF_FILE && echo COMPOSE_DIFF_SCAN=FAIL || echo COMPOSE_DIFF_SCAN=PASS"
# Branching:
# - SCAN=PASS → counts only (DIFF_LINE_COUNT, NUXT_DIFF_LINE_COUNT), NO raw diff print
# - SCAN=FAIL_FILENAME_FALSE_POSITIVE → counts only, classification noted, NO raw diff content
# - SCAN=FAIL_SECRET_LEAK_SUSPECTED → STOP, restore backup, no diff inspection at all
# IF SCAN=PASS or SCAN=FAIL_FILENAME_FALSE_POSITIVE — counts only:
# Total < and > diff lines (must = 2 for single image change)
DIFF_LINE_COUNT=$(ssh contabo "grep -c '^[<>]' $DIFF_FILE")
# nuxt-ssr-local lines among diff (must = DIFF_LINE_COUNT)
NUXT_DIFF_LINE_COUNT=$(ssh contabo "grep -E '^[<>]' $DIFF_FILE | grep -c 'nuxt-ssr-local'")
# Validation: DIFF_LINE_COUNT must = 2 AND NUXT_DIFF_LINE_COUNT must = DIFF_LINE_COUNT
# Else: restore backup + STOP
# Cleanup diff file (NEVER print contents — rev4)
ssh contabo "rm -f $DIFF_FILE"
Bất kỳ unexpected diff (line count ≠ 2 hoặc nuxt count ≠ diff count) → restore backup, STOP:
ssh contabo "cp $STAGE1_COMPOSE_BACKUP_PATH /opt/incomex/docker/docker-compose.yml"
SCAN=FAIL_SECRET_LEAK_SUSPECTED → STOP ngay, restore backup, classification only, không inspect counts.
Report:
compose_file_modified=true|false
compose_backup_path=$STAGE1_COMPOSE_BACKUP_PATH
compose_diff_scanned_before_print=true (rev4)
compose_diff_raw_printed=false (rev4)
compose_diff_secret_scan=PASS|FAIL_FILENAME_FALSE_POSITIVE|FAIL_LEAK
compose_diff_only_nuxt_image_line=true|false
compose_patch_line_count=1
1F. Container restart (rev5 — service name "nuxt", log-safe)
TS_LOG=$(date +%s)
LOGFILE="/tmp/d28-deploy-up-$TS_LOG.log"
ssh contabo "cd /opt/incomex/docker && docker compose up -d nuxt > $LOGFILE 2>&1; echo UP_EXIT=\$?"
ssh contabo "chmod 600 $LOGFILE"
ssh contabo "grep -qi 'token\|secret\|bearer\|password\|authorization' $LOGFILE && echo SCAN=FAIL || echo SCAN=PASS"
Same branching as 1D. SCAN=FAIL → no tail print.
ssh contabo "rm -f $LOGFILE"
1G. Wait for ready (rev5 — service name "nuxt", poll up to 60s)
for i in $(seq 1 30); do
STATE=$(ssh contabo "docker compose -f /opt/incomex/docker/docker-compose.yml ps nuxt --format '{{.State}}'")
[ "$STATE" = "running" ] && echo "READY_AFTER=${i}_polls" && break
sleep 2
done
Not ready 60s → STOP, recommend rollback.
1H. Container logs check (rev5 — service name "nuxt", log-safe)
TS_LOG=$(date +%s)
LOGFILE="/tmp/d28-container-logs-$TS_LOG.log"
ssh contabo "docker compose -f /opt/incomex/docker/docker-compose.yml logs --tail=50 nuxt > $LOGFILE 2>&1"
ssh contabo "chmod 600 $LOGFILE"
ssh contabo "grep -qi 'token\|secret\|bearer\|password\|authorization' $LOGFILE && echo SCAN=FAIL || echo SCAN=PASS"
Branching same. SCAN=FAIL → no tail.
ssh contabo "rm -f $LOGFILE"
Phase 2 — Live smoke
Target counts
13 registry routes (excluding event_outbox)
3 non-registry pages
2-4 special routes (depends on Stage 1 workflow_sample_status)
1 API endpoint
event_outbox: EXCLUDED
total_max=21 (= 13+3+4+1), total_min=19 (if workflow tabs SKIPPED — DEFAULT rev4)
2A. Wait SSR warmup
sleep 15
2B-2D. HTTP smoke (only status, no body)
declare -a ROUTES_REGISTRY=(catalog table module dot_tool page collection agent checkpoint_type checkpoint_set entity_dependency checkpoint_instance changelog system_issue)
declare -a ROUTES_NONREG=("/knowledge/workflows" "/knowledge/modules" "/knowledge/current-tasks")
declare -a ROUTES_SPECIAL=("/admin/proposals" "/knowledge/registries")
# (workflow tabs added if Stage 1 reported workflow_sample_status=FOUND
# AND OPTIONAL_WORKFLOW_DISCOVERY=true was used in Stage 1)
for r in registry+nonreg+special:
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -L "<base_url>$path" --max-time 10)
count PASS/FAIL
2E. Relations endpoint (1 curl, secret-scan FIRST, then body shape — rev4)
TS_LOG=$(date +%s)
RESPFILE="/tmp/d28-relations-$TS_LOG.txt"
# SINGLE curl call (rev3)
STATUS=$(ssh contabo "curl -s -o $RESPFILE -w '%{http_code}' '<base_url>/api/discovery/relations?collection=workflow_steps' --max-time 10")
# rev6: endpoint contract (web/server/api/discovery/relations.get.ts lines 25-30)
# requires ?collection=<name>. workflow_steps used as stable, verified collection
# (200 + relation-shape confirmed in Stage 2 diagnostic 2026-05-10).
ssh contabo "chmod 600 $RESPFILE"
# Secret scan FIRST (rev4 — must precede ANY body inspection or grep)
ssh contabo "grep -qi 'token\|secret\|bearer\|password\|authorization' $RESPFILE && echo RELATIONS_SCAN=FAIL || echo RELATIONS_SCAN=PASS"
# Branching (rev4):
# - SCAN=PASS:
# allow body-shape boolean grep + size sanity (no body content print)
# - SCAN=FAIL_FILENAME_FALSE_POSITIVE:
# report status + classification only, NO body grep, NO size print
# - SCAN=FAIL_SECRET_LEAK_SUSPECTED:
# STOP, no body grep, no size, no inspection — alert User as CRITICAL
# IF SCAN=PASS:
ssh contabo "grep -q 'system_issue' $RESPFILE && echo RELATIONS_HAS_DATA=true || echo RELATIONS_HAS_DATA=false"
ssh contabo "wc -c $RESPFILE"
# Cleanup
ssh contabo "rm -f $RESPFILE"
Report:
relations_status=<HTTP code>
relations_response_secret_scan=PASS|FAIL_FILENAME_FALSE_POSITIVE|FAIL_SECRET_LEAK_SUSPECTED (rev4)
relations_has_data=true|false (only valid if SCAN=PASS)
relations_body_size_bytes=<int> (only valid if SCAN=PASS)
relations_body_grepped_after_scan=true|false (rev4 — must be true only when SCAN=PASS)
KHÔNG print body content under any branch.
2F. event_outbox EXCLUDED
event_outbox_route_smoke=DEFERRED_TO_PHASE_1C
Phase 3 — Decision & report
3A. Result decision
| Result | phase_status | rollback_recommended |
|---|---|---|
| All PASS | PASS | No |
| 1-2 non-critical FAIL | PARTIAL | User decides |
| 3+ FAIL | PARTIAL | Yes |
| Relations endpoint HTTP FAIL — diagnostic confirms PROMPT_URL_DRIFT | PARTIAL_PROMPT_DRIFT | No (prompt fix only, no rollback) |
| Relations endpoint HTTP FAIL — diagnostic confirms SERVER_BREAKAGE | FAIL | Yes (CRITICAL — rollback recommend) |
| Relations endpoint HTTP FAIL — diagnostic INCONCLUSIVE hoặc not run | FAIL | Yes (CRITICAL — conservative) |
| Container restart FAIL | FAIL | Yes (CRITICAL) |
| Compose diff SCAN=FAIL_LEAK (rev4) | FAIL | Yes (restore backup before any further mutation) |
| Relations SCAN=FAIL_LEAK (rev4) | FAIL | Yes (CRITICAL — possible API data leak) |
3B. Rollback method (rev3 — restore compose backup, rev5 — service name "nuxt")
NOT auto-execute. Requires:
APPROVE D28 ROLLBACK: I authorize restoring compose backup <COMPOSE_BACKUP_PATH> and bringing up incomex-nuxt with the previous image.
<COMPOSE_BACKUP_PATH> PHẢI khớp $STAGE1_COMPOSE_BACKUP_PATH.
With phrase:
# Restore compose backup using STAGE1_COMPOSE_BACKUP_PATH (rev4)
ssh contabo "cp $STAGE1_COMPOSE_BACKUP_PATH /opt/incomex/docker/docker-compose.yml"
# Restart with previous image config (rev5 — service name "nuxt")
ssh contabo "cd /opt/incomex/docker && docker compose up -d nuxt"
# Wait + smoke 1-2 routes to verify rollback (with same log safety pattern)
KHÔNG dùng sed reverse — restore backup file là cleaner (full compose state).
3C. Stage 2 report
Path: knowledge/dev/laws/dieu28-trien-khai/reports/d28-deploy-and-live-smoke-stage2-execution-report.md
phase=DEPLOY_AND_SMOKE
run_stage=2_DEPLOY_AND_SMOKE
approval_phrase_verified=true
stage1_report_binding_verified=true
stage1_rev5_clean_verified=true
stage1_drift_recheck=PASS|FAIL
## Stage 1 binding (rev4)
stage1_values_loaded=true
stage2_used_stage1_backup_tag=true
stage2_used_stage1_compose_backup_path=true
stage2_used_stage1_new_image_tag=true
stage2_used_stage1_current_production_image=true
stage2_recomputed_backup_timestamp=false
## Service name binding (rev5)
compose_service_name_used=nuxt
container_name_reference_used=incomex-nuxt|N/A
service_name_rule_applied=true
undeclared_substitution_used=false
drift_detected=false
## Image lifecycle
old_image=$STAGE1_CURRENT_PRODUCTION_IMAGE
backup_image_tag_created=true|false
backup_image_tag=$STAGE1_BACKUP_IMAGE_TAG
new_image_created=true|false
new_image_tag=$STAGE1_NEW_IMAGE_TAG
new_image_removed_on_abort=true|false|N/A
old_image_preserved=true
## Compose lifecycle (rev4 — log-safe)
compose_file_modified=true|false
compose_backup_path=$STAGE1_COMPOSE_BACKUP_PATH
compose_diff_scanned_before_print=true (rev4)
compose_diff_raw_printed=false (rev4)
compose_diff_secret_scan=PASS|FAIL_FILENAME_FALSE_POSITIVE|FAIL_LEAK
compose_diff_only_nuxt_image_line=true|false
compose_patch_line_count=1
## Logs (all branched per safety pattern)
build_log_secret_scan=PASS|FAIL_FILENAME_FALSE_POSITIVE|FAIL_LEAK
build_log_tail_printed=true|false
compose_up_log_secret_scan=PASS|FAIL_FILENAME_FALSE_POSITIVE|FAIL_LEAK
compose_up_log_tail_printed=true|false
container_log_secret_scan=PASS|FAIL_FILENAME_FALSE_POSITIVE|FAIL_LEAK
container_log_tail_printed=true|false
## Restart
container_restart_status=PASS|FAIL
container_restart_duration_seconds=
## Smoke
smoke_executed=true|false
smoke_base_url_mode=
total_smoked=
registry_routes_passed=N/13
non_registry_pages_passed=N/3
special_routes_passed=N/4_or_2
api_endpoint_status=
relations_response_secret_scan=PASS|FAIL_FILENAME_FALSE_POSITIVE|FAIL_LEAK (rev4)
relations_has_data=true|false (only if SCAN=PASS)
relations_body_grepped_after_scan=true|false (rev4)
event_outbox_route_skipped=true (deferred Phase 1C)
fail_list=
## Attestation
no_directus_mutation=true
no_pg_mutation=true
no_publish_event_outbox=true
no_table_registry_mutation=true
no_secret_printed=true
no_http_body_printed=true
no_tail_printed_after_scan_fail=true
no_compose_diff_raw_printed=true (rev4)
no_unauthorized_deploy=true
no_auto_rollback_executed=true
no_overwrite_backup_tag=true
no_overwrite_backup_file=true
no_stage2_recomputed_backup_timestamp=true (rev4)
no_relations_body_grep_before_scan=true (rev4)
no_undeclared_substitution=true (rev5)
## Status
phase_status=PASS|PARTIAL|FAIL
rollback_executed=false (or true if separate phrase received)
rollback_recommended=true|false
next_required_pack=
- P3D4C2U_RESUME_NOTIFICATION_DISPLAY_PROMPT_REVIEW (if PASS)
- D28_DEPLOY_PARTIAL_FIX_PACK (if PARTIAL)
- D28_GENERATED_MAP_FIX (if FAIL with rollback approved)
- D28_PROD_HOTFIX (if FAIL without rollback)
Stage 1 outcome routing (rev5)
| Stage 1 outcome | Action |
|---|---|
PASS clean (undeclared_substitution_used=false, service_name_rule_applied=true, drift_detected=false, all 0A-0K PASS) |
Proceed to Stage 2 dispatch — compose approval phrase với 3 STAGE1 values |
PASS with undeclared substitution disclosed (undeclared_substitution_used=true) |
Revise prompt + re-run Stage 1 clean before Stage 2. TS từ run cũ NOT carry-forward. |
BLOCKED (drift_detected=true, agent stopped per NO_UNDECLARED_SUBSTITUTION) |
Investigate drift root cause, patch prompt, re-run Stage 1. |
| FAIL (any 0A-0K check) | Investigate root cause, no Stage 2 dispatch. |
Future improvements (non-blocking, rev6+ candidate)
-
Stage 1 0H healthcheck discovery enhancement: Hiện
grep -A3 'healthcheck:'có thể miss healthcheck declared trong context khác (nested, multi-line, base image inherit). Stage 2 rev1 (2026-05-10) phát hiện runtime healthy mà Stage 1 0H báo NONE.Suggested expansion:
- Thêm
docker inspect <container> --format '{{.State.Health.Status}}'trong Phase 0H sau khi capture compose ps. - Thêm
docker compose config(normalized YAML, post-merge) thay grep raw file.
Không block dispatch hiện tại. Codify khi có pack tương lai cần Stage 1 0H deep dive.
- Thêm
Failure handling matrix
| When | Status | Auto rollback? | Service impact |
|---|---|---|---|
| Phase 0 fail | STOP | N/A | None |
| Phase 0 verbatim command fails (e.g., "no such service") (rev5) | STOP <PHASE>_DRIFT, report drift, no substitution per NO_UNDECLARED_SUBSTITUTION |
N/A | None |
| Phase 1A approval/binding mismatch | STOP | N/A | None |
| Phase 1A STAGE1 values load fail | STOP | N/A | None |
| Phase 1A Stage 1 not rev5-clean (rev5) | STOP STAGE1_NOT_REV5_CLEAN |
N/A | None |
| Phase 1B drift | STOP | N/A | None |
| Phase 1C backup tag fail | STOP | N/A | None |
| Phase 1D build fail (BEFORE restart) | STOP, cleanup new image | No | None |
| Phase 1D build log SCAN=FAIL_LEAK | STOP, cleanup new image, no log content print | No | None |
| Phase 1E compose patch ambiguous | STOP, restore backup | N/A | None |
| Phase 1E compose diff SCAN=FAIL_LEAK (rev4) | STOP, restore backup, no diff print | N/A | None |
| Phase 1F restart fail | Recommend rollback | No (User decides) | Service down |
| Phase 1F up log SCAN=FAIL_LEAK | Recommend rollback, no log content print | No | Service ambiguous |
| Phase 1G not ready 60s | Recommend rollback | No | Service degraded |
| Phase 1H container log SCAN=FAIL_LEAK | Alert User, no log content print | No | Possible runtime leak |
| Phase 2 smoke 1-2 FAIL | Document, ask User | No | Some routes broken |
| Phase 2 smoke 3+ FAIL | Strong rollback recommend | No | Multiple routes broken |
| Phase 2 relations HTTP FAIL | CRITICAL rollback recommend | No | Server import broken |
| Phase 2 relations SCAN=FAIL_LEAK (rev4) | CRITICAL — no body inspection, alert User | No | Possible API data leak |
Decision matrix sau pack
| Stage 2 result | Next pack |
|---|---|
| PASS | P3D4C2U_RESUME_NOTIFICATION_DISPLAY_PROMPT_REVIEW |
| PARTIAL | D28_DEPLOY_PARTIAL_FIX_PACK |
| PARTIAL_PROMPT_DRIFT | D28_DEPLOY_PARTIAL_FIX_PACK (prompt patch + targeted re-smoke only) |
| FAIL with rollback | D28_GENERATED_MAP_FIX |
| FAIL without rollback | D28_PROD_HOTFIX (emergency) |
| FAIL_LEAK (compose diff or relations) | D28_LEAK_INVESTIGATION_PACK (offline review, no further deploy until resolved) |
D28 Deploy + Live Smoke Pack | Rev6 DRAFT | + Phase 2E URL template fix (?collection=workflow_steps) + Forensic diagnostic discipline + Decision matrix relations split | 2026-05-10