KB-19D0 rev 12

D28 — Deploy + Live Smoke Pack — Agent Prompt (REVIEW DRAFT Rev6)

37 min read Revision 12

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-nuxt container (~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 trong docker 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-nuxt SẼ FAIL với no such service: incomex-nuxt
  • Raw docker logs <container>, docker inspect <container>, docker ps — chấp nhận container name → có thể dùng incomex-nuxt nếu cần. Hiện rev5 KHÔNG dùng raw docker logs (vẫn dùng docker 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:

  1. STOP execution ngay tại phase đó.
  2. 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
    
  3. 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:

  1. Verbatim result preserved: Verbatim FAIL phải giữ nguyên trong official smoke count. Diagnostic KHÔNG được override smoke result.
  2. 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.
  3. Read-only: Diagnostic chỉ được là HTTP GET hoặc read-only DB query. KHÔNG mutation, KHÔNG side effect.
  4. 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).
  5. 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:

  1. Verify RUN_STAGE=2_DEPLOY_AND_SMOKE trong dispatch
  2. Read Stage 1 report from KB: knowledge/dev/laws/dieu28-trien-khai/reports/d28-deploy-and-live-smoke-stage1-preflight-report.md
  3. Verify Stage 1 preflight_status=PASSstatus=AWAITING_DEPLOY_APPROVAL
  4. Verify rev5 cleanliness (rev5):
    • service_name_rule_applied=true
    • undeclared_substitution_used=false
    • drift_detected=false
    • Mismatch any → STOP STAGE1_NOT_REV5_CLEAN
  5. Extract Stage 1's proposed_new_image_tag, proposed_backup_image_tag, proposed_compose_backup_path, current_production_image
  6. Verify approval phrase contains EXACT 3 values (NEW_TAG, BACKUP_TAG, COMPOSE_BACKUP_PATH) match parsed Stage 1 values
  7. Mismatch any → STOP APPROVAL_PHRASE_OR_BINDING_MISMATCH
  8. 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 -15 filtered 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)

  1. 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.


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