Files
platform-core/scripts/validate/test-observability-slack-risk-policy.sh

164 lines
7.4 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
# This closed policy check intentionally reports only labels and token counts.
readonly ROOT='/home/donghyeon/workspace/platform'
readonly CENTRAL_DOCS='/home/donghyeon/workspace/docs/platform'
readonly MASTER="$CENTRAL_DOCS/plans/2026-07-31-observability-phase4.md"
readonly ACCESS="$CENTRAL_DOCS/plans/2026-07-31-observability-access-alerting.md"
readonly MANUAL="$ROOT/bootstrap/manual/phase4-observability-access.md"
readonly WORKLOG="$CENTRAL_DOCS/runbooks/2026-08-13-observability-phase4-resume-worklog.md"
readonly HISTORICAL_PLAN="$CENTRAL_DOCS/plans/2026-08-11-gitea-metrics-checkpoint.md"
readonly HISTORICAL_DESIGN="$CENTRAL_DOCS/plans/2026-08-11-gitea-metrics-checkpoint-design.md"
failures=0
fail() {
printf 'FAIL: %s\n' "$1" >&2
failures=$((failures + 1))
}
require_file() {
[[ -f "$1" && ! -L "$1" ]] || fail "$2 is not a regular file"
}
count_token() {
local file=$1 token=$2
grep -Foc -- "$token" "$file" || true
}
require_count() {
local file=$1 label=$2 token=$3 expected=$4 actual
actual="$(count_token "$file" "$token")"
[[ "$actual" == "$expected" ]] || fail "$label token_count=$actual expected=$expected"
}
require_hash() {
local file=$1 label=$2 expected=$3 actual
actual="$(sha256sum -- "$file" | awk '{print $1}')"
[[ "$actual" == "$expected" ]] || fail "$label sha256=$actual expected=$expected"
}
require_execution_relationships() {
python3 - "$ACCESS" "$MANUAL" <<'PY'
import pathlib
import sys
access = pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')
manual = pathlib.Path(sys.argv[2]).read_text(encoding='utf-8')
def commands(text):
result = []
lines = text.splitlines()
index = 0
while index < len(lines):
line = lines[index]
if line.lstrip().startswith('bash scripts/bootstrap/create-observability-secrets.sh'):
start = index
parts = [line.strip()]
while parts[-1].rstrip().endswith('\\') and index + 1 < len(lines):
index += 1
parts.append(lines[index].strip())
result.append((start, ' '.join(parts)))
index += 1
return result
def slack_execute(items):
return [item for item in items if '--execute' in item[1] and '--slack-webhook' in item[1]]
def checker_after_count(text, command_index):
return sum(
index > command_index and '--check-slack-deployment-evidence' in line
for index, line in enumerate(text.splitlines())
)
failures = 0
def require(label, actual, expected):
global failures
if actual != expected:
print(f'FAIL: {label} count={actual} expected={expected}')
failures += 1
hostile = '''padding padding padding padding padding padding padding padding padding padding padding padding
--check-slack-deployment-evidence
bash scripts/bootstrap/create-observability-secrets.sh --execute --slack-webhook --accept-no-off-host-slack-recovery
'''
hostile_slack = slack_execute(commands(hostile))
require('hostile checker-before-Slack ordering', checker_after_count(hostile, hostile_slack[0][0]), 0)
step = '- [ ] **Step 3: 실행 순서를 manual 문서에 고정한다**'
start = access.find(step)
if start < 0:
require('authoritative access cutover block', 0, 1)
block = ''
else:
fence_start = access.find(' ```bash', start)
fence_end = access.find(' ```', fence_start + len(' ```bash'))
block = access[fence_start:fence_end] if fence_start >= 0 and fence_end >= 0 else ''
require('authoritative access cutover block', int(bool(block)), 1)
access_slack = slack_execute(commands(block))
require('authoritative access Slack execute command', len(access_slack), 1)
if len(access_slack) == 1:
require('authoritative access Slack risk flag', int('--accept-no-off-host-slack-recovery' in access_slack[0][1]), 1)
manual_slack = slack_execute(commands(manual))
require('manual Slack execute command', len(manual_slack), 1)
if len(manual_slack) == 1:
command_index, command = manual_slack[0]
require('manual Slack risk flag', int('--accept-no-off-host-slack-recovery' in command), 1)
require('manual deployment checker after Slack bootstrap', checker_after_count(manual, command_index), 1)
sys.exit(1 if failures else 0)
PY
}
require_file "$MASTER" master
require_file "$ACCESS" access
require_file "$MANUAL" manual
require_file "$WORKLOG" worklog
require_file "$HISTORICAL_PLAN" historical_plan
require_file "$HISTORICAL_DESIGN" historical_design
if (( failures == 0 )); then
require_count "$MASTER" 'master deployment gate' '--check-slack-deployment-evidence' 1
require_count "$ACCESS" 'access deployment gate' '--check-slack-deployment-evidence' 2
require_count "$ACCESS" 'access strict recovery gate' '--check-slack-recovery-evidence' 1
require_count "$MANUAL" 'manual deployment gate' '--check-slack-deployment-evidence' 1
require_count "$MANUAL" 'manual strict recovery gate' '--check-slack-recovery-evidence' 1
require_count "$MASTER" 'master strict DR-only wording' 'off-host Slack DR-complete 판정에만 사용' 1
require_count "$ACCESS" 'access strict DR-only wording' 'operational apply를 허용하지 않는다' 1
require_count "$MANUAL" 'manual risk-path DR-deferred wording' '이 경로는 DR-complete을 주장하지 않습니다' 1
require_count "$MANUAL" 'manual risk flag' '--accept-no-off-host-slack-recovery' 1
require_count "$MANUAL" 'manual exact risk confirmation' 'ACCEPT NO OFF-HOST SLACK RECOVERY default' 1
require_count "$MANUAL" 'manual kubectl prohibition' 'kubectl로 Secret을 수동 생성하지 않습니다' 1
require_count "$MANUAL" 'manual false recovery prohibition' '거짓 `RECOVERY SLACK default` 확인을 입력하지 않습니다' 1
require_count "$MANUAL" 'manual two deployment-gate calls' '두 번 호출' 1
require_count "$MANUAL" 'manual v2 acceptance schema' 'platform-observability-rules-alerts-v2' 1
require_count "$MANUAL" 'manual bare risk ledger value' '`RISK_ACCEPTED`' 1
require_count "$MANUAL" 'manual bare recovery ledger value' '`RECOVERY`' 1
require_count "$ACCESS" 'access risk flag order' '--accept-no-off-host-slack-recovery' 3
require_count "$MASTER" 'master operational acceptance status' 'Phase 4 operational acceptance: complete with active Slack DR exception' 1
require_count "$MASTER" 'master deferred DR status' 'Slack off-host disaster recovery: deferred / not complete' 1
require_count "$ACCESS" 'access operational acceptance status' 'Phase 4 operational acceptance: complete with active Slack DR exception' 1
require_count "$ACCESS" 'access deferred DR status' 'Slack off-host disaster recovery: deferred / not complete' 1
require_count "$WORKLOG" 'worklog user decision' 'Slack risk acceptance decision' 1
require_count "$WORKLOG" 'worklog KeePass deferred' 'KeePass work remains deferred' 1
require_count "$WORKLOG" 'worklog absent Slack Secret' 'alertmanager-slack-webhook Secret remains absent' 1
require_hash "$HISTORICAL_PLAN" 'historical plan byte preservation' '10ccc38835928304bd631a60680ecc8e6c1d565d51cbe7047169eb609aa38ce5'
require_hash "$HISTORICAL_DESIGN" 'historical design byte preservation' 'a9fd69e0241e5b56c5328076648a8ecf4a6f71d416aae7d53453ce7ff969249d'
if ! require_execution_relationships; then
failures=$((failures + 1))
fi
fi
if [[ -d "$ROOT/docs" ]]; then
fail 'platform docs boundary token_count=1 expected=0'
fi
if (( failures != 0 )); then
exit 1
fi
printf 'PASS: observability Slack risk policy token_counts=exact\n'