첫 커밋: 로컬 프로젝트 업로드

This commit is contained in:
2026-06-10 15:51:34 +09:00
commit 6a8dbeb2e9
1211 changed files with 312864 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() {
echo "ERROR: $*" >&2
exit 1
}
assert_contains() {
local file="$1"
local pattern="$2"
grep -Fq -- "$pattern" "$file" || fail "$file must contain: $pattern"
}
assert_not_contains() {
local file="$1"
local pattern="$2"
if grep -Fq -- "$pattern" "$file"; then
fail "$file must not contain: $pattern"
fi
}
for config in \
"$ROOT_DIR/adminfront/tailwind.config.ts" \
"$ROOT_DIR/devfront/tailwind.config.ts" \
"$ROOT_DIR/orgfront/tailwind.config.ts"
do
assert_not_contains "$config" "../common/**/*.{ts,tsx,css}"
assert_contains "$config" "../common/core/**/*.{ts,tsx}"
assert_contains "$config" "../common/shell/**/*.{ts,tsx}"
done
assert_contains "$ROOT_DIR/common/config/vite.base.ts" "/workspace/common"
assert_not_contains \
"$ROOT_DIR/adminfront/src/features/tenants/routes/TenantDetailPage.tsx" \
"export function canShowWorksmobileEntry"
assert_not_contains \
"$ROOT_DIR/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx" \
"export function createSchemaField"
assert_not_contains \
"$ROOT_DIR/adminfront/src/features/tenants/routes/TenantWorksmobilePage.tsx" \
"export function buildWorksmobilePasswordManageUrl"
assert_not_contains \
"$ROOT_DIR/adminfront/src/features/tenants/components/ParentTenantSelector.tsx" \
"export function filterParentTenants"
echo "OK: adminfront dev performance settings avoid wide scans and route HMR invalidation"

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
EXPECTED_FILES=(
"$ROOT_DIR/docker-compose.yaml"
"$ROOT_DIR/docker/staging_pull_compose.template.yaml"
"$ROOT_DIR/docker/docker-compose.staging.template.yaml"
)
for file in "${EXPECTED_FILES[@]}"; do
if [[ ! -f "$file" ]]; then
echo "ERROR: expected file not found: $file"
exit 1
fi
done
legacy_refs="$(grep -R -n '\${ADMIN_PORT:-' "${EXPECTED_FILES[@]}" || true)"
if [[ -n "$legacy_refs" ]]; then
echo "ERROR: legacy ADMIN_PORT references remain"
echo "$legacy_refs"
exit 1
fi
for file in "${EXPECTED_FILES[@]}"; do
if ! grep -q '\${ADMINFRONT_PORT:-5173}:5173' "$file"; then
echo "ERROR: ADMINFRONT_PORT mapping missing in $file"
exit 1
fi
done
echo "OK: AdminFront compose port policy uses ADMINFRONT_PORT only"

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RUNTIME_SCRIPT="$ROOT_DIR/adminfront/scripts/runtime-mode.sh"
from_admin_url="$(
APP_ENV=stage \
ADMINFRONT_URL=https://sadmin.hmac.kr \
sh "$RUNTIME_SCRIPT" --print-admin-public-url
)"
if [[ "$from_admin_url" != "https://sadmin.hmac.kr" ]]; then
echo "ERROR: ADMINFRONT_URL was not exported as VITE_ADMIN_PUBLIC_URL" >&2
exit 1
fi
from_callback="$(
APP_ENV=stage \
ADMINFRONT_CALLBACK_URLS=https://sadmin.hmac.kr/auth/callback \
sh "$RUNTIME_SCRIPT" --print-admin-public-url
)"
if [[ "$from_callback" != "https://sadmin.hmac.kr" ]]; then
echo "ERROR: ADMINFRONT_CALLBACK_URLS did not derive VITE_ADMIN_PUBLIC_URL" >&2
exit 1
fi
explicit_value="$(
APP_ENV=stage \
ADMINFRONT_URL=https://wrong.example.test \
VITE_ADMIN_PUBLIC_URL=https://sadmin.hmac.kr \
sh "$RUNTIME_SCRIPT" --print-admin-public-url
)"
if [[ "$explicit_value" != "https://sadmin.hmac.kr" ]]; then
echo "ERROR: explicit VITE_ADMIN_PUBLIC_URL should take precedence" >&2
exit 1
fi
echo "OK: AdminFront public URL env policy is stable"

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUTPUT_FILE="$ROOT_DIR/config/.generated/auth-config.env"
bash "$ROOT_DIR/scripts/auth_config.sh" build >/tmp/baron-auth-config-orgfront-test.log
orgfront_callbacks="$(grep -E '^ORGFRONT_CALLBACK_URLS=' "$OUTPUT_FILE" | cut -d= -f2- || true)"
if [[ -z "$orgfront_callbacks" ]]; then
echo "ERROR: generated auth config must include ORGFRONT_CALLBACK_URLS." >&2
exit 1
fi
first_orgfront_callback="${orgfront_callbacks%%,*}"
if [[ -z "$first_orgfront_callback" ]]; then
echo "ERROR: generated ORGFRONT_CALLBACK_URLS must not be empty." >&2
exit 1
fi
allowed_returns="$(grep -E '^KRATOS_ALLOWED_RETURN_URLS_JSON=' "$OUTPUT_FILE" | cut -d= -f2- || true)"
if ! grep -Fq "$first_orgfront_callback" <<<"$allowed_returns"; then
echo "ERROR: KRATOS_ALLOWED_RETURN_URLS_JSON must include orgfront callback: $first_orgfront_callback" >&2
exit 1
fi
echo "OK: auth config includes OrgFront callback URLs"

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TARGET_GO_VERSION="1.26.2"
GO_MOD="$ROOT_DIR/backend/go.mod"
BACKEND_DOCKERFILE="$ROOT_DIR/backend/Dockerfile"
LOCAL_COMPOSE="$ROOT_DIR/docker-compose.yaml"
STAGING_COMPOSE="$ROOT_DIR/docker/docker-compose.staging.template.yaml"
PULL_COMPOSE="$ROOT_DIR/docker/staging_pull_compose.template.yaml"
DEPLOY_TEMPLATE="$ROOT_DIR/deploy/templates/docker-compose.yaml"
README="$ROOT_DIR/README.md"
README_EN="$ROOT_DIR/README_en.md"
TEST_GUIDE="$ROOT_DIR/docs/TEST_GUIDE.md"
COMPLETION_REPORT="$ROOT_DIR/docs/개발완료보고서.md"
for file in \
"$GO_MOD" \
"$BACKEND_DOCKERFILE" \
"$LOCAL_COMPOSE" \
"$STAGING_COMPOSE" \
"$PULL_COMPOSE" \
"$DEPLOY_TEMPLATE" \
"$README" \
"$README_EN" \
"$TEST_GUIDE" \
"$COMPLETION_REPORT"
do
if [[ ! -f "$file" ]]; then
echo "ERROR: expected file not found: $file" >&2
exit 1
fi
done
if ! grep -Eq "^go ${TARGET_GO_VERSION}$" "$GO_MOD"; then
echo "ERROR: backend go.mod must use go ${TARGET_GO_VERSION}." >&2
exit 1
fi
if ! grep -Eq "^FROM golang:${TARGET_GO_VERSION}-alpine$" "$BACKEND_DOCKERFILE"; then
echo "ERROR: backend Dockerfile must use golang:${TARGET_GO_VERSION}-alpine." >&2
exit 1
fi
for file in "$LOCAL_COMPOSE" "$PULL_COMPOSE"; do
if ! grep -Fq "context: ./backend" "$file" && ! grep -Fq "context: ../../backend" "$file"; then
echo "ERROR: backend compose build context is missing in $file." >&2
exit 1
fi
done
for file in "$STAGING_COMPOSE" "$DEPLOY_TEMPLATE"; do
if ! grep -Eq "^[[:space:]]+backend:$" "$file"; then
echo "ERROR: backend service is missing in $file." >&2
exit 1
fi
done
legacy_refs="$(
grep -R -nE "golang:1\\.25|^go 1\\.25" \
"$ROOT_DIR/backend" \
"$ROOT_DIR/docker-compose.yaml" \
"$ROOT_DIR/docker" \
"$ROOT_DIR/deploy/templates" \
"$README" \
"$README_EN" \
"$TEST_GUIDE" \
"$COMPLETION_REPORT" || true
)"
if [[ -n "$legacy_refs" ]]; then
echo "ERROR: legacy backend Go version references remain." >&2
echo "$legacy_refs" >&2
exit 1
fi
echo "OK: backend Go base version policy is ${TARGET_GO_VERSION}"

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() {
echo "ERROR: $*" >&2
exit 1
}
assert_dry_run_contains() {
local output="$1"
local expected="$2"
grep -Fq -- "$expected" <<<"$output" || fail "dry-run output must contain: $expected"
}
grep -Fq "FROM debian:trixie-slim" "$repo_root/docker/backup-tools/Dockerfile" \
|| fail "backup-tools image must be based on debian:trixie-slim."
grep -Fq "zstd" "$repo_root/docker/backup-tools/Dockerfile" \
|| fail "backup-tools image must include zstd so restore does not depend on the host."
grep -Fq "docker-cli" "$repo_root/docker/backup-tools/Dockerfile" \
|| fail "backup-tools image must include docker CLI for containerized dump/restore orchestration."
grep -Fq "perl" "$repo_root/docker/backup-tools/Dockerfile" \
|| fail "backup-tools image must include perl for legacy ClickHouse schema dump decoding."
dump_dry_run="$(
make --dry-run --always-make -C "$repo_root" dump DUMP_SERVICES="postgres,config" DUMP_MODE="maintenance" 2>&1
)"
assert_dry_run_contains "$dump_dry_run" "docker build"
assert_dry_run_contains "$dump_dry_run" "docker run"
assert_dry_run_contains "$dump_dry_run" "baron-sso-backup-tools:local"
assert_dry_run_contains "$dump_dry_run" "--env-file .env"
assert_dry_run_contains "$dump_dry_run" "/var/run/docker.sock:/var/run/docker.sock"
assert_dry_run_contains "$dump_dry_run" "/tmp:/tmp"
assert_dry_run_contains "$dump_dry_run" "scripts/backup/dump.sh"
assert_dry_run_contains "$dump_dry_run" "DUMP_SERVICES=\"postgres,config\""
assert_dry_run_contains "$dump_dry_run" "DUMP_MODE=\"maintenance\""
restore_dry_run="$(
make --dry-run --always-make -C "$repo_root" restore BACKUP="backups/example" DUMP_FILE="backups/example.tar.zst" RESTORE_SERVICES="postgres,config" CONFIRM_RESTORE="baron-sso" RESTORE_REPORT="reports/restore-report.json" 2>&1
)"
assert_dry_run_contains "$restore_dry_run" "scripts/backup/restore.sh"
assert_dry_run_contains "$restore_dry_run" "docker run"
assert_dry_run_contains "$restore_dry_run" "BACKUP=\"backups/example\""
assert_dry_run_contains "$restore_dry_run" "DUMP_FILE=\"backups/example.tar.zst\""
assert_dry_run_contains "$restore_dry_run" "RESTORE_SERVICES=\"postgres,config\""
assert_dry_run_contains "$restore_dry_run" "CONFIRM_RESTORE=\"baron-sso\""
assert_dry_run_contains "$restore_dry_run" "RESTORE_REPORT=\"reports/restore-report.json\""
for target in dump-verify restore-verify dump-list restore-plan; do
target_dry_run="$(
make --dry-run --always-make -C "$repo_root" "$target" BACKUP="backups/example" 2>&1
)"
assert_dry_run_contains "$target_dry_run" "scripts/backup/"
done
upload_dry_run="$(
make --dry-run --always-make -C "$repo_root" upload-cloud BACKUP="backups/example" WORKS_DRIVE_DRY_RUN=true 2>&1
)"
assert_dry_run_contains "$upload_dry_run" "docker run"
assert_dry_run_contains "$upload_dry_run" "scripts/backup/upload_cloud.sh"
assert_dry_run_contains "$upload_dry_run" "BACKUP=\"backups/example\""
assert_dry_run_contains "$upload_dry_run" "WORKS_DRIVE_DRY_RUN=\"true\""
if make -C "$repo_root" BACKUP_USE_DOCKER=false restore >/tmp/baron-sso-restore-missing.out 2>&1; then
fail "make restore must fail when BACKUP and CONFIRM_RESTORE are not provided."
fi
if ! grep -Fq "CONFIRM_RESTORE=baron-sso" /tmp/baron-sso-restore-missing.out; then
fail "make restore failure must mention the required confirmation value."
fi
echo "OK: backup Makefile targets expose the expected guarded interface"

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() {
echo "ERROR: $*" >&2
exit 1
}
source "$repo_root/scripts/backup/lib/common.sh"
source "$repo_root/scripts/backup/lib/manifest.sh"
source "$repo_root/scripts/backup/lib/report.sh"
assert_eq() {
local expected="$1"
local actual="$2"
local message="$3"
[[ "$actual" == "$expected" ]] || fail "$message: expected '$expected', got '$actual'"
}
all_services="$(normalize_service_filter "all")"
assert_eq "postgres ory-postgres clickhouse ory-clickhouse config" "$all_services" "all must expand to every supported service"
selected_services="$(normalize_service_filter "postgres,config")"
assert_eq "postgres config" "$selected_services" "comma-separated services must be normalized in input order"
ory_services="$(normalize_service_filter "ory-postgres,ory-clickhouse")"
assert_eq "ory-postgres ory-clickhouse" "$ory_services" "ory service filter must not expand to Baron services"
if service_enabled "postgres" "$ory_services"; then
fail "service_enabled must not match postgres inside ory-postgres"
fi
if service_enabled "clickhouse" "$ory_services"; then
fail "service_enabled must not match clickhouse inside ory-clickhouse"
fi
service_enabled "ory-postgres" "$ory_services" || fail "service_enabled must match exact ory-postgres token"
service_enabled "ory-clickhouse" "$ory_services" || fail "service_enabled must match exact ory-clickhouse token"
grep -Fq "drop database if exists" "$repo_root/scripts/backup/lib/clickhouse.sh" \
|| fail "ClickHouse restore must drop restored databases before replaying schema/data to avoid duplicate rows or init-table conflicts."
grep -Fq "FORMAT RawBLOB" "$repo_root/scripts/backup/lib/clickhouse.sh" \
|| fail "ClickHouse dump must store SHOW CREATE TABLE as raw SQL, not escaped TSV."
grep -Fq "render_clickhouse_schema" "$repo_root/scripts/backup/lib/clickhouse.sh" \
|| fail "ClickHouse restore must route schema files through the compatibility decoder."
grep -Fq "s{\\\\n}" "$repo_root/scripts/backup/lib/clickhouse.sh" \
|| fail "ClickHouse restore must decode escaped newlines from older schema dumps."
grep -Fq "x27" "$repo_root/scripts/backup/lib/clickhouse.sh" \
|| fail "ClickHouse restore must decode escaped single quotes from older schema dumps."
grep -Fq "filter_clickhouse_stable_row_counts" "$repo_root/scripts/backup/restore.sh" \
|| fail "restore verification must not fail on unstable ClickHouse aggregate/view physical row counts."
grep -Fq "collect_clickhouse_native_stable_row_counts" "$repo_root/scripts/backup/restore.sh" \
|| fail "restore verification must derive ClickHouse expected counts from Native data files."
if normalize_service_filter "postgres,unknown" >/tmp/baron-sso-service-filter.out 2>&1; then
fail "unknown backup service must be rejected"
fi
if ! grep -Fq "unknown backup service" /tmp/baron-sso-service-filter.out; then
fail "unknown service rejection must explain the service filter problem"
fi
tmp_dir="$(mktemp -d /tmp/baron-sso-backup-policy.XXXXXX)"
trap 'rm -rf "$tmp_dir"' EXIT INT TERM
mkdir -p "$tmp_dir/reports"
create_manifest "$tmp_dir" "maintenance" "postgres config"
manifest="$tmp_dir/manifest.json"
[[ -f "$manifest" ]] || fail "create_manifest must write manifest.json"
grep -Fq '"format_version": "1"' "$manifest" || fail "manifest must include format_version"
grep -Fq '"mode": "maintenance"' "$manifest" || fail "manifest must include backup mode"
grep -Fq '"services": [' "$manifest" || fail "manifest must include service list"
grep -Fq '"git_commit":' "$manifest" || fail "manifest must include git commit"
cat >"$tmp_dir/reports/baron-postgres-row-counts.txt" <<'EOF'
public.users:228
public.tenants:266
public.relying_parties:1
EOF
cat >"$tmp_dir/reports/baron-postgres-custom-claim-counts.txt" <<'EOF'
public.rp_user_metadata:7
public.users.global_custom_claims:3
public.users.global_custom_claim_types:2
EOF
cat >"$tmp_dir/reports/ory_hydra-row-counts.txt" <<'EOF'
public.hydra_client:5
EOF
write_backup_markdown_report "$tmp_dir" "succeeded" "postgres config" '[{"service":"postgres","duration_seconds":2},{"service":"config","duration_seconds":1}]'
backup_md="$tmp_dir/reports/backup-report.md"
[[ -f "$backup_md" ]] || fail "backup markdown report must be created."
grep -Fq "# Baron SSO Backup Report" "$backup_md" || fail "backup report must have a markdown title."
grep -Fq "| 사용자 | 228 |" "$backup_md" || fail "backup report must include user count."
grep -Fq "| 테넌트 | 266 |" "$backup_md" || fail "backup report must include tenant count."
grep -Fq "| RP | 1 |" "$backup_md" || fail "backup report must include RP count."
grep -Fq "| RP 사용자 custom claim | 7 |" "$backup_md" || fail "backup report must include RP user custom claim count."
grep -Fq "| 전역 custom claim 사용자 | 3 |" "$backup_md" || fail "backup report must include global custom claim user count."
grep -Fq "| postgres | 2 |" "$backup_md" || fail "backup report must include service duration."
grep -Fq "baron-postgres-custom-claim-counts.txt" "$repo_root/scripts/backup/lib/postgres.sh" \
|| fail "Baron Postgres dump must collect custom claim backup verification counts."
grep -Fq "global_custom_claims" "$repo_root/scripts/backup/lib/postgres.sh" \
|| fail "Baron Postgres dump must count users.metadata.global_custom_claims for backup verification."
grep -Fq "rp_user_metadata" "$repo_root/scripts/backup/lib/postgres.sh" \
|| fail "Baron Postgres dump must count rp_user_metadata rows for backup verification."
printf 'original\n' >"$tmp_dir/example.txt"
(cd "$tmp_dir" && sha256sum example.txt > checksums.sha256)
printf 'changed\n' >"$tmp_dir/example.txt"
if BACKUP="$tmp_dir" "$repo_root/scripts/backup/verify-dump.sh" >/tmp/baron-sso-checksum.out 2>&1; then
fail "verify-dump must fail on checksum mismatch"
fi
if ! grep -Fq "checksum verification failed" /tmp/baron-sso-checksum.out; then
fail "checksum mismatch output must be explicit"
fi
if BACKUP="$tmp_dir" CONFIRM_RESTORE="baron-sso" RESTORE_TEST_NON_EMPTY=1 "$repo_root/scripts/backup/restore.sh" --dry-run >/tmp/baron-sso-non-empty-restore.out 2>&1; then
fail "restore must reject non-empty targets by default"
fi
if ! grep -Fq "non-empty restore target is not allowed" /tmp/baron-sso-non-empty-restore.out; then
fail "restore must explain the non-empty target guard"
fi
archive_source="$tmp_dir/archive-source"
mkdir -p "$archive_source/reports"
create_manifest "$archive_source" "maintenance" "postgres"
printf 'archive fixture\n' >"$archive_source/example.txt"
(cd "$archive_source" && sha256sum manifest.json example.txt > checksums.sha256)
archive_file="$tmp_dir/archive-source.tar.zst"
tar --zstd -cf "$archive_file" -C "$tmp_dir" archive-source
restore_report="$tmp_dir/restore-report.json"
DUMP_FILE="$archive_file" \
CONFIRM_RESTORE="baron-sso" \
RESTORE_REPORT="$restore_report" \
"$repo_root/scripts/backup/restore.sh" --dry-run >/tmp/baron-sso-dump-file-restore.out
[[ -f "$restore_report" ]] || fail "restore dry-run with DUMP_FILE must write RESTORE_REPORT."
jq -e \
--arg dump_file "$archive_file" \
'.status == "planned" and .dump_file == $dump_file and .backup_dir != null and (.services | index("postgres"))' \
"$restore_report" >/dev/null || fail "restore report must describe planned DUMP_FILE restore."
echo "OK: backup scripts enforce parser, manifest, checksum, and restore safety policies"

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() {
echo "ERROR: $*" >&2
exit 1
}
tmp_dir="$(mktemp -d /tmp/baron-sso-upload-cloud-test.XXXXXX)"
trap 'rm -rf "$tmp_dir"' EXIT INT TERM
backup_dir="$tmp_dir/baron-sso-backup-20260605-000000Z"
mkdir -p "$backup_dir/postgres" "$backup_dir/reports"
printf '{"format_version":"1"}\n' >"$backup_dir/manifest.json"
printf 'postgres dump fixture\n' >"$backup_dir/postgres/baron.dump"
printf '# Baron SSO Backup Report\n' >"$backup_dir/reports/backup-report.md"
(cd "$backup_dir" && sha256sum manifest.json postgres/baron.dump > checksums.sha256)
if "$repo_root/scripts/backup/upload_cloud.sh" >/tmp/baron-sso-upload-missing.out 2>&1; then
fail "upload_cloud.sh must require BACKUP."
fi
if ! grep -Fq "BACKUP is required" /tmp/baron-sso-upload-missing.out; then
fail "missing BACKUP error must be explicit."
fi
curl_log="$tmp_dir/curl.log"
fake_curl="$tmp_dir/fake-curl.sh"
fake_bin="$tmp_dir/bin"
mkdir -p "$fake_bin"
cat >"$fake_curl" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >>"${FAKE_CURL_LOG}"
last_arg="${!#}"
case "$last_arg" in
https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/folder-1/children)
printf '{"files":[]}'
;;
https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/folder-1/createfolder)
printf '{"fileId":"reports-folder-1","fileName":"reports","fileType":"FOLDER"}'
;;
https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/folder-1)
printf '{"uploadUrl":"https://upload.example.test/upload-1"}'
;;
https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/reports-folder-1)
printf '{"uploadUrl":"https://upload.example.test/upload-report-1"}'
;;
https://upload.example.test/upload-1)
printf '{"fileId":"file-1"}'
;;
https://upload.example.test/upload-report-1)
printf '{"fileId":"report-file-1"}'
;;
*)
echo "unexpected curl URL: $last_arg" >&2
exit 2
;;
esac
EOF
chmod +x "$fake_curl"
cat >"$fake_bin/zstd" <<'EOF'
#!/usr/bin/env bash
cat
EOF
chmod +x "$fake_bin/zstd"
WORKS_DRIVE_ACCESS_TOKEN="test-access-token" \
WORKS_DRIVE_TARGET="sharedrive" \
WORKS_DRIVE_SHARED_DRIVE_ID="shared-drive-1" \
WORKS_DRIVE_PARENT_FILE_ID="folder-1" \
WORKS_DRIVE_CURL_BIN="$fake_curl" \
FAKE_CURL_LOG="$curl_log" \
PATH="$fake_bin:$PATH" \
BACKUP="$backup_dir" \
"$repo_root/scripts/backup/upload_cloud.sh" >"$tmp_dir/upload.out"
grep -Fq "Upload complete" "$tmp_dir/upload.out" || fail "upload must complete with fake curl."
grep -Fq "sharedrives/shared-drive-1/files/folder-1" "$curl_log" || fail "must create upload URL for the configured shared drive folder."
grep -Fq "https://upload.example.test/upload-1" "$curl_log" || fail "must upload to the issued upload URL."
grep -Fq "Authorization: Bearer test-access-token" "$curl_log" || fail "must pass bearer token to WORKS API calls."
grep -Fq "Filedata=@" "$curl_log" || fail "must upload the packaged backup as multipart Filedata."
grep -Fq ".tar.zst" "$curl_log" || fail "backup directory uploads must be packaged as .tar.zst."
grep -Fq "createfolder" "$curl_log" || fail "must create or resolve a report subfolder."
grep -Fq "reports-folder-1" "$curl_log" || fail "must upload markdown reports to the reports folder."
grep -Eq "backup-report-[0-9]{8}-[0-9]{6}Z.md" "$curl_log" || fail "must upload timestamped backup markdown report."
if grep -Fq "cloud-upload.json" "$curl_log"; then
fail "cloud-upload.json must not be uploaded to WORKS Drive."
fi
report_file="$backup_dir/reports/cloud-upload.json"
[[ -f "$report_file" ]] || fail "upload must write reports/cloud-upload.json."
jq -e '.target == "sharedrive" and .files[0].status == "uploaded" and .report_files[0].status == "uploaded" and (.report_files[0].file_name | test("^backup-report-[0-9]{8}-[0-9]{6}Z[.]md$"))' "$report_file" >/dev/null || fail "upload report must include timestamped markdown report file status."
WORKS_DRIVE_DRY_RUN=true \
WORKS_DRIVE_TARGET="sharedrive" \
WORKS_DRIVE_SHARED_DRIVE_ID="shared-drive-1" \
WORKS_DRIVE_PARENT_FILE_ID="folder-1" \
PATH="$fake_bin:$PATH" \
BACKUP="$backup_dir" \
"$repo_root/scripts/backup/upload_cloud.sh" >"$tmp_dir/dry-run.out"
grep -Fq "Dry run" "$tmp_dir/dry-run.out" || fail "dry-run must not require a token or call curl."
echo "OK: upload_cloud uploads current backup artifacts to WORKS Drive"

View File

@@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORKFLOW_FILE="$ROOT_DIR/.gitea/workflows/code_check.yml"
FULL_NIGHTLY_WORKFLOW_FILE="$ROOT_DIR/.gitea/workflows/userfront_e2e_full_nightly.yml"
README_FILE="$ROOT_DIR/README.md"
fail() {
echo "ERROR: $*" >&2
exit 1
}
assert_contains() {
local file="$1"
local pattern="$2"
grep -Fq -- "$pattern" "$file" || fail "missing pattern in $file: $pattern"
}
assert_not_contains() {
local file="$1"
local pattern="$2"
if grep -Fq -- "$pattern" "$file"; then
fail "forbidden pattern in $file: $pattern"
fi
}
assert_contains "$WORKFLOW_FILE" "BADGE_BRANCH=badges"
assert_contains "$WORKFLOW_FILE" 'push origin HEAD:${BADGE_BRANCH}'
assert_contains "$WORKFLOW_FILE" 'BADGE_SOURCE_SHA: ${{ github.sha }}'
assert_contains "$WORKFLOW_FILE" 'BADGE_LATEST_DIR="${BADGE_WORKTREE}/latest"'
assert_contains "$WORKFLOW_FILE" 'BADGE_SHA_DIR="${BADGE_WORKTREE}/dev/${GITHUB_SHA}"'
assert_contains "$WORKFLOW_FILE" "Restore published badge state"
assert_contains "$WORKFLOW_FILE" "refs/remotes/origin/badges:latest/badges.json"
assert_contains "$WORKFLOW_FILE" "userfront-flutter-coverage:"
assert_contains "$WORKFLOW_FILE" "adminfront-vitest-coverage:"
assert_contains "$WORKFLOW_FILE" "devfront-vitest-coverage:"
assert_contains "$WORKFLOW_FILE" "orgfront-vitest-coverage:"
if grep -Eq "^[[:space:]]+front-vitest-coverage:$" "$WORKFLOW_FILE"; then
fail "Code Check workflow must use package-specific Vitest coverage jobs"
fi
assert_contains "$WORKFLOW_FILE" "ADMINFRONT_COVERAGE_RESULT: \${{ needs['adminfront-vitest-coverage'].result }}"
assert_contains "$WORKFLOW_FILE" "DEVFRONT_COVERAGE_RESULT: \${{ needs['devfront-vitest-coverage'].result }}"
assert_contains "$WORKFLOW_FILE" "ORGFRONT_COVERAGE_RESULT: \${{ needs['orgfront-vitest-coverage'].result }}"
assert_contains "$WORKFLOW_FILE" "USERFRONT_COVERAGE_RESULT: \${{ needs['userfront-flutter-coverage'].result }}"
assert_contains "$WORKFLOW_FILE" "name: userfront-flutter-coverage-report"
assert_contains "$WORKFLOW_FILE" "name: adminfront-vitest-coverage-report"
assert_contains "$WORKFLOW_FILE" "name: devfront-vitest-coverage-report"
assert_contains "$WORKFLOW_FILE" "name: orgfront-vitest-coverage-report"
if grep -Eq "^[[:space:]]+git push$" "$WORKFLOW_FILE"; then
fail "Code Check workflow must not push back to the current branch"
fi
assert_contains "$README_FILE" "https://gitea.hmac.kr/baron/baron-sso/raw/branch/badges/latest/code-check.svg"
assert_contains "$README_FILE" "https://gitea.hmac.kr/baron/baron-sso/raw/branch/badges/latest/dev-sha.svg"
assert_contains "$README_FILE" "https://gitea.hmac.kr/baron/baron-sso/raw/branch/badges/latest/backend-tests.svg"
assert_contains "$README_FILE" "https://gitea.hmac.kr/baron/baron-sso/raw/branch/badges/latest/userfront.svg"
assert_contains "$README_FILE" "https://gitea.hmac.kr/baron/baron-sso/raw/branch/badges/latest/adminfront.svg"
assert_contains "$README_FILE" "https://gitea.hmac.kr/baron/baron-sso/raw/branch/badges/latest/devfront.svg"
assert_contains "$README_FILE" "https://gitea.hmac.kr/baron/baron-sso/raw/branch/badges/latest/orgfront.svg"
assert_contains "$README_FILE" "https://gitea.hmac.kr/baron/baron-sso/raw/branch/badges/latest/userfront-chrome.svg"
assert_contains "$README_FILE" "https://gitea.hmac.kr/baron/baron-sso/raw/branch/badges/latest/userfront-firefox.svg"
assert_contains "$README_FILE" "https://gitea.hmac.kr/baron/baron-sso/raw/branch/badges/latest/userfront-safari.svg"
assert_not_contains "$README_FILE" "userfront-coverage.svg"
assert_not_contains "$README_FILE" "adminfront-coverage.svg"
assert_not_contains "$README_FILE" "adminfront-e2e.svg"
assert_not_contains "$README_FILE" "userfront-e2e-fast.svg"
assert_not_contains "$README_FILE" "userfront-e2e-full.svg"
assert_not_contains "$README_FILE" "](docs/badges/"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "cron: \"0 18 * * *\""
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "make code-check-lint"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "refs/remotes/origin/badges:dev/\${target_sha}/badges.json"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "full-result-exists:\${full_message}"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "USERFRONT_E2E_FULL: \"true\""
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "chromium-desktop"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "chromium-mobile-webapp"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "firefox-desktop"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "webkit-desktop"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "webkit-mobile-webapp"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "USERFRONT_E2E_CHROMIUM_DESKTOP_RESULT:"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "USERFRONT_E2E_CHROMIUM_MOBILE_RESULT:"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "USERFRONT_E2E_FIREFOX_DESKTOP_RESULT:"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "USERFRONT_E2E_FIREFOX_MOBILE_RESULT:"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "USERFRONT_E2E_WEBKIT_DESKTOP_RESULT:"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "USERFRONT_E2E_WEBKIT_MOBILE_RESULT:"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "BADGE_UPDATE_CODE_CHECK: \"false\""
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "Restore published badge state"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "refs/remotes/origin/badges:latest/badges.json"
assert_contains "$FULL_NIGHTLY_WORKFLOW_FILE" "npx playwright test"
echo "OK: Code Check badges are published to the badges branch"

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORKFLOW_FILE="$ROOT_DIR/.gitea/workflows/code_check.yml"
fail() {
echo "ERROR: $*" >&2
exit 1
}
grep -Fq -- "scripts/playwrightPackageVersion.cjs" "$WORKFLOW_FILE" || \
fail "Code Check workflow must compute Playwright cache versions through the shared package.json reader"
if grep -Fq -- "npm list @playwright/test" "$WORKFLOW_FILE"; then
fail "Code Check workflow must not call npm list before npm ci"
fi
if grep -Fq -- "pnpm list -C ../common @playwright/test" "$WORKFLOW_FILE"; then
fail "Code Check workflow must not call pnpm list before pnpm install"
fi
grep -Fq -- "run_userfront_e2e_full:" "$WORKFLOW_FILE" || \
fail "Code Check workflow must expose a manual full userfront-e2e switch"
grep -Fq -- "USERFRONT_E2E_FULL:" "$WORKFLOW_FILE" || \
fail "Code Check workflow must map the full userfront-e2e input into the job environment"
grep -Fq -- "npx playwright install --with-deps chromium" "$WORKFLOW_FILE" || \
fail "Userfront E2E fast lane must provision Chromium only"
grep -Fq -- "npm test -- --project=chromium-desktop --project=chromium-mobile-webapp" "$WORKFLOW_FILE" || \
fail "Userfront E2E fast lane must run only Chromium desktop and mobile projects"
echo "OK: Code Check Playwright cache keys do not depend on preinstalled node_modules"

View File

@@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
assert_contains() {
local file="$1"
local pattern="$2"
if ! grep -Fq -- "$pattern" "$file"; then
echo "ERROR: missing pattern in $file: $pattern" >&2
exit 1
fi
}
assert_public_url() {
local app_dir="$1"
local app_url_env="$2"
local callback_env="$3"
local vite_public_env="$4"
local expected_url="$5"
local runtime_script="$ROOT_DIR/$app_dir/scripts/runtime-mode.sh"
assert_contains "$runtime_script" "--print-public-url"
local from_app_url
from_app_url="$(
env APP_ENV=stage \
"$app_url_env=$expected_url" \
sh "$runtime_script" --print-public-url
)"
if [[ "$from_app_url" != "$expected_url" ]]; then
echo "ERROR: $app_url_env was not exported as $vite_public_env for $app_dir" >&2
exit 1
fi
local from_callback
from_callback="$(
env APP_ENV=stage \
"$callback_env=$expected_url/auth/callback" \
sh "$runtime_script" --print-public-url
)"
if [[ "$from_callback" != "$expected_url" ]]; then
echo "ERROR: $callback_env did not derive $vite_public_env for $app_dir" >&2
exit 1
fi
local explicit_value
explicit_value="$(
env APP_ENV=stage \
"$app_url_env=https://wrong.example.test" \
"$vite_public_env=$expected_url" \
sh "$runtime_script" --print-public-url
)"
if [[ "$explicit_value" != "$expected_url" ]]; then
echo "ERROR: explicit $vite_public_env should take precedence for $app_dir" >&2
exit 1
fi
}
assert_contains "$ROOT_DIR/devfront/src/lib/authConfig.ts" "DEVFRONT_AUTH_CALLBACK_PATH"
assert_contains "$ROOT_DIR/devfront/src/app/routes.tsx" "DEVFRONT_AUTH_CALLBACK_PATH"
assert_contains "$ROOT_DIR/devfront/src/lib/auth.ts" "VITE_DEVFRONT_PUBLIC_URL"
assert_public_url \
"devfront" \
"DEVFRONT_URL" \
"DEVFRONT_CALLBACK_URLS" \
"VITE_DEVFRONT_PUBLIC_URL" \
"https://sdev.hmac.kr"
assert_contains "$ROOT_DIR/orgfront/src/lib/authConfig.ts" "ORGFRONT_AUTH_CALLBACK_PATH"
assert_contains "$ROOT_DIR/orgfront/src/app/routes.tsx" "ORGFRONT_AUTH_CALLBACK_PATH"
assert_contains "$ROOT_DIR/orgfront/src/lib/auth.ts" "VITE_ORGFRONT_PUBLIC_URL"
assert_public_url \
"orgfront" \
"ORGFRONT_URL" \
"ORGFRONT_CALLBACK_URLS" \
"VITE_ORGFRONT_PUBLIC_URL" \
"https://sorg.hmac.kr"
echo "OK: DevFront/OrgFront callback public URL policy is stable"

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
env_file="$repo_root/.env"
gitignore_file="$repo_root/.gitignore"
if [[ -f "$env_file" ]] && grep -q -- "-----BEGIN PRIVATE KEY-----" "$env_file"; then
echo "ERROR: .env must not contain a multi-line PEM private key; put it under config/ and reference WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE." >&2
exit 1
fi
if [[ -f "$env_file" ]] && ! grep -q '^WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE=' "$env_file"; then
echo "ERROR: .env must reference WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE." >&2
exit 1
fi
if ! grep -Eq '(^|/)config/\*\.pem$' "$gitignore_file"; then
echo "ERROR: .gitignore must ignore config/*.pem secret files." >&2
exit 1
fi
make --dry-run --always-make -C "$repo_root" dev DEV_SERVICES="backend adminfront" >/dev/null

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_FILE="$ROOT_DIR/docker-compose.yaml"
USERFRONT_DOCKERFILE="$ROOT_DIR/userfront/Dockerfile"
USERFRONT_DEV_SERVER="$ROOT_DIR/userfront/scripts/dev-server.sh"
fail() {
echo "ERROR: $*" >&2
exit 1
}
assert_contains() {
local pattern="$1"
grep -Fq -- "$pattern" "$COMPOSE_FILE" || fail "docker-compose.yaml must contain: $pattern"
}
assert_not_contains() {
local pattern="$1"
if grep -Fq -- "$pattern" "$COMPOSE_FILE"; then
fail "docker-compose.yaml must not contain stale frontend mount: $pattern"
fi
}
for app in adminfront devfront orgfront; do
assert_contains "./$app:/workspace/$app"
assert_contains "/workspace/$app/node_modules"
assert_not_contains "./$app:/app"
done
assert_contains 'target: ${USERFRONT_BUILD_TARGET:-dev}'
assert_contains "./userfront/lib:/workspace/userfront/lib"
assert_contains "./userfront/assets:/workspace/userfront/assets"
assert_contains "./userfront/web:/workspace/userfront/web"
assert_contains "./userfront/scripts:/workspace/userfront/scripts:ro"
assert_contains "./scripts:/workspace/scripts:ro"
assert_contains "./locales:/workspace/locales:ro"
grep -Fq -- "AS dev" "$USERFRONT_DOCKERFILE" || fail "userfront Dockerfile must define a dev build target"
grep -Fq -- "AS production" "$USERFRONT_DOCKERFILE" || fail "userfront Dockerfile must keep an explicit production target"
grep -Fq -- "flutter run" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must use flutter run"
grep -Fq -- "--wasm" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must keep WebAssembly enabled"
grep -Fq -- "--dart-define=BACKEND_URL=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass backend URL through dart-define"
grep -Fq -- "--dart-define=CLIENT_LOG_DEBUG=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass client log debug mode through dart-define"
grep -Fq -- "--dart-define=APP_ENV=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass app env through dart-define"
grep -Fq -- "--dart-define=USERFRONT_URL=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass userfront URL through dart-define"
grep -Fq -- 'USERFRONT_FLUTTER_RUN_FLAGS' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must accept optional Flutter run flags"
grep -Fq -- 'USERFRONT_FLUTTER_RUN_FLAGS="${USERFRONT_FLUTTER_RUN_FLAGS:---debug}"' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must keep Flutter debug mode as the default"
grep -Fq -- 'warm_userfront_once' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must run a one-shot boot warmup"
grep -Fq -- 'rm -rf build/web' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must remove stale build/web before flutter run"
grep -Fq -- 'USERFRONT_BOOT_BUILD_MARKER' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must track the current flutter run build start"
grep -Fq -- 'USERFRONT_BOOT_LOG' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must capture flutter run output"
grep -Fq -- 'wait_for_userfront_serve_log' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must wait for Flutter serve completion log"
grep -Fq -- 'lib/main.dart is being served at' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must start only after Flutter serves main.dart"
grep -Fq -- 'tee "$USERFRONT_BOOT_LOG"' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must preserve flutter output while tracking serve readiness"
grep -Fq -- '-nt "$USERFRONT_BOOT_BUILD_MARKER"' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must only accept build/web from the current flutter run"
grep -Fq -- 'USERFRONT_BOOT_WARMUP_LOCALES' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must declare the language matrix"
grep -Fq -- 'USERFRONT_BOOT_WARMUP_VIEWPORTS' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must declare the viewport matrix"
grep -Fq -- 'Accept-Language:' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must GET each route with the locale header"
grep -Fq -- 'Viewport-Width:' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must GET each route with viewport width metadata"
grep -Fq -- '/signin' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must cover signin routes"
grep -Fq -- '/flutter_bootstrap.js' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must load the Flutter bootstrap entrypoint"
grep -Fq -- 'warm_flutter_bootstrap_assets' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must discover hashed Flutter assets from flutter_bootstrap.js"
if grep -Fq -- '/main.dart.mjs' "$USERFRONT_DEV_SERVER" || grep -Fq -- '/main.dart.wasm' "$USERFRONT_DEV_SERVER"; then
fail "userfront boot warmup must not use stale unversioned Flutter wasm entrypoint paths"
fi
if grep -Fq -- 'client.navigate' "$USERFRONT_DEV_SERVER"; then
fail "userfront dev service worker reset must not navigate clients during Flutter bootstrap"
fi
grep -Fq -- 'disable_userfront_dev_service_worker' "$USERFRONT_DEV_SERVER" || fail "userfront dev boot must disable Flutter service worker navigation loops"
warmup_timer_line="$(grep -nF 'started_at="$(date +%s)"' "$USERFRONT_DEV_SERVER" | cut -d: -f1)"
warmup_start_log_line="$(grep -nF 'one-shot warmup starting' "$USERFRONT_DEV_SERVER" | cut -d: -f1)"
http_readiness_skip_line="$(grep -nF 'readiness attempts' "$USERFRONT_DEV_SERVER" | tail -n 1 | cut -d: -f1)"
if [ "$warmup_timer_line" -le "$http_readiness_skip_line" ] || [ "$warmup_timer_line" -ge "$warmup_start_log_line" ]; then
fail "userfront warmup timer must start after current Flutter server readiness and immediately before warmup start log"
fi
assert_contains 'CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false}'
assert_contains 'BACKEND_URL=${BACKEND_URL:-}'
assert_contains 'USERFRONT_URL=${USERFRONT_URL}'
assert_contains 'USERFRONT_FLUTTER_RUN_FLAGS=${USERFRONT_FLUTTER_RUN_FLAGS:-}'
if grep -Fq -- "while true" "$USERFRONT_DEV_SERVER"; then
fail "userfront boot warmup must not run as a periodic health check"
fi
if grep -Fq -- "--release" "$USERFRONT_DEV_SERVER"; then
fail "userfront dev server must not run Flutter in release mode"
fi
assert_contains "./common:/workspace/common"
assert_contains "/workspace/common/node_modules"
assert_contains "./locales:/workspace/locales"
for runtime in \
"$ROOT_DIR/adminfront/scripts/runtime-mode.sh" \
"$ROOT_DIR/devfront/scripts/runtime-mode.sh" \
"$ROOT_DIR/orgfront/scripts/runtime-mode.sh"
do
grep -Fq -- "/workspace/common" "$runtime" || fail "$runtime must install dependencies from /workspace/common"
grep -Fq -- "pnpm install --filter" "$runtime" || fail "$runtime must install only its workspace slice"
grep -Fq -- "--frozen-lockfile --ignore-scripts" "$runtime" || fail "$runtime must preserve the workspace lockfile with pnpm"
if grep -Fq -- "npm install --no-workspaces" "$runtime"; then
fail "$runtime must not install common dependencies outside the workspace graph"
fi
done
echo "OK: frontend dev containers bind-mount source into Dockerfile WORKDIR paths"

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env sh
set -eu
assert_contains() {
file="$1"
pattern="$2"
if ! grep -Fq "$pattern" "$file"; then
echo "missing pattern in $file: $pattern" >&2
exit 1
fi
}
assert_not_contains() {
file="$1"
pattern="$2"
if grep -Fq "$pattern" "$file"; then
echo "forbidden pattern in $file: $pattern" >&2
exit 1
fi
}
deploy_gateway="deploy/templates/gateway/nginx.conf"
if [ ! -f "$deploy_gateway" ]; then
echo "missing expected file: $deploy_gateway" >&2
exit 1
fi
assert_contains "$deploy_gateway" "root /usr/share/nginx/html;"
assert_contains "$deploy_gateway" 'try_files $uri $uri/ /index.html;'
assert_not_contains "$deploy_gateway" "baron_userfront"
assert_not_contains "$deploy_gateway" "userfront_upstream"
assert_not_contains "$deploy_gateway" "proxy_pass http://baron_userfront"
echo "gateway userfront residue policy checks passed"

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env sh
set -eu
schema_file="docker/ory/kratos/identity.schema.json"
forbidden_traits="hanmacFamily userType"
for trait in $forbidden_traits; do
if grep -Fq "\"$trait\"" "$schema_file"; then
echo "forbidden Kratos trait in $schema_file: $trait" >&2
exit 1
fi
done
echo "kratos identity schema policy checks passed"

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
policy_doc="$repo_root/docs/identity-redis-mirror-policy-2026-06-09.md"
allowed_files="$(
cat <<'EOF'
backend/cmd/adminctl/main.go
backend/cmd/fix_kratos_roles.go
backend/internal/bootstrap/admin_account.go
backend/internal/bootstrap/kratos_seed.go
backend/internal/handler/auth_handler.go
backend/internal/handler/user_handler.go
backend/internal/service/kratos_admin_service.go
backend/internal/service/ory_service.go
backend/internal/service/user_group_service.go
scripts/clear_orphan_tenant_memberships.sh
EOF
)"
pattern='admin/identities|KratosAdmin[.]UpdateIdentity|KratosAdmin[.]DeleteIdentity|kratos[.]UpdateIdentity|kratosAdmin[.]UpdateIdentity|identityAdmin[.]CreateUser|identityAdmin[.]UpdateIdentityPassword|idp[.]CreateUser|IdpProvider[.]CreateUser|IdpProvider[.]UpdateUserPassword|OryProvider[.]CreateUser|OryProvider[.]UpdateUserPassword|UPDATE identities'
findings="$(
git -C "$repo_root" grep -nE "$pattern" -- \
backend \
scripts \
':(exclude)backend/**/*_test.go' \
':(exclude)scripts/backup' || true
)"
unexpected=""
if [ -n "$findings" ]; then
while IFS= read -r finding; do
file="${finding%%:*}"
if ! grep -Fxq "$file" <<<"$allowed_files"; then
unexpected="${unexpected}${finding}"$'\n'
fi
done <<<"$findings"
fi
if [ -n "$unexpected" ]; then
echo "ERROR: undocumented Kratos identity write path detected." >&2
printf '%s' "$unexpected" >&2
exit 1
fi
if [ ! -f "$policy_doc" ]; then
echo "ERROR: missing identity Redis mirror policy document: $policy_doc" >&2
exit 1
fi
if grep -Fq "admin/identities/" "$repo_root/backend/internal/handler/auth_handler.go"; then
echo "ERROR: auth_handler must not call Kratos admin identity endpoints directly; use KratosAdminService or IdentityWriteService." >&2
exit 1
fi
if ! grep -Fq "maintenance-window" "$repo_root/backend/cmd/fix_kratos_roles.go" || \
! grep -Fq "dry-run" "$repo_root/backend/cmd/fix_kratos_roles.go" || \
! grep -Fq "mark-mirror-stale" "$repo_root/backend/cmd/fix_kratos_roles.go"; then
echo "ERROR: fix_kratos_roles must require dry-run/maintenance-window/mark-mirror-stale guards." >&2
exit 1
fi
if ! grep -Fq "CONFIRM_KRATOS_DB_MAINTENANCE" "$repo_root/scripts/clear_orphan_tenant_memberships.sh" || \
! grep -Fq "MARK_IDENTITY_MIRROR_STALE" "$repo_root/scripts/clear_orphan_tenant_memberships.sh"; then
echo "ERROR: clear_orphan_tenant_memberships.sh must require explicit Kratos DB maintenance and mirror-stale guards." >&2
exit 1
fi
if ! grep -Fq "## Kratos write 경로 감사" "$policy_doc"; then
echo "ERROR: policy document must include the Kratos write path audit section." >&2
exit 1
fi
while IFS= read -r file; do
if [ -n "$file" ] && ! grep -Fq "$file" "$policy_doc"; then
echo "ERROR: allowed Kratos write path is not documented: $file" >&2
exit 1
fi
done <<<"$allowed_files"
echo "kratos identity write path policy checks passed"

View File

@@ -0,0 +1,196 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
dry_run_default_dev="$(
make --dry-run --always-make -C "$repo_root" dev 2>&1
)"
default_app_up_line="$(
grep -E "docker compose .* -f docker-compose.yaml up .*backend.*adminfront.*devfront.*orgfront.*userfront" <<<"$dry_run_default_dev" | tail -1
)"
if [[ -z "$default_app_up_line" ]]; then
echo "make dev must include orgfront in the default development app services." >&2
exit 1
fi
dry_run_dev="$(
make --dry-run --always-make -C "$repo_root" dev DEV_SERVICES="backend adminfront" 2>&1
)"
if ! grep -q "Ensuring Infra stack" <<<"$dry_run_dev"; then
echo "make dev must ensure the infra stack first." >&2
exit 1
fi
if ! grep -q "Ensuring Ory stack" <<<"$dry_run_dev"; then
echo "make dev must ensure the Ory stack first." >&2
exit 1
fi
if ! grep -q "Rendering Ory config" <<<"$dry_run_dev"; then
echo "make dev must render Ory config before starting services." >&2
exit 1
fi
app_up_line="$(
grep -E "docker compose .* -f docker-compose.yaml up .*backend.*adminfront" <<<"$dry_run_dev" | tail -1
)"
if [[ -z "$app_up_line" ]]; then
echo "make dev must run docker compose up for development app services." >&2
exit 1
fi
if grep -q -- " -d" <<<"$app_up_line"; then
echo "make dev must run app services in foreground attach mode without -d." >&2
exit 1
fi
if ! grep -q -- " --build" <<<"$app_up_line"; then
echo "make dev must rebuild app service images before starting development containers." >&2
exit 1
fi
if ! grep -q -- "BACKEND_LOG_LEVEL=info" <<<"$app_up_line"; then
echo "make dev must run backend at info log level." >&2
exit 1
fi
if ! grep -q -- "CLIENT_LOG_DEBUG=false" <<<"$app_up_line"; then
echo "make dev must disable verbose client debug log ingestion." >&2
exit 1
fi
if ! grep -q -- "VITE_CLIENT_LOG_DEBUG=false" <<<"$app_up_line"; then
echo "make dev must disable React client debug console logs." >&2
exit 1
fi
if grep -q -- "BACKEND_LOG_LEVEL=debug" <<<"$app_up_line"; then
echo "make dev must not run backend at debug log level." >&2
exit 1
fi
if grep -q -- "USERFRONT_FLUTTER_RUN_FLAGS=--debug" <<<"$app_up_line"; then
echo "make dev must not run userfront with explicit Flutter debug flags." >&2
exit 1
fi
dry_run_dev_debug="$(
make --dry-run --always-make -C "$repo_root" dev-debug DEV_SERVICES="backend userfront" 2>&1
)"
if ! grep -q "Ensuring Infra stack" <<<"$dry_run_dev_debug"; then
echo "make dev-debug must ensure the infra stack first." >&2
exit 1
fi
if ! grep -q "Ensuring Ory stack" <<<"$dry_run_dev_debug"; then
echo "make dev-debug must ensure the Ory stack first." >&2
exit 1
fi
dev_debug_app_up_line="$(
grep -E "BACKEND_LOG_LEVEL=debug CLIENT_LOG_DEBUG=true VITE_CLIENT_LOG_DEBUG=true USERFRONT_FLUTTER_RUN_FLAGS=--debug docker compose .* -f docker-compose.yaml up .*backend.*userfront" <<<"$dry_run_dev_debug" | tail -1
)"
if [[ -z "$dev_debug_app_up_line" ]]; then
echo "make dev-debug must run app services with explicit backend, client log, and userfront debug flags." >&2
exit 1
fi
dry_run_up_dev="$(
make --dry-run --always-make -C "$repo_root" up-dev 2>&1
)"
if ! grep -q "Ensuring Infra stack" <<<"$dry_run_up_dev"; then
echo "make up-dev must ensure the infra stack." >&2
exit 1
fi
if ! grep -q "Ensuring Ory stack" <<<"$dry_run_up_dev"; then
echo "make up-dev must ensure the Ory stack." >&2
exit 1
fi
dry_run_up_app="$(
make --dry-run --always-make -C "$repo_root" up-app 2>&1
)"
if ! grep -q "Starting App stack (backend/userfront/adminfront/devfront/orgfront)" <<<"$dry_run_up_app"; then
echo "make up-app must announce orgfront as part of the app stack." >&2
exit 1
fi
if ! grep -q "Rendering Ory config" <<<"$dry_run_up_app"; then
echo "make up-app must render Ory config before starting services." >&2
exit 1
fi
up_app_line="$(
grep -E "docker compose .* -f docker-compose.yaml up .*backend.*adminfront.*devfront.*orgfront.*userfront|docker compose .* -f docker-compose.yaml up " <<<"$dry_run_up_app" | tail -1
)"
if ! grep -q -- " --build" <<<"$up_app_line"; then
echo "make up-app must rebuild app service images before starting containers." >&2
exit 1
fi
dry_run_up_all="$(
make --dry-run --always-make -C "$repo_root" up-all 2>&1
)"
if ! dry_run_up="$(
make --dry-run --always-make -C "$repo_root" up 2>&1
)"; then
echo "make up must be available as the default full-stack startup target." >&2
echo "$dry_run_up" >&2
exit 1
fi
if ! grep -q "Starting ALL stacks (infra + ory + app)" <<<"$dry_run_up"; then
echo "make up must delegate to the full-stack startup flow." >&2
exit 1
fi
if ! grep -q "config/.generated/auth-config.env" <<<"$dry_run_up"; then
echo "make up must use generated env from config/.generated." >&2
exit 1
fi
if ! grep -q "Rendering Ory config" <<<"$dry_run_up"; then
echo "make up must render Ory config before compose up." >&2
exit 1
fi
if ! grep -q "Ensuring Docker networks" <<<"$dry_run_up_all"; then
echo "make up-all must ensure external Docker networks before compose up." >&2
exit 1
fi
if ! grep -q 'docker network create "$network"' <<<"$dry_run_up_all"; then
echo "make up-all must create missing external Docker networks." >&2
exit 1
fi
dry_run_drop="$(
make --dry-run --always-make -C "$repo_root" drop 2>&1
)"
if ! grep -q "Dropping Baron SSO local Docker stack" <<<"$dry_run_drop"; then
echo "make drop must announce that it is dropping the local stack." >&2
exit 1
fi
if ! grep -q -- "down -v --rmi local" <<<"$dry_run_drop"; then
echo "make drop must remove containers, volumes, and local compose images." >&2
exit 1
fi
if ! grep -q "docker rm -f" <<<"$dry_run_drop"; then
echo "make drop must force-remove known fixed-name stack containers." >&2
exit 1
fi

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env bash
set -euo pipefail
require_container() {
local name="$1"
if ! docker inspect "$name" >/dev/null 2>&1; then
echo "ERROR: required container is missing: $name" >&2
exit 1
fi
}
for container in ory_oathkeeper ory_vector ory_clickhouse baron_backend; do
require_container "$container"
done
vector_state="$(docker inspect -f '{{.State.Status}}' ory_vector)"
if [[ "$vector_state" != "running" ]]; then
echo "ERROR: ory_vector must be running, got: $vector_state" >&2
docker logs --tail 100 ory_vector >&2 || true
exit 1
fi
before_lines="$(docker exec ory_oathkeeper sh -lc 'test -f /var/log/oathkeeper/access.log && wc -l < /var/log/oathkeeper/access.log || echo 0')"
before_rows="$(docker exec ory_clickhouse clickhouse-client --user "${ORY_CLICKHOUSE_USER:-ory}" --password "${ORY_CLICKHOUSE_PASSWORD:-orypass}" --query "SELECT count() FROM ory.oathkeeper_access_logs")"
docker run --rm --network public_net curlimages/curl:8.10.1 \
-fsS http://ory_oathkeeper:4455/health >/dev/null
deadline=$((SECONDS + 20))
after_lines="$before_lines"
while (( SECONDS < deadline )); do
after_lines="$(docker exec ory_oathkeeper sh -lc 'test -f /var/log/oathkeeper/access.log && wc -l < /var/log/oathkeeper/access.log || echo 0')"
if (( after_lines > before_lines )); then
break
fi
sleep 1
done
if (( after_lines <= before_lines )); then
echo "ERROR: Oathkeeper access log did not grow after a proxied request." >&2
docker exec ory_oathkeeper sh -lc 'ls -l /var/log/oathkeeper && tail -n 50 /var/log/oathkeeper/access.log 2>/dev/null || true' >&2
exit 1
fi
deadline=$((SECONDS + 30))
after_rows="$before_rows"
while (( SECONDS < deadline )); do
after_rows="$(docker exec ory_clickhouse clickhouse-client --user "${ORY_CLICKHOUSE_USER:-ory}" --password "${ORY_CLICKHOUSE_PASSWORD:-orypass}" --query "SELECT count() FROM ory.oathkeeper_access_logs")"
if (( after_rows > before_rows )); then
break
fi
sleep 2
done
if (( after_rows <= before_rows )); then
echo "ERROR: Vector did not insert the new Oathkeeper access log into ClickHouse." >&2
echo "before_rows=$before_rows after_rows=$after_rows" >&2
docker logs --tail 100 ory_vector >&2 || true
exit 1
fi
before_auth_ts="$(docker exec ory_clickhouse clickhouse-client --user "${ORY_CLICKHOUSE_USER:-ory}" --password "${ORY_CLICKHOUSE_PASSWORD:-orypass}" --query "SELECT now64(3)")"
auth_status="$(docker run --rm --network public_net curlimages/curl:8.10.1 \
-sS -o /dev/null -w '%{http_code}' \
'http://ory_oathkeeper:4455/oauth2/auth?client_id=orgfront&redirect_uri=http%3A%2F%2Flocalhost%3A5175%2Fauth%2Fcallback&response_type=code&scope=openid&state=access-log-e2e&code_challenge=accessloge2e&code_challenge_method=S256')"
if [[ "$auth_status" != "302" ]]; then
echo "ERROR: expected Oathkeeper OIDC auth request to return 302, got: $auth_status" >&2
exit 1
fi
deadline=$((SECONDS + 30))
completed_rows=0
granted_rows=0
while (( SECONDS < deadline )); do
completed_rows="$(docker exec ory_clickhouse clickhouse-client --user "${ORY_CLICKHOUSE_USER:-ory}" --password "${ORY_CLICKHOUSE_PASSWORD:-orypass}" --query "
SELECT count()
FROM ory.oathkeeper_access_logs
WHERE timestamp >= toDateTime64('$before_auth_ts', 3)
AND method = 'GET'
AND path = '/oauth2/auth'
AND status = 302
")"
granted_rows="$(docker exec ory_clickhouse clickhouse-client --user "${ORY_CLICKHOUSE_USER:-ory}" --password "${ORY_CLICKHOUSE_PASSWORD:-orypass}" --query "
SELECT count()
FROM ory.oathkeeper_access_logs
WHERE timestamp >= toDateTime64('$before_auth_ts', 3)
AND method = 'GET'
AND path = '/oauth2/auth'
AND client_id = 'orgfront'
AND decision = 'granted'
")"
if (( completed_rows > 0 && granted_rows > 0 )); then
break
fi
sleep 2
done
if (( completed_rows <= 0 )); then
echo "ERROR: Oathkeeper completed request log did not preserve method/path/status." >&2
docker exec ory_clickhouse clickhouse-client --user "${ORY_CLICKHOUSE_USER:-ory}" --password "${ORY_CLICKHOUSE_PASSWORD:-orypass}" --query "
SELECT timestamp, method, path, status, client_id, decision
FROM ory.oathkeeper_access_logs
WHERE timestamp >= toDateTime64('$before_auth_ts', 3)
ORDER BY timestamp DESC
LIMIT 20
FORMAT Vertical
" >&2 || true
exit 1
fi
if (( granted_rows <= 0 )); then
echo "ERROR: Oathkeeper granted request log did not preserve client_id." >&2
docker exec ory_clickhouse clickhouse-client --user "${ORY_CLICKHOUSE_USER:-ory}" --password "${ORY_CLICKHOUSE_PASSWORD:-orypass}" --query "
SELECT timestamp, method, path, status, client_id, decision
FROM ory.oathkeeper_access_logs
WHERE timestamp >= toDateTime64('$before_auth_ts', 3)
ORDER BY timestamp DESC
LIMIT 20
FORMAT Vertical
" >&2 || true
exit 1
fi

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
failures=0
rule_files=()
while IFS= read -r file; do
rule_files+=("$file")
done < <(find \
"$repo_root/docker/ory/oathkeeper" \
"$repo_root/config/.generated/ory/oathkeeper" \
-maxdepth 1 -name 'rules*.json' -print | sort)
for file in "${rule_files[@]}"; do
if grep -Eq '"id"[[:space:]]*:[[:space:]]*"kratos-public"' "$file"; then
echo "ERROR: $file must not define a public Kratos proxy rule." >&2
failures=$((failures + 1))
fi
if grep -Eq '"url"[[:space:]]*:[[:space:]]*"[^"]*/kratos/<\.\*>"' "$file"; then
echo "ERROR: $file must not expose Kratos under /kratos." >&2
failures=$((failures + 1))
fi
if grep -Eq '"url"[[:space:]]*:[[:space:]]*"http://kratos:4433"' "$file"; then
echo "ERROR: $file must not proxy public requests directly to kratos:4433." >&2
failures=$((failures + 1))
fi
done
for compose_file in \
"$repo_root/compose.ory.yaml" \
"$repo_root/docker/compose.ory.yaml" \
"$repo_root/docker/staging_pull_compose.template.yaml" \
"$repo_root/deploy/templates/docker-compose.yaml"
do
kratos_block="$(
awk '
/^[[:space:]]+kratos:/ { in_block=1; print; next }
in_block && /^[[:space:]]+[A-Za-z0-9_-]+:/ { exit }
in_block { print }
' "$compose_file"
)"
if grep -Eq '^[[:space:]]+ports:' <<<"$kratos_block"; then
echo "ERROR: $compose_file must not publish Kratos ports directly." >&2
failures=$((failures + 1))
fi
done
if [[ "$failures" -gt 0 ]]; then
exit 1
fi
echo "OK: Kratos public API is not exposed through Oathkeeper rules or compose ports."

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
assert_contains() {
local file="$1"
local pattern="$2"
if ! grep -Fq -- "$pattern" "$file"; then
echo "ERROR: missing pattern in $file: $pattern" >&2
exit 1
fi
}
assert_not_contains() {
local file="$1"
local pattern="$2"
if grep -Fq -- "$pattern" "$file"; then
echo "ERROR: forbidden pattern remains in $file: $pattern" >&2
exit 1
fi
}
LOCAL_COMPOSE="$ROOT_DIR/docker-compose.yaml"
STAGING_COMPOSE="$ROOT_DIR/docker/docker-compose.staging.template.yaml"
PULL_COMPOSE="$ROOT_DIR/docker/staging_pull_compose.template.yaml"
DEPLOY_TEMPLATE="$ROOT_DIR/deploy/templates/docker-compose.yaml"
BUILD_RC="$ROOT_DIR/.gitea/workflows/build_RC.yml"
CODE_CHECK="$ROOT_DIR/.gitea/workflows/code_check.yml"
STAGING_RELEASE="$ROOT_DIR/.gitea/workflows/staging_release.yml"
STAGING_PULL="$ROOT_DIR/.gitea/workflows/staging_code_pull.yml"
ORGFRONT_VITE="$ROOT_DIR/orgfront/vite.config.ts"
ORGFRONT_RUNTIME="$ROOT_DIR/orgfront/scripts/runtime-mode.sh"
for file in \
"$LOCAL_COMPOSE" \
"$STAGING_COMPOSE" \
"$PULL_COMPOSE" \
"$DEPLOY_TEMPLATE" \
"$BUILD_RC" \
"$CODE_CHECK" \
"$STAGING_RELEASE" \
"$STAGING_PULL" \
"$ORGFRONT_VITE" \
"$ORGFRONT_RUNTIME"
do
if [[ ! -f "$file" ]]; then
echo "ERROR: expected file not found: $file" >&2
exit 1
fi
done
assert_contains "$LOCAL_COMPOSE" "dockerfile: ./orgfront/Dockerfile"
assert_contains "$LOCAL_COMPOSE" "./orgfront:/workspace/orgfront"
assert_contains "$LOCAL_COMPOSE" "VITE_ORGFRONT_PUBLIC_URL: \${ORGFRONT_URL}"
assert_contains "$LOCAL_COMPOSE" "VITE_OIDC_CLIENT_ID: orgfront"
assert_not_contains "$LOCAL_COMPOSE" "../baron-orgchart"
for file in "$STAGING_COMPOSE" "$PULL_COMPOSE" "$DEPLOY_TEMPLATE"; do
assert_contains "$file" "orgfront:"
assert_contains "$file" "ORGFRONT_PORT"
done
for file in "$STAGING_COMPOSE" "$PULL_COMPOSE"; do
assert_contains "$file" "API_PROXY_TARGET=http://baron_backend:3000"
done
assert_contains "$STAGING_COMPOSE" 'image: ${ORGFRONT_IMAGE_NAME}:${IMAGE_TAG}'
assert_contains "$PULL_COMPOSE" "context: ."
assert_contains "$PULL_COMPOSE" "dockerfile: ./orgfront/Dockerfile"
assert_contains "$PULL_COMPOSE" "VITE_ORGFRONT_PUBLIC_URL: \${ORGFRONT_URL:-}"
assert_not_contains "$PULL_COMPOSE" "./orgfront:/app"
assert_contains "$DEPLOY_TEMPLATE" "dockerfile: ./orgfront/Dockerfile"
assert_contains "$DEPLOY_TEMPLATE" "VITE_ORGFRONT_PUBLIC_URL: \${ORGFRONT_URL}"
assert_not_contains "$DEPLOY_TEMPLATE" "../../orgfront:/app"
assert_not_contains "$DEPLOY_TEMPLATE" "./orgfront/vite.config.ts:/app/vite.config.ts:ro"
assert_not_contains "$DEPLOY_TEMPLATE" "./orgfront/auth.ts:/app/src/lib/auth.ts:ro"
assert_contains "$BUILD_RC" "Build and push orgfront RC image"
assert_contains "$BUILD_RC" "context: ."
assert_contains "$BUILD_RC" "file: ./orgfront/Dockerfile"
assert_contains "$BUILD_RC" "/baron_sso/orgfront:"
assert_contains "$CODE_CHECK" "run_orgfront_tests"
assert_contains "$CODE_CHECK" "cd orgfront"
assert_contains "$CODE_CHECK" "pnpm install -C ../common --no-frozen-lockfile"
assert_contains "$CODE_CHECK" "pnpm run test"
assert_contains "$STAGING_RELEASE" "ORGFRONT_IMAGE_NAME"
assert_contains "$STAGING_RELEASE" "ORGFRONT_PORT="
assert_contains "$STAGING_RELEASE" "ORGFRONT_CALLBACK_URLS="
assert_contains "$STAGING_RELEASE" "export ORGFRONT_IMAGE_NAME="
assert_contains "$STAGING_RELEASE" "ORGFRONT_URL="
assert_not_contains "$STAGING_RELEASE" "VITE_ORGCHART_URL="
assert_contains "$STAGING_PULL" "ORGFRONT_PORT="
assert_contains "$STAGING_PULL" "ORGFRONT_CALLBACK_URLS="
assert_contains "$STAGING_PULL" "ORGFRONT_URL="
assert_not_contains "$STAGING_PULL" "VITE_ORGCHART_URL="
assert_contains "$ORGFRONT_VITE" "baron-orgchart.hmac.kr"
assert_not_contains "$ORGFRONT_VITE" "VITE_ORGCHART_URL"
assert_contains "$ORGFRONT_RUNTIME" "npm run dev -- --host 0.0.0.0 --port 5175"
assert_contains "$ORGFRONT_RUNTIME" "npm run preview -- --host 0.0.0.0 --port 5175"
assert_contains "$ROOT_DIR/adminfront/vite.config.ts" 'envPrefix: ["VITE_", "USERFRONT_", "ORGFRONT_"]'
assert_contains "$ROOT_DIR/deploy/templates/adminfront/vite.config.ts" 'envPrefix: ["VITE_", "USERFRONT_", "ORGFRONT_"]'
legacy_orgchart_refs="$(grep -R -n "VITE_ORGCHART_URL" \
"$ROOT_DIR/adminfront/src" \
"$ROOT_DIR/adminfront/vite.config.ts" \
"$ROOT_DIR/deploy/templates/adminfront/vite.config.ts" \
"$ROOT_DIR/orgfront/vite.config.ts" \
"$STAGING_PULL" \
"$STAGING_RELEASE" || true)"
if [[ -n "$legacy_orgchart_refs" ]]; then
echo "ERROR: legacy VITE_ORGCHART_URL references remain"
echo "$legacy_orgchart_refs"
exit 1
fi
echo "OK: OrgFront compose, CI, and staging deployment policy is wired"

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
fail() {
echo "[org-context-chart-package] $*" >&2
exit 1
}
assert_contains() {
local file="$1"
local needle="$2"
grep -Fq "$needle" "$file" || fail "$file must contain: $needle"
}
assert_contains orgfront/package.json "build:org-context-chart:min"
assert_contains orgfront/package.json "scripts/build-org-context-chart.mjs"
assert_contains orgfront/scripts/build-org-context-chart.mjs "ORG_CONTEXT_CHART_BUILD_ID"
assert_contains orgfront/vite.org-context-chart.config.ts "ORG_CONTEXT_CHART_MINIFY"
assert_contains orgfront/vite.org-context-chart.config.ts "ORG_CONTEXT_CHART_BUILD_ID"
assert_contains orgfront/vite.org-context-chart.config.ts "boc-"
assert_contains orgfront/vite.org-context-chart.config.ts ".min"
assert_contains orgfront/scripts/build-org-context-chart.mjs 'return `${year}${month}${random}`;'
assert_contains orgfront/vite.org-context-chart.config.ts 'return `${year}${month}${random}`;'
if grep -Fq '${year}${month}${day}' orgfront/scripts/build-org-context-chart.mjs orgfront/vite.org-context-chart.config.ts; then
fail "OrgContext chart build id must use YYMM plus 4 random digits, without day or separators"
fi
echo "OK: OrgContext chart package emits timestamped short bundle names"

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
# Kratos/Hydra admin endpoints should be reachable only on ory-net.
# Frontend network (baron_net) must NOT reach admin endpoints.
IMAGE="curlimages/curl:8.10.1"
# ory-net should succeed
# 한국어: ory-net에서는 admin 포트 접근이 가능해야 함
docker run --rm --network ory-net "$IMAGE" -fsS http://hydra:4445/health/ready > /dev/null
docker run --rm --network ory-net "$IMAGE" -fsS http://kratos:4434/health/ready > /dev/null
# baron_net should fail
# 한국어: baron_net에서는 admin 포트 접근이 불가능해야 함
if docker run --rm --network baron_net "$IMAGE" -fsS http://hydra:4445/health/ready > /dev/null 2>&1; then
echo "ERROR: hydra admin is reachable from baron_net"
exit 1
fi
if docker run --rm --network baron_net "$IMAGE" -fsS http://kratos:4434/health/ready > /dev/null 2>&1; then
echo "ERROR: kratos admin is reachable from baron_net"
exit 1
fi
echo "OK: admin endpoints are reachable on ory-net only"

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
"$repo_root/scripts/render_ory_config.sh" >/dev/null
docker run --rm \
-e ORY_CLICKHOUSE_USER=ory \
-e ORY_CLICKHOUSE_PASSWORD=orypass \
-v "$repo_root/docker/ory/vector:/etc/vector:ro" \
timberio/vector:0.36.0-alpine validate --no-environment /etc/vector/vector.toml >/dev/null
if grep -q '/etc/config/oathkeeper/rules.active.json' "$repo_root/docker/ory/oathkeeper/entrypoint.sh"; then
echo "ERROR: Oathkeeper entrypoint must not write active rules into the bind-mounted config directory." >&2
exit 1
fi
if ! grep -q 'file:///tmp/oathkeeper/rules.active.json' "$repo_root/config/.generated/ory/oathkeeper/oathkeeper.yml"; then
echo "ERROR: Oathkeeper config must load active rules from writable runtime storage." >&2
exit 1
fi
if ! grep -q '^version: v26.2.0$' "$repo_root/config/.generated/ory/kratos/kratos.yml"; then
echo "ERROR: Kratos config version must match the v26.2.0 runtime." >&2
exit 1
fi
cookie_secret="$(grep -E '^COOKIE_SECRET=' "$repo_root/.env" | cut -d= -f2-)"
if [[ ${#cookie_secret} -ne 32 ]]; then
echo "ERROR: COOKIE_SECRET must be exactly 32 bytes/chars for backend encryptcookie." >&2
exit 1
fi
root_config="$(
docker compose --env-file "$repo_root/.env" -f "$repo_root/compose.ory.yaml" config
)"
if ! grep -q "oathkeeper_logs_init:" <<<"$root_config"; then
echo "ERROR: compose.ory.yaml must initialize the Oathkeeper log volume permissions." >&2
exit 1
fi

View File

@@ -0,0 +1,411 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
root_config="$(
docker compose --env-file "$repo_root/.env" -f "$repo_root/compose.ory.yaml" config
)"
docker_config="$(
docker compose --env-file "$repo_root/.env" -f "$repo_root/docker/compose.ory.yaml" config
)"
override_env="$(mktemp)"
grep -Ev '^(USERFRONT_URL|HYDRA_PUBLIC_URL|KRATOS_UI_URL|KRATOS_BROWSER_URL|ADMINFRONT_CALLBACK_URLS|DEVFRONT_CALLBACK_URLS|ORGFRONT_CALLBACK_URLS)=' "$repo_root/.env" >"$override_env"
cat >> "$override_env" <<'EOF'
USERFRONT_URL=https://compose-policy.example.test/sso
HYDRA_PUBLIC_URL=https://compose-policy.example.test/sso/oidc
KRATOS_UI_URL=https://compose-policy.example.test/ui
KRATOS_BROWSER_URL=https://compose-policy.example.test/auth
ADMINFRONT_CALLBACK_URLS=https://compose-policy.example.test/admin/callback
DEVFRONT_CALLBACK_URLS=https://compose-policy.example.test/dev/callback
ORGFRONT_CALLBACK_URLS=https://compose-policy.example.test/org/callback
EOF
trap 'rm -f "$override_env"' EXIT
override_config="$(
docker compose --env-file "$override_env" -f "$repo_root/compose.ory.yaml" config
)"
override_docker_config="$(
docker compose --env-file "$override_env" -f "$repo_root/docker/compose.ory.yaml" config
)"
for service in kratos hydra keto oathkeeper; do
version_key="$(tr '[:lower:]' '[:upper:]' <<<"$service")_VERSION"
expected_version="$(grep -E "^${version_key}=" "$repo_root/.env" | cut -d= -f2-)"
if [[ -z "$expected_version" ]]; then
echo "ERROR: $version_key must be set in .env" >&2
exit 1
fi
if ! grep -q "image: oryd/${service}:${expected_version}" <<<"$root_config"; then
echo "ERROR: compose.ory.yaml must render oryd/${service}:${expected_version}" >&2
exit 1
fi
done
if grep -q "oryd/hydra:v25.4.0" <<<"$root_config"; then
echo "ERROR: compose.ory.yaml must not hard-code init-rp to hydra v25.4.0." >&2
exit 1
fi
for compose_file in "$repo_root/compose.ory.yaml" "$repo_root/docker/compose.ory.yaml"; do
if grep -Eq 'redirect-uri .*:-.*https?://' "$compose_file"; then
echo "ERROR: $compose_file must not hard-code external redirect URI fallbacks; use .env variables." >&2
exit 1
fi
if grep -Eq 'KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=.*https?://localhost' "$compose_file"; then
echo "ERROR: $compose_file must not hard-code Kratos allowed return URL fallbacks; use .env variables." >&2
exit 1
fi
if awk 'in_block && /^ [A-Za-z0-9_-]+:/ { exit } /^ init-rp:/ { in_block=1 } in_block { print }' "$compose_file" | grep -q -- '--endpoint http://hydra:4445'; then
echo "ERROR: $compose_file init-rp must use HYDRA_ADMIN_URL instead of hard-coded Hydra admin endpoint." >&2
exit 1
fi
if awk 'in_block && /^ [A-Za-z0-9_-]+:/ { exit } /^[[:space:]]+oathkeeper:/ { in_block=1 } in_block { print }' "$compose_file" | grep -q "command: serve proxy --config /etc/config/oathkeeper/oathkeeper.yml"; then
echo "ERROR: $compose_file Oathkeeper must use entrypoint.sh instead of bypassing rules.active.json generation." >&2
exit 1
fi
done
for stack_check_file in \
"$repo_root/compose.ory.yaml" \
"$repo_root/docker/compose.ory.yaml" \
"$repo_root/docker/staging_pull_compose.template.yaml" \
"$repo_root/deploy/templates/docker-compose.yaml"
do
if grep -q 'until curl -s http://' "$stack_check_file"; then
echo "ERROR: Ory stack check must not wait forever; use bounded readiness checks in $stack_check_file." >&2
exit 1
fi
if ! grep -q 'ORY_STACK_CHECK_MAX_ATTEMPTS' "$stack_check_file"; then
echo "ERROR: Ory stack check must expose ORY_STACK_CHECK_MAX_ATTEMPTS in $stack_check_file." >&2
exit 1
fi
if ! grep -q 'ERROR: Ory service not ready' "$stack_check_file"; then
echo "ERROR: Ory stack check must report the failed service name in $stack_check_file." >&2
exit 1
fi
if ! grep -q 'check_ready kratos .* || exit 1' "$stack_check_file"; then
echo "ERROR: Ory stack check must raise a non-zero exit when Kratos is not ready in $stack_check_file." >&2
exit 1
fi
done
for expected_url in \
"https://compose-policy.example.test/sso/oidc" \
"https://compose-policy.example.test/sso/login" \
"https://compose-policy.example.test/sso/consent" \
"https://compose-policy.example.test/sso/error" \
"https://compose-policy.example.test/admin/callback" \
"https://compose-policy.example.test/dev/callback" \
"https://compose-policy.example.test/org/callback"
do
if ! grep -q "$expected_url" <<<"$override_config$override_docker_config"; then
echo "ERROR: Ory compose config must render env override URL: $expected_url" >&2
exit 1
fi
done
root_init_rp="$(
awk 'in_block && /^ [A-Za-z0-9_-]+:/ { exit } /^ init-rp:/ { in_block=1 } in_block { print }' "$repo_root/compose.ory.yaml"
)"
docker_init_rp="$(
awk 'in_block && /^ [A-Za-z0-9_-]+:/ { exit } /^ init-rp:/ { in_block=1 } in_block { print }' "$repo_root/docker/compose.ory.yaml"
)"
for init_rp_file in \
"$repo_root/compose.ory.yaml" \
"$repo_root/docker/compose.ory.yaml" \
"$repo_root/docker/staging_pull_compose.template.yaml" \
"$repo_root/deploy/templates/docker-compose.yaml"
do
if ! grep -Fq 'image: oryd/hydra:${HYDRA_CLI_VERSION:-v26.2.0}' "$init_rp_file"; then
echo "ERROR: init-rp must use the official Hydra CLI image with HYDRA_CLI_VERSION in $init_rp_file." >&2
exit 1
fi
if ! grep -Fq 'entrypoint: ["/bin/sh", "-ec"]' "$init_rp_file"; then
echo "ERROR: init-rp must override the Hydra image entrypoint with /bin/sh -ec in $init_rp_file." >&2
exit 1
fi
if grep -Fq 'HYDRA_CLI_ARCHIVE_VERSION' "$init_rp_file" || grep -Fq 'hydra.tar.gz' "$init_rp_file"; then
echo "ERROR: init-rp must not download Hydra CLI tarballs at runtime in $init_rp_file." >&2
exit 1
fi
done
if grep -q "image: alpine:latest" <<<"$root_init_rp$docker_init_rp"; then
echo "ERROR: init-rp must not use alpine plus runtime Hydra CLI download." >&2
exit 1
fi
if ! grep -q "migrate sql up" "$repo_root/compose.ory.yaml"; then
echo "ERROR: compose.ory.yaml Kratos migration must use migrate sql up." >&2
exit 1
fi
if ! grep -q "keto-migrate:" <<<"$docker_config"; then
echo "ERROR: docker/compose.ory.yaml must include keto-migrate for clean Ory installs." >&2
exit 1
fi
if grep -q "releases/download/v25.4.0" "$repo_root/docker/staging_pull_compose.template.yaml"; then
echo "ERROR: staging pull compose must not download a hard-coded Hydra v25.4.0 CLI." >&2
exit 1
fi
staging_pull_template="$repo_root/docker/staging_pull_compose.template.yaml"
if ! grep -q 'entrypoint: \["/etc/config/oathkeeper/entrypoint.sh"\]' "$staging_pull_template"; then
echo "ERROR: staging pull Oathkeeper must use the env-aware entrypoint." >&2
exit 1
fi
if grep -q "command: serve proxy --config /etc/config/oathkeeper/oathkeeper.yml" "$staging_pull_template"; then
echo "ERROR: staging pull Oathkeeper must not bypass entrypoint.sh with a direct command." >&2
exit 1
fi
if ! grep -q "URLS_SELF_ISSUER=\${HYDRA_PUBLIC_URL}" "$staging_pull_template"; then
echo "ERROR: staging pull Hydra issuer must use HYDRA_PUBLIC_URL." >&2
exit 1
fi
if grep -Eq '(KRATOS_(SERVE|SELFSERVICE|UI|BROWSER|PUBLIC|ADMIN).*:-http://localhost|URLS_.*:-http://localhost)' "$staging_pull_template"; then
echo "ERROR: staging pull Ory browser URLs must not fall back to localhost." >&2
exit 1
fi
if ! grep -q 'KRATOS_SELFSERVICE_ALLOWED_RETURN_URLS=${KRATOS_ALLOWED_RETURN_URLS_JSON' "$staging_pull_template"; then
echo "ERROR: staging pull Kratos allowed_return_urls must be driven by KRATOS_ALLOWED_RETURN_URLS_JSON." >&2
exit 1
fi
for return_path in '/ko' '/en' '/auth/callback' '/ko/auth/callback' '/en/auth/callback'; do
if ! grep -q "$return_path" "$staging_pull_template" "$repo_root/deploy/templates/.env.template" "$repo_root/.gitea/workflows/staging_code_pull.yml"; then
echo "ERROR: staging/prod allowed_return_urls must include locale/callback path: $return_path" >&2
exit 1
fi
done
if grep -Eq 'ORGFRONT_CALLBACK_URLS=.*(172\.16\.10\.176|baron-orgchart\.hmac\.kr|, https?://)' "$staging_pull_template" "$repo_root/.gitea/workflows/staging_code_pull.yml"; then
echo "ERROR: staging pull OrgFront callbacks must not keep private IP, legacy orgchart domain, or comma-space URI entries." >&2
exit 1
fi
if grep -q "rewrite \\^/oidc" "$repo_root/gateway/nginx.conf"; then
echo "ERROR: gateway must preserve the /oidc prefix and let Oathkeeper strip it." >&2
exit 1
fi
for rules_file in \
"$repo_root/docker/ory/oathkeeper/rules.json" \
"$repo_root/docker/ory/oathkeeper/rules.stage.json" \
"$repo_root/docker/ory/oathkeeper/rules.prod.json"
do
for rule_id in hydra-well-known hydra-well-known-oidc hydra-oauth2 hydra-oauth2-oidc hydra-userinfo hydra-userinfo-oidc; do
if ! grep -q "\"id\": \"$rule_id\"" "$rules_file"; then
echo "ERROR: Oathkeeper rules must expose Hydra public route in $rules_file: $rule_id" >&2
exit 1
fi
done
for prefixed_rule in hydra-well-known-oidc hydra-oauth2-oidc hydra-userinfo-oidc; do
if ! awk -v id="\"id\": \"$prefixed_rule\"" '
$0 ~ id { in_rule = 1 }
in_rule && /strip_path/ && /\/oidc/ { found = 1 }
in_rule && /^ }[,]?$/ { in_rule = 0 }
END { exit found ? 0 : 1 }
' "$rules_file"; then
echo "ERROR: prefixed Oathkeeper route must strip /oidc in $rules_file: $prefixed_rule" >&2
exit 1
fi
done
done
for wildcard_rules_file in \
"$repo_root/docker/ory/oathkeeper/rules.json" \
"$repo_root/docker/ory/oathkeeper/rules.stage.json"
do
if grep -q "<\\.\\*>://<\\.\\*>/" "$wildcard_rules_file"; then
echo "ERROR: wildcard Oathkeeper host must not swallow path segments in $wildcard_rules_file." >&2
exit 1
fi
done
deploy_template="$repo_root/deploy/templates/docker-compose.yaml"
deploy_env_template="$repo_root/deploy/templates/.env.template"
deploy_gateway_template="$repo_root/deploy/templates/gateway/nginx.conf"
deploy_kratos_template="$repo_root/deploy/templates/ory/kratos/kratos.yml.template"
deploy_oathkeeper_rules_template="$repo_root/deploy/templates/ory/oathkeeper/rules.json"
for required_template in \
"$repo_root/deploy/templates/orgfront/vite.config.ts" \
"$repo_root/deploy/templates/orgfront/auth.ts" \
"$repo_root/docker/ory/init-db/01_create_dbs.sh" \
"$repo_root/docker/ory/hydra/hydra.yml.template" \
"$repo_root/docker/ory/keto/keto.yml.template" \
"$repo_root/docker/ory/oathkeeper/entrypoint.sh" \
"$repo_root/docker/ory/oathkeeper/oathkeeper.yml.template"
do
if [[ ! -f "$required_template" ]]; then
echo "ERROR: deploy instance generation requires missing source file: $required_template" >&2
exit 1
fi
done
if grep -Eq "oryd/(kratos|hydra|keto|oathkeeper):v25\\.4\\.0" "$deploy_template"; then
echo "ERROR: deploy template Ory stack must not hard-code v25.4.0 images." >&2
exit 1
fi
for prod_sensitive_file in \
"$repo_root/docker/ory/oathkeeper/rules.prod.json" \
"$repo_root/docker/ory/kratos/kratos.yml.template" \
"$repo_root/deploy/templates/ory/kratos/kratos.yml.template"
do
if grep -q "app\\.brsw\\.kr" "$prod_sensitive_file"; then
echo "ERROR: Ory production-sensitive config must not hard-code app.brsw.kr: $prod_sensitive_file" >&2
exit 1
fi
done
for compose_file in "$repo_root/compose.ory.yaml" "$repo_root/docker/compose.ory.yaml" "$repo_root/docker/staging_pull_compose.template.yaml"; do
if grep -Eq './docker/ory/(kratos|hydra|keto|oathkeeper):/etc/config/' "$compose_file"; then
echo "ERROR: Ory compose must mount rendered config/.generated/ory config, not source templates: $compose_file" >&2
exit 1
fi
done
if grep -Eq '\./ory/(kratos|hydra|keto|oathkeeper):/etc/config/' "$deploy_template"; then
echo "ERROR: deploy template must mount rendered config/.generated/ory config, not source templates." >&2
exit 1
fi
if grep -q 'ory/generated' "$deploy_template" "$repo_root/deploy/create-instance.sh"; then
echo "ERROR: deploy template must use config/.generated/ory, not ory/generated." >&2
exit 1
fi
if ! grep -q '^render-ory-config:' "$repo_root/Makefile"; then
echo "ERROR: Makefile must render Ory config before starting Ory services." >&2
exit 1
fi
if ! awk '/^ensure-ory:/ { in_target=1 } in_target && /^[^[:space:]].*:/ && $0 !~ /^ensure-ory:/ { exit } in_target { print }' "$repo_root/Makefile" | grep -q 'restart kratos'; then
echo "ERROR: make up-dev must restart Kratos when Ory is already running so rendered dev config is applied." >&2
exit 1
fi
if ! awk '/^up-all:/ { in_target=1 } in_target && /^[^[:space:]].*:/ && $0 !~ /^up-all:/ { exit } in_target { print }' "$repo_root/Makefile" | grep -q 'restart kratos'; then
echo "ERROR: make up must restart Kratos after rendering Ory config." >&2
exit 1
fi
if ! awk '/^up-ory:/ { in_target=1 } in_target && /^[^[:space:]].*:/ && $0 !~ /^up-ory:/ { exit } in_target { print }' "$repo_root/Makefile" | grep -q 'restart kratos'; then
echo "ERROR: make up-ory must restart Kratos after rendering Ory config." >&2
exit 1
fi
if ! grep -q 'scripts/render_ory_config.sh' "$repo_root/.gitea/workflows/staging_code_pull.yml"; then
echo "ERROR: staging code pull must render Ory config before docker compose up." >&2
exit 1
fi
if ! grep -q 'up -d --force-recreate kratos hydra keto oathkeeper' "$repo_root/.gitea/workflows/staging_code_pull.yml"; then
echo "ERROR: staging code pull must restart Ory services after rendering static config." >&2
exit 1
fi
if grep -Eq '^[[:space:]]*rm -rf "?\$OUTPUT_DIR"?[[:space:]]*$' "$repo_root/scripts/render_ory_config.sh"; then
echo "ERROR: Ory renderer must preserve config/.generated/ory service directories so live bind mounts stay valid." >&2
exit 1
fi
"$repo_root/scripts/render_ory_config.sh" >/dev/null
local_rendered_kratos="$repo_root/config/.generated/ory/kratos/kratos.yml"
if ! awk '/session:/ { in_session=1 } in_session && /domain:/ { print; exit }' "$local_rendered_kratos" | grep -q 'domain: localhost'; then
echo "ERROR: rendered local Kratos config must use localhost as session.cookie.domain for dev runs." >&2
exit 1
fi
stage_render_dir="$(mktemp -d)"
stage_render_env="$(mktemp)"
cat > "$stage_render_env" <<'EOF'
USERFRONT_URL=https://sso.hmac.kr
ADMINFRONT_URL=https://sadmin.hmac.kr
DEVFRONT_URL=https://sdev.hmac.kr
ORGFRONT_URL=https://sorg.hmac.kr
KRATOS_UI_URL=https://sso.hmac.kr
KRATOS_BROWSER_URL=https://sso.hmac.kr/auth
KRATOS_ADMIN_URL=http://kratos:4434
ORY_POSTGRES_PASSWORD=policy-test
KRATOS_ALLOWED_RETURN_URLS_JSON=
KRATOS_ALLOWED_RETURN_URLS_EXTRA=
EOF
ORY_CONFIG_ENV_FILES="$stage_render_env" ORY_CONFIG_OUTPUT_DIR="$stage_render_dir/ory" "$repo_root/scripts/render_ory_config.sh" >/dev/null
stage_rendered_kratos="$stage_render_dir/ory/kratos/kratos.yml"
if ! awk '/allowed_return_urls:/ { in_block=1; next } in_block && /^[[:space:]]+methods:/ { exit } in_block { print }' "$stage_rendered_kratos" | grep -q 'https://sso.hmac.kr'; then
echo "ERROR: rendered stage Kratos config must include the public userfront URL in allowed_return_urls." >&2
exit 1
fi
if awk '/allowed_return_urls:/ { in_block=1; next } in_block && /^[[:space:]]+methods:/ { exit } in_block { print }' "$stage_rendered_kratos" | grep -q 'http://localhost:5000'; then
echo "ERROR: rendered stage Kratos allowed_return_urls must not fall back to localhost." >&2
exit 1
fi
if ! awk '/session:/ { in_session=1 } in_session && /domain:/ { print; exit }' "$stage_rendered_kratos" | grep -q 'domain: hmac.kr'; then
echo "ERROR: rendered stage Kratos config must derive hmac.kr as session.cookie.domain." >&2
exit 1
fi
rm -rf "$stage_render_dir" "$stage_render_env"
for generated_config in \
"$repo_root/config/.generated/ory/kratos/kratos.yml" \
"$repo_root/config/.generated/ory/hydra/hydra.yml" \
"$repo_root/config/.generated/ory/keto/keto.yml" \
"$repo_root/config/.generated/ory/oathkeeper/oathkeeper.yml"
do
if [[ ! -f "$generated_config" ]]; then
echo "ERROR: Ory rendered config is missing: $generated_config" >&2
exit 1
fi
if grep -q '\${' "$generated_config"; then
echo "ERROR: Ory rendered config must not contain placeholders: $generated_config" >&2
exit 1
fi
done
for service in kratos-migrate kratos hydra-migrate hydra keto-migrate keto oathkeeper_logs_init oathkeeper; do
if ! grep -q "^ $service:" "$deploy_template"; then
echo "ERROR: deploy template Ory stack must include service: $service" >&2
exit 1
fi
done
for version_key in KRATOS_VERSION HYDRA_VERSION KETO_VERSION OATHKEEPER_VERSION; do
if ! grep -q "^$version_key=v26\\.2\\.0$" "$deploy_env_template"; then
echo "ERROR: deploy env template must define $version_key=v26.2.0." >&2
exit 1
fi
done
if ! grep -q 'entrypoint: \["/etc/config/oathkeeper/entrypoint.sh"\]' "$deploy_template"; then
echo "ERROR: deploy template Oathkeeper must use the env-aware entrypoint." >&2
exit 1
fi
if grep -q "rewrite \\^/oidc" "$deploy_gateway_template"; then
echo "ERROR: deploy template gateway must preserve the /oidc prefix." >&2
exit 1
fi
if ! grep -q '^version: v26.2.0$' "$deploy_kratos_template"; then
echo "ERROR: deploy Kratos template config version must match v26.2.0." >&2
exit 1
fi
for rule_id in hydra-well-known hydra-well-known-oidc hydra-oauth2 hydra-oauth2-oidc hydra-userinfo hydra-userinfo-oidc; do
if ! grep -q "\"id\": \"$rule_id\"" "$deploy_oathkeeper_rules_template"; then
echo "ERROR: deploy Oathkeeper rules must expose Hydra public route: $rule_id" >&2
exit 1
fi
done
if ! grep -q '"strip_path": "/oidc"' "$deploy_oathkeeper_rules_template"; then
echo "ERROR: deploy Oathkeeper prefixed routes must strip /oidc with strip_path." >&2
exit 1
fi

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() {
echo "ERROR: $*" >&2
exit 1
}
assert_version() {
local package_dir="$1"
local expected="$2"
local actual
actual="$(node "$ROOT_DIR/scripts/playwrightPackageVersion.cjs" "$package_dir")"
[ "$actual" = "version=$expected" ] || \
fail "$package_dir Playwright version must be $expected, got: $actual"
}
assert_version userfront-e2e 1.58.2
assert_version adminfront 1.60.0
assert_version devfront 1.60.0
assert_version orgfront 1.60.0
echo "OK: Playwright package versions are read from package.json"

View File

@@ -0,0 +1,102 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
assert_contains() {
local file="$1"
local pattern="$2"
if ! grep -Fq -- "$pattern" "$file"; then
echo "ERROR: missing pattern in $file: $pattern" >&2
exit 1
fi
}
assert_not_contains() {
local file="$1"
local pattern="$2"
if grep -Fq -- "$pattern" "$file"; then
echo "ERROR: forbidden pattern remains in $file: $pattern" >&2
exit 1
fi
}
build_rc="$ROOT_DIR/.gitea/workflows/build_RC.yml"
staging_release="$ROOT_DIR/.gitea/workflows/staging_release.yml"
production_release="$ROOT_DIR/.gitea/workflows/production_release.yml"
production_compose="$ROOT_DIR/docker/docker-compose.template.yaml"
for file in "$build_rc" "$staging_release" "$production_release" "$production_compose"; do
if [[ ! -f "$file" ]]; then
echo "ERROR: expected file not found: $file" >&2
exit 1
fi
done
for app in adminfront devfront orgfront; do
assert_contains "$build_rc" "Build and push $app RC image"
assert_contains "$build_rc" "file: ./$app/Dockerfile"
assert_contains "$build_rc" "build-args: |"
assert_contains "$build_rc" "VITE_OIDC_AUTHORITY=\${{ vars.VITE_OIDC_AUTHORITY }}"
done
assert_contains "$build_rc" "Validate RC build configuration"
assert_contains "$build_rc" "Missing required RC build value"
assert_contains "$build_rc" "Check Gitea repo variables/secrets"
assert_contains "$build_rc" "VITE_ADMIN_PUBLIC_URL=\${{ vars.ADMINFRONT_URL }}"
assert_contains "$build_rc" "VITE_DEVFRONT_PUBLIC_URL=\${{ vars.DEVFRONT_URL }}"
assert_contains "$build_rc" "VITE_ORGFRONT_PUBLIC_URL=\${{ vars.ORGFRONT_URL }}"
assert_contains "$build_rc" "ORGFRONT_URL=\${{ vars.ORGFRONT_URL }}"
assert_contains "$staging_release" "CLICKHOUSE_PASSWORD=\${{ secrets.CLICKHOUSE_PASSWORD }}"
assert_not_contains "$staging_release" "CLICKHOUSE_PASSWORD=\${{ vars.CLICKHOUSE_PASSWORD }}"
assert_contains "$staging_release" "PROFILE_CACHE_TTL=\${{ vars.PROFILE_CACHE_TTL }}"
assert_contains "$staging_release" "KRATOS_UI_NODE_VERSION=\${{ vars.KRATOS_UI_NODE_VERSION }}"
assert_contains "$staging_release" "Missing required staging .env value"
assert_contains "$staging_release" "Check Gitea repo variables/secrets"
assert_contains "$staging_release" "scp scripts/render_ory_config.sh"
assert_contains "$staging_release" "scp compose.ory.yaml"
assert_not_contains "$staging_release" "scp docker/compose.ory.yaml"
assert_contains "$staging_release" "bash scripts/render_ory_config.sh"
assert_contains "$staging_release" "chmod -R 777 config/.generated/ory"
assert_contains "$production_release" "for image in backend userfront adminfront devfront orgfront; do"
assert_contains "$production_release" 'docker://${HARBOR_HOSTNAME}/baron_sso/${image}:${BASE_TAG}'
assert_contains "$production_release" 'docker://${HARBOR_HOSTNAME}/baron_sso/${image}:${RE_TAG}'
assert_contains "$production_release" "ADMINFRONT_IMAGE_NAME: \${{ vars.HARBOR_HOSTNAME }}/baron_sso/adminfront"
assert_contains "$production_release" "DEVFRONT_IMAGE_NAME: \${{ vars.HARBOR_HOSTNAME }}/baron_sso/devfront"
assert_contains "$production_release" "ORGFRONT_IMAGE_NAME: \${{ vars.HARBOR_HOSTNAME }}/baron_sso/orgfront"
assert_contains "$production_release" "USERFRONT_URL=\${{ vars.PROD_FRONTEND_URL }}"
assert_contains "$production_release" "BACKEND_URL=\${{ vars.PROD_BACKEND_URL }}"
assert_contains "$production_release" "USERFRONT_PORT=\${{ vars.PROD_FRONTEND_PORT }}"
assert_contains "$production_release" "PROD_BACKEND_PORT=\${{ vars.PROD_BACKEND_PORT }}"
assert_contains "$production_release" "BACKEND_PORT=3000"
assert_contains "$production_release" "ADMINFRONT_URL=\${{ vars.ADMINFRONT_URL }}"
assert_contains "$production_release" "DEVFRONT_URL=\${{ vars.DEVFRONT_URL }}"
assert_contains "$production_release" "ORGFRONT_URL=\${{ vars.ORGFRONT_URL }}"
assert_contains "$production_release" "VITE_OIDC_AUTHORITY=\${{ vars.VITE_OIDC_AUTHORITY }}"
assert_contains "$production_release" "ADMINFRONT_CALLBACK_URLS=\${{ vars.ADMINFRONT_CALLBACK_URLS }}"
assert_contains "$production_release" "DEVFRONT_CALLBACK_URLS=\${{ vars.DEVFRONT_CALLBACK_URLS }}"
assert_contains "$production_release" "ORGFRONT_CALLBACK_URLS=\${{ vars.ORGFRONT_CALLBACK_URLS }}"
assert_contains "$production_release" "ADMINFRONT_PORT=\${{ vars.ADMINFRONT_PORT }}"
assert_contains "$production_release" "DEVFRONT_PORT=\${{ vars.DEVFRONT_PORT }}"
assert_contains "$production_release" "ORGFRONT_PORT=\${{ vars.ORGFRONT_PORT }}"
assert_contains "$production_release" "export ADMINFRONT_IMAGE_NAME='\${ADMINFRONT_IMAGE_NAME}'"
assert_contains "$production_release" "export DEVFRONT_IMAGE_NAME='\${DEVFRONT_IMAGE_NAME}'"
assert_contains "$production_release" "export ORGFRONT_IMAGE_NAME='\${ORGFRONT_IMAGE_NAME}'"
assert_contains "$production_release" "Missing required production .env value"
assert_not_contains "$production_release" "PROD_USERFRONT_URL"
assert_not_contains "$production_release" "PROD_USERFRONT_PORT"
for app in adminfront devfront orgfront; do
assert_contains "$production_compose" "$app:"
done
assert_contains "$production_compose" 'image: ${ADMINFRONT_IMAGE_NAME}:${IMAGE_TAG}'
assert_contains "$production_compose" 'image: ${DEVFRONT_IMAGE_NAME}:${IMAGE_TAG}'
assert_contains "$production_compose" 'image: ${ORGFRONT_IMAGE_NAME}:${IMAGE_TAG}'
assert_contains "$production_compose" 'API_PROXY_TARGET=http://baron_backend:${BACKEND_PORT:-3000}'
assert_contains "$production_compose" '${PROD_BACKEND_PORT:-3010}:3000'
assert_contains "$production_compose" '${USERFRONT_PORT:-80}:5000'
assert_contains "$production_compose" 'BACKEND_PORT=3000'
assert_contains "$production_compose" 'http://127.0.0.1:3000/health'
echo "production image release policy checks passed"

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LAYOUT_FILE="$ROOT_DIR/common/shell/layout.ts"
fail() {
echo "ERROR: $*" >&2
exit 1
}
assert_contains() {
local pattern="$1"
grep -Fq -- "$pattern" "$LAYOUT_FILE" || fail "common shell layout must contain: $pattern"
}
assert_not_contains() {
local pattern="$1"
if grep -Fq -- "$pattern" "$LAYOUT_FILE"; then
fail "common shell layout must not contain: $pattern"
fi
}
assert_contains "root: \"grid min-h-screen grid-cols-[240px,minmax(0,1fr)]"
assert_not_contains "md:grid-cols-[240px,1fr]"
assert_contains "aside:"
assert_contains "sticky top-0 h-screen"
assert_not_contains "border-b border-border bg-card md:sticky"
echo "OK: shell layout keeps the navigation in a fixed left column"

View File

@@ -0,0 +1,164 @@
#!/usr/bin/env sh
set -eu
assert_contains() {
file="$1"
pattern="$2"
if ! grep -Fq "$pattern" "$file"; then
echo "missing pattern in $file: $pattern" >&2
exit 1
fi
}
assert_not_contains() {
file="$1"
pattern="$2"
if grep -Fq "$pattern" "$file"; then
echo "forbidden pattern in $file: $pattern" >&2
exit 1
fi
}
staging_pull=".gitea/workflows/staging_code_pull.yml"
pull_compose="docker/staging_pull_compose.template.yaml"
deploy_compose="deploy/templates/docker-compose.yaml"
deploy_gateway="deploy/templates/gateway/nginx.conf"
userfront_dockerfile="userfront/Dockerfile"
devfront_vite="devfront/vite.config.ts"
orgfront_vite="orgfront/vite.config.ts"
adminfront_vite="adminfront/vite.config.ts"
adminfront_runtime="adminfront/scripts/runtime-mode.sh"
devfront_runtime="devfront/scripts/runtime-mode.sh"
orgfront_runtime="orgfront/scripts/runtime-mode.sh"
adminfront_dockerfile="adminfront/Dockerfile"
devfront_dockerfile="devfront/Dockerfile"
orgfront_dockerfile="orgfront/Dockerfile"
for file in \
"$staging_pull" \
"$pull_compose" \
"$deploy_compose" \
"$deploy_gateway" \
"$userfront_dockerfile" \
"$adminfront_vite" \
"$devfront_vite" \
"$orgfront_vite" \
"$adminfront_runtime" \
"$devfront_runtime" \
"$orgfront_runtime" \
"$adminfront_dockerfile" \
"$devfront_dockerfile" \
"$orgfront_dockerfile"
do
if [ ! -f "$file" ]; then
echo "missing expected file: $file" >&2
exit 1
fi
done
for workflow in "$staging_pull"; do
assert_contains "$workflow" 'ADMINFRONT_URL=${{ vars.ADMINFRONT_URL }}'
assert_contains "$workflow" 'DEVFRONT_URL=${{ vars.DEVFRONT_URL }}'
assert_contains "$workflow" 'ORGFRONT_URL=${{ vars.ORGFRONT_URL }}'
assert_contains "$workflow" 'KRATOS_ALLOWED_RETURN_URLS_JSON=${{ vars.KRATOS_ALLOWED_RETURN_URLS_JSON }}'
assert_contains "$workflow" 'KRATOS_ALLOWED_RETURN_URLS_EXTRA=${{ vars.KRATOS_ALLOWED_RETURN_URLS_EXTRA }}'
done
assert_contains "$staging_pull" 'bash scripts/render_ory_config.sh'
assert_contains "$staging_pull" 'chmod -R 777 config/.generated/ory'
assert_contains "$staging_pull" 'docker compose -f staging_pull_compose.yaml build --pull'
assert_contains "$staging_pull" 'docker compose -f staging_pull_compose.yaml up -d --remove-orphans --renew-anon-volumes'
assert_contains "$staging_pull" 'check_container_http baron_gateway 5000'
assert_contains "$staging_pull" 'check_container_http baron_userfront 5000'
assert_contains "$userfront_dockerfile" "FROM ghcr.io/cirruslabs/flutter:3.38.0 AS build"
assert_contains ".dockerignore" "**/build"
assert_contains "$userfront_dockerfile" "RUN rm -rf build/web && flutter build web --release --wasm"
assert_contains "$userfront_dockerfile" "FROM alpine:3.23 AS production"
assert_contains "$userfront_dockerfile" "COPY --from=optimize /work/build/web /usr/share/nginx/html"
assert_contains ".gitea/workflows/code_check.yml" "rm -rf build/web"
assert_contains ".gitea/workflows/userfront_e2e_full_nightly.yml" "rm -rf build/web"
assert_contains "$pull_compose" "baron_devfront"
assert_contains "$pull_compose" "baron_orgfront"
for app in adminfront devfront orgfront; do
assert_contains "$pull_compose" "$app:"
assert_contains "$pull_compose" "context: ."
assert_contains "$pull_compose" "dockerfile: ./$app/Dockerfile"
assert_contains "$pull_compose" "VITE_OIDC_AUTHORITY: \${VITE_OIDC_AUTHORITY:-}"
assert_not_contains "$pull_compose" "context: ./$app"
assert_not_contains "$pull_compose" "./$app:/app"
done
assert_not_contains "$pull_compose" "/app/node_modules"
assert_contains "$pull_compose" "dockerfile: userfront/Dockerfile"
assert_contains "$pull_compose" "target: production"
assert_not_contains "$pull_compose" 'target: ${USERFRONT_BUILD_TARGET:-dev}'
assert_not_contains "$pull_compose" "target: dev"
assert_not_contains "$pull_compose" "flutter run"
assert_contains "$pull_compose" "http://127.0.0.1:5173/"
assert_contains "$pull_compose" "http://127.0.0.1:5175/"
assert_contains "$pull_compose" 'APP_ENV=${APP_ENV:-stage}'
assert_contains "$deploy_compose" "dockerfile: ./adminfront/Dockerfile"
assert_contains "$deploy_compose" "dockerfile: ./devfront/Dockerfile"
assert_contains "$deploy_compose" "dockerfile: ./orgfront/Dockerfile"
assert_contains "$deploy_compose" "dockerfile: ./userfront/Dockerfile"
assert_contains "$deploy_compose" "target: production"
assert_not_contains "$deploy_compose" "sh ./scripts/runtime-mode.sh"
assert_not_contains "$deploy_compose" "command: npm run dev"
assert_not_contains "$deploy_compose" "image: node:20-alpine"
assert_contains "$deploy_gateway" "root /usr/share/nginx/html;"
assert_contains "$deploy_gateway" 'try_files $uri $uri/ /index.html;'
assert_not_contains "$deploy_gateway" "baron_userfront"
assert_not_contains "$deploy_gateway" "userfront_upstream"
assert_not_contains "$deploy_gateway" "proxy_pass http://baron_userfront"
for app in adminfront devfront orgfront; do
assert_contains ".gitea/workflows/build_RC.yml" "Build and push $app RC image"
assert_contains ".gitea/workflows/build_RC.yml" "file: ./$app/Dockerfile"
assert_not_contains ".gitea/workflows/build_RC.yml" "context: ./$app"
done
assert_contains ".gitea/workflows/build_RC.yml" "Build and push userfront RC image"
assert_contains ".gitea/workflows/build_RC.yml" "context: ."
assert_contains ".gitea/workflows/build_RC.yml" "file: ./userfront/Dockerfile"
assert_contains ".gitea/workflows/build_RC.yml" "target: production"
assert_not_contains ".gitea/workflows/build_RC.yml" "context: ./userfront"
for app in adminfront devfront orgfront; do
dockerfile="$app/Dockerfile"
assert_contains "$dockerfile" "COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./"
assert_contains "$dockerfile" "RUN pnpm install --frozen-lockfile --ignore-scripts"
assert_contains "$dockerfile" "FROM node:24-alpine AS production"
assert_contains "$dockerfile" "COPY scripts/serve_frontend_prod.mjs ./serve_frontend_prod.mjs"
assert_contains "$dockerfile" "RUN npm run build"
assert_contains "$dockerfile" 'CMD ["node", "./serve_frontend_prod.mjs"]'
assert_not_contains "$dockerfile" "cd common && pnpm install"
assert_not_contains "$dockerfile" "npm install -g serve"
assert_not_contains "$dockerfile" "runtime-mode.sh"
done
assert_contains "scripts/serve_frontend_prod.mjs" "pathname === \"/api\" || pathname.startsWith(\"/api/\")"
assert_contains "scripts/serve_frontend_prod.mjs" "API_PROXY_TARGET"
assert_contains "$adminfront_vite" "/tmp/baron-sso-adminfront-dist"
assert_contains "$adminfront_vite" "/tmp/baron-sso-adminfront-vite-cache"
assert_contains "adminfront/biome.json" '".vite"'
assert_contains "adminfront/tsconfig.app.json" '"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]'
assert_contains "$devfront_vite" "/tmp/baron-sso-devfront-dist"
assert_contains "$devfront_vite" "/tmp/baron-sso-devfront-vite-cache"
assert_contains "$devfront_vite" "hostFromUrl(process.env.DEVFRONT_URL)"
assert_contains "$devfront_vite" "process.env.DEVFRONT_ALLOWED_HOSTS"
assert_contains "devfront/biome.json" '".vite"'
assert_contains "devfront/tsconfig.app.json" '"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]'
assert_contains "$orgfront_vite" "/tmp/baron-sso-orgfront-dist"
assert_contains "$orgfront_vite" "/tmp/baron-sso-orgfront-vite-cache"
assert_contains "$orgfront_vite" '"sorg.hmac.kr"'
assert_contains "orgfront/biome.json" '".vite"'
assert_contains "orgfront/tsconfig.app.json" '"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]'
for runtime_script in "$adminfront_runtime" "$devfront_runtime" "$orgfront_runtime"; do
assert_contains "$runtime_script" "ensure_frontend_dependencies"
assert_contains "$runtime_script" "package-lock.json"
assert_contains "$runtime_script" "npm ci"
assert_contains "$runtime_script" ".baron-deps-hash"
done
echo "staging frontend deploy policy checks passed"

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env sh
set -eu
compose_file="docker/staging_pull_compose.template.yaml"
if [ ! -f "$compose_file" ]; then
echo "missing expected file: $compose_file" >&2
exit 1
fi
assert_service_has_restart_policy() {
service="$1"
awk -v service="$service" '
$0 ~ "^ " service ":" {
in_service = 1
found = 0
next
}
in_service && /^ [A-Za-z0-9_-]+:/ {
exit found ? 0 : 1
}
in_service && /^[[:space:]]+restart:[[:space:]]+(always|unless-stopped)[[:space:]]*$/ {
found = 1
}
END {
if (in_service) {
exit found ? 0 : 1
}
}
' "$compose_file" || {
echo "ERROR: long-running staging service must define restart: always or restart: unless-stopped: $service" >&2
exit 1
}
}
assert_service_has_no_restart_policy() {
service="$1"
if awk -v service="$service" '
$0 ~ "^ " service ":" {
in_service = 1
found = 0
next
}
in_service && /^ [A-Za-z0-9_-]+:/ {
exit found ? 0 : 1
}
in_service && /^[[:space:]]+restart:/ {
found = 1
}
END {
if (in_service) {
exit found ? 0 : 1
}
}
' "$compose_file"; then
echo "ERROR: one-shot staging service must not define restart policy: $service" >&2
exit 1
fi
}
for service in \
postgres \
clickhouse \
redis \
gateway \
postgres_ory \
kratos \
hydra \
keto \
oathkeeper \
ory_clickhouse \
backend \
adminfront \
devfront \
orgfront \
userfront
do
assert_service_has_restart_policy "$service"
done
for service in \
kratos-migrate \
hydra-migrate \
keto-migrate \
oathkeeper_logs_init \
ory_stack_check \
init-rp \
infra_check
do
assert_service_has_no_restart_policy "$service"
done
echo "staging pull restart policy checks passed"

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
mkdir -p "$WORK_DIR/userfront/coverage"
cat > "$WORK_DIR/userfront/coverage/lcov.info" <<'LCOV'
SF:lib/main.dart
DA:1,1
DA:2,0
LF:2
LH:1
end_of_record
SF:lib/i18n_data.dart
DA:1,0
LF:1
LH:0
end_of_record
SF:lib/features/auth/domain/login_challenge_resolver.dart
DA:10,1
DA:11,1
DA:12,0
LF:3
LH:2
end_of_record
SF:lib/core/services/logout_service.dart
DA:20,1
DA:21,1
LF:2
LH:2
end_of_record
LCOV
(
cd "$WORK_DIR"
node "$ROOT_DIR/scripts/summarize_flutter_coverage.mjs" userfront
)
node - "$WORK_DIR/reports/package-coverage-summary.json" <<'NODE'
const fs = require("node:fs");
const summary = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const row = summary.packages[0];
function assertEqual(actual, expected, message) {
if (actual !== expected) {
throw new Error(`${message}: expected ${expected}, got ${actual}`);
}
}
assertEqual(row.package, "userfront", "package name");
assertEqual(row.statements, 80, "line coverage must exclude bootstrap/generated files");
assertEqual(row.lines, 80, "lines coverage must match statements for Flutter lcov");
assertEqual(row.coveredLines, 4, "covered lines");
assertEqual(row.totalLines, 5, "total lines");
assertEqual(row.lcovPath, "userfront/coverage/lcov.info", "lcov path");
NODE
grep -Fq "| userfront | 80.00% | 4 / 5 | userfront/coverage/lcov.info |" \
"$WORK_DIR/reports/userfront-coverage-summary.md"
echo "OK: userfront Flutter LCOV summary is generated"

111
baron-sso/test/test_sms.py Normal file
View File

@@ -0,0 +1,111 @@
# python3 test/test_sms.py 01027774695
import os
import requests
import time
import hmac
import hashlib
import base64
import json
import sys
from dotenv import load_dotenv
def get_env_variable(key, env_file):
"""Reads an environment variable from a given .env file."""
with open(env_file, "r") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#"):
k, v = line.split("=", 1)
if k == key:
return v.strip()
return None
def main():
if len(sys.argv) < 2:
print("Usage: python test/test_sms.py <recipient_phone_number>")
sys.exit(1)
recipient_phone = sys.argv[1]
# Load environment variables from .env or .env.sample
env_path = os.path.join(os.getcwd(), ".env")
if not os.path.exists(env_path):
print("Info: .env file not found. Using .env.sample as a fallback.")
env_path = os.path.join(os.getcwd(), ".env.sample")
if not os.path.exists(env_path):
print("Error: No configuration file found (.env or .env.sample).")
sys.exit(1)
access_key = get_env_variable("NAVER_CLOUD_ACCESS_KEY", env_path)
secret_key = get_env_variable("NAVER_CLOUD_SECRET_KEY", env_path)
service_id = get_env_variable("NAVER_CLOUD_SERVICE_ID", env_path)
sender_phone = get_env_variable("NAVER_SENDER_PHONE_NUMBER", env_path)
if not all([access_key, secret_key, service_id, sender_phone]):
print(
f"Error: One or more required environment variables are missing in {env_path}."
)
sys.exit(1)
timestamp = str(int(time.time() * 1000))
api_path = f"/sms/v2/services/{service_id}/messages"
api_url = f"https://sens.apigw.ntruss.com{api_path}"
# Create the signature for the API request
message = f"POST {api_path}\n{timestamp}\n{access_key}"
h = hmac.new(bytes(secret_key, "UTF-8"), bytes(message, "UTF-8"), hashlib.sha256)
signature = base64.b64encode(h.digest()).decode("UTF-8")
# Construct the JSON request body
json_body = {
"type": "SMS",
"contentType": "COMM",
"countryCode": "82",
"from": sender_phone,
"content": "[Baron 로그인] Test message from Python script.",
"messages": [{"to": recipient_phone}],
}
headers = {
"Content-Type": "application/json; charset=utf-8",
"x-ncp-apigw-timestamp": timestamp,
"x-ncp-iam-access-key": access_key,
"x-ncp-apigw-signature-v2": signature,
}
print("========================================")
print(" Attempting to send SMS via SENS API (Python)")
print("========================================")
print(f" Recipient: {recipient_phone}")
print(f" Timestamp: {timestamp}")
print(f" Service ID: {service_id}")
print("========================================")
print()
try:
response = requests.post(api_url, headers=headers, json=json_body)
response.raise_for_status() # Raise an exception for HTTP errors
print("API Response:")
print(json.dumps(response.json(), indent=2, ensure_ascii=False))
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
if hasattr(e, "response") and e.response is not None:
print("API Error Response:")
try:
print(json.dumps(e.response.json(), indent=2, ensure_ascii=False))
except json.JSONDecodeError:
print(e.response.text)
except Exception as e:
print(f"An unexpected error occurred: {e}")
print()
print("========================================")
print(" Request complete.")
print("========================================")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,153 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
mkdir -p "$WORK_DIR/docs/badges"
mkdir -p "$WORK_DIR/badge-artifacts/backend/reports"
mkdir -p "$WORK_DIR/badge-artifacts/userfront/reports"
mkdir -p "$WORK_DIR/badge-artifacts/adminfront/reports"
mkdir -p "$WORK_DIR/badge-artifacts/orgfront/reports"
cat > "$WORK_DIR/docs/badges/badges.json" <<'JSON'
{
"schemaVersion": 1,
"generatedBy": "scripts/update_code_check_badges.mjs",
"updatedAt": "2026-01-01T00:00:00.000Z",
"source": {
"branch": "dev",
"sha": "abc123456789",
"shortSha": "abc123456789",
"runId": "1",
"runNumber": "1"
},
"badges": {
"userfront": {
"label": "userfront",
"message": "unknown",
"color": "#6e7781"
},
"adminfront": {
"label": "adminfront",
"message": "10.00%",
"color": "#cf222e"
},
"devfront": {
"label": "devfront",
"message": "20.00%",
"color": "#cf222e"
},
"orgfront": {
"label": "orgfront",
"message": "30.00%",
"color": "#cf222e"
}
}
}
JSON
cat > "$WORK_DIR/badge-artifacts/backend/reports/backend-coverage-summary.json" <<'JSON'
{
"package": "backend",
"statements": 90.0
}
JSON
cat > "$WORK_DIR/badge-artifacts/userfront/reports/package-coverage-summary.json" <<'JSON'
{
"packages": [
{
"package": "userfront",
"statements": 85.4
}
]
}
JSON
cat > "$WORK_DIR/badge-artifacts/adminfront/reports/vitest-coverage-summary.json" <<'JSON'
{
"packages": [
{
"package": "adminfront",
"statements": 82.345
}
]
}
JSON
cat > "$WORK_DIR/badge-artifacts/orgfront/reports/vitest-coverage-summary.json" <<'JSON'
{
"packages": [
{
"package": "orgfront",
"statements": 36.1
}
]
}
JSON
run_badge_update() {
(
cd "$WORK_DIR"
LINT_RESULT=success \
BIOME_RESULT=success \
BACKEND_RESULT=success \
BACKEND_COVERAGE_RESULT=success \
USERFRONT_RESULT=success \
USERFRONT_E2E_RESULT=success \
USERFRONT_E2E_CHROMIUM_DESKTOP_RESULT=success \
USERFRONT_E2E_CHROMIUM_MOBILE_RESULT=failure \
USERFRONT_E2E_FIREFOX_DESKTOP_RESULT=success \
USERFRONT_E2E_FIREFOX_MOBILE_RESULT=skipped \
USERFRONT_E2E_WEBKIT_DESKTOP_RESULT=failure \
USERFRONT_E2E_WEBKIT_MOBILE_RESULT=success \
ADMINFRONT_RESULT=success \
DEVFRONT_RESULT=success \
ORGFRONT_RESULT=success \
USERFRONT_COVERAGE_RESULT=success \
ADMINFRONT_COVERAGE_RESULT=success \
DEVFRONT_COVERAGE_RESULT=failure \
ORGFRONT_COVERAGE_RESULT=success \
BADGE_SOURCE_BRANCH=dev \
BADGE_SOURCE_SHA=abc123456789 \
GITHUB_RUN_ID=2 \
GITHUB_RUN_NUMBER=2 \
node "$ROOT_DIR/scripts/update_code_check_badges.mjs"
)
}
run_badge_update
node - "$WORK_DIR/docs/badges/badges.json" <<'NODE'
const fs = require("node:fs");
const manifest = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const badges = manifest.badges;
function assertEqual(actual, expected, message) {
if (actual !== expected) {
throw new Error(`${message}: expected ${expected}, got ${actual}`);
}
}
assertEqual(badges["backend-tests"].label, "backend", "backend badge label must be compact");
assertEqual(badges["backend-tests"].message, "pass | 90.00%", "backend badge must combine test result and coverage");
assertEqual(badges.userfront.message, "pass | 85.40%", "userfront badge must combine fast E2E result and coverage");
assertEqual(badges.adminfront.message, "pass | 82.34%", "adminfront badge must combine E2E result and coverage");
assertEqual(badges.devfront.message, "pass | fail", "devfront badge must fail coverage independently");
assertEqual(badges.orgfront.message, "pass | 36.10%", "orgfront badge must combine E2E result and coverage");
assertEqual(badges["userfront-chrome"].label, "chrome", "chromium full badge label must name the browser");
assertEqual(badges["userfront-chrome"].message, "pass | fail", "chromium full badge must show desktop and mobile results");
assertEqual(badges["userfront-firefox"].label, "firefox", "firefox full badge label must name the browser");
assertEqual(badges["userfront-firefox"].message, "pass | skip", "firefox full badge must show desktop and mobile results");
assertEqual(badges["userfront-safari"].label, "safari", "webkit full badge label must be shown as safari");
assertEqual(badges["userfront-safari"].message, "fail | pass", "webkit full badge must show desktop and mobile results");
NODE
cp "$WORK_DIR/docs/badges/badges.json" "$WORK_DIR/first-badges.json"
run_badge_update
cmp "$WORK_DIR/first-badges.json" "$WORK_DIR/docs/badges/badges.json"
echo "OK: package coverage badges update independently and avoid rerun churn"

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
mkdir -p "$WORK_DIR/docs/badges"
cat > "$WORK_DIR/docs/badges/badges.json" <<'JSON'
{
"schemaVersion": 1,
"generatedBy": "scripts/update_code_check_badges.mjs",
"updatedAt": "2026-01-01T00:00:00.000Z",
"source": {
"branch": "dev",
"sha": "abc123456789",
"shortSha": "abc123456789",
"runId": "1",
"runNumber": "1"
},
"badges": {
"code-check": {
"label": "code check",
"message": "passing",
"color": "#2ea043"
},
"biome": {
"label": "biome",
"message": "passing",
"color": "#2ea043"
},
"backend-tests": {
"label": "backend",
"message": "pass | 93.20%",
"color": "#2ea043"
},
"userfront": {
"label": "userfront",
"message": "pass | 56.52%",
"color": "#bf8700"
},
"adminfront": {
"label": "adminfront",
"message": "pass | 60.01%",
"color": "#bf8700"
},
"devfront": {
"label": "devfront",
"message": "pass | 58.02%",
"color": "#bf8700"
},
"orgfront": {
"label": "orgfront",
"message": "pass | 61.18%",
"color": "#bf8700"
},
"userfront-chrome": {
"label": "chrome",
"message": "unknown",
"color": "#6e7781"
},
"userfront-firefox": {
"label": "firefox",
"message": "unknown",
"color": "#6e7781"
},
"userfront-safari": {
"label": "safari",
"message": "unknown",
"color": "#6e7781"
}
}
}
JSON
(
cd "$WORK_DIR"
USERFRONT_E2E_RESULT=failure \
USERFRONT_E2E_FULL=true \
USERFRONT_E2E_CHROMIUM_DESKTOP_RESULT=success \
USERFRONT_E2E_CHROMIUM_MOBILE_RESULT=success \
USERFRONT_E2E_FIREFOX_DESKTOP_RESULT=success \
USERFRONT_E2E_FIREFOX_MOBILE_RESULT=skipped \
USERFRONT_E2E_WEBKIT_DESKTOP_RESULT=failure \
USERFRONT_E2E_WEBKIT_MOBILE_RESULT=success \
BADGE_UPDATE_CODE_CHECK=false \
BADGE_SOURCE_BRANCH=dev \
BADGE_SOURCE_SHA=abc123456789 \
GITHUB_RUN_ID=2 \
GITHUB_RUN_NUMBER=2 \
node "$ROOT_DIR/scripts/update_code_check_badges.mjs"
)
node - "$WORK_DIR/docs/badges/badges.json" <<'NODE'
const fs = require("node:fs");
const manifest = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const badges = manifest.badges;
function assertEqual(actual, expected, message) {
if (actual !== expected) {
throw new Error(`${message}: expected ${expected}, got ${actual}`);
}
}
assertEqual(badges["code-check"].message, "passing", "full nightly must preserve code-check badge");
assertEqual(badges.biome.message, "passing", "full nightly must preserve biome badge");
assertEqual(badges["backend-tests"].message, "pass | 93.20%", "full nightly must preserve backend badge");
assertEqual(badges.userfront.message, "pass | 56.52%", "full nightly must preserve userfront package badge");
assertEqual(badges.adminfront.message, "pass | 60.01%", "full nightly must preserve adminfront badge");
assertEqual(badges.devfront.message, "pass | 58.02%", "full nightly must preserve devfront badge");
assertEqual(badges.orgfront.message, "pass | 61.18%", "full nightly must preserve orgfront badge");
assertEqual(badges["userfront-chrome"].message, "pass | pass", "full nightly must update chrome badge");
assertEqual(badges["userfront-firefox"].message, "pass | skip", "full nightly must update firefox badge");
assertEqual(badges["userfront-safari"].message, "fail | pass", "full nightly must update safari badge");
NODE
echo "OK: full nightly preserves existing non-browser badges"

View File

@@ -0,0 +1,122 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
fail() {
echo "[userfront-loading-policy] $*" >&2
exit 1
}
if rg -n "FontLoader|assets/fonts/NotoSansKR|_loadBundledFonts" userfront/lib userfront/pubspec.yaml; then
fail "userfront must not block first render on bundled NotoSansKR font loading"
fi
if rg -n "dotenv\.load|touch \.env" userfront/lib/main.dart userfront/Dockerfile; then
fail "userfront web startup must not request or create public .env assets"
fi
if rg -n "fontFamily:\s*['\"]NotoSansKR['\"]" userfront/lib; then
fail "userfront theme must use the platform default font"
fi
if rg -n "await ThemeController\.(app|auth)\.restore" userfront/lib/main.dart; then
fail "theme restore must not block the first render"
fi
if rg -n "fonts\.googleapis\.com/icon\?family=Material\+Icons" userfront/web/index.html; then
fail "userfront must not load Google Material Icons stylesheet on the login critical path"
fi
if rg -n -- "--no-tree-shake-icons" userfront/Dockerfile userfront-e2e/package.json; then
fail "userfront web release build must allow icon tree shaking"
fi
rg -q "optimize-web-build\.mjs" userfront/Dockerfile || fail "Docker build must hash and pre-compress Flutter web entrypoints"
rg -q "nginx-mod-http-brotli" userfront/Dockerfile || fail "runtime image must install the nginx Brotli module"
rg -Fq "main\\.dart\\.[0-9a-f]{12}" userfront/nginx.conf || fail "hashed app entrypoints must use immutable cache"
rg -q "brotli_static\s+on;" userfront/nginx.conf || fail "nginx must serve pre-compressed brotli assets"
rg -q "brotliCompressSync" userfront/scripts/optimize-web-build.mjs || fail "Docker build optimization must generate brotli assets"
rg -q "modulepreload" userfront/scripts/optimize-web-build.mjs || fail "Docker build optimization must preload wasm module entrypoints"
rg -q "canvasKitBaseUrl:\"canvaskit/\"" userfront/scripts/optimize-web-build.mjs || fail "userfront web build must force local CanvasKit instead of fetching engine resources from a CDN"
rg -q "serviceWorkerSettings" userfront/scripts/optimize-web-build.mjs || fail "Flutter service worker registration must be preserved so deployed clients can update cached bundles"
rg -q "serviceWorkerUrl" userfront/scripts/optimize-web-build.mjs || fail "Flutter service worker URL must be explicit so new clients register the worker"
if rg -n "gzip|gzipSync|\\.gz" userfront/nginx.conf userfront/scripts/optimize-web-build.mjs; then
fail "userfront web compression must be managed as brotli-only"
fi
rg -q "Cache-Control.*no-cache" userfront/nginx.conf || fail "HTML/app shell must use no-cache revalidation"
if rg -n "assets/\\.env|/\\.env|runtimeEnvBody|dotenv\\.load" userfront/lib userfront/nginx.conf userfront-e2e/scripts/serve-userfront-build.mjs; then
fail "userfront must not request, load, or serve public .env assets to browsers"
fi
if rg -n "/usr/share/nginx/html/.+\\.env|assets/\\.env|cp .+\\.env" docker/docker-compose.staging.template.yaml docker/staging_pull_compose.template.yaml; then
fail "userfront deployment must not write runtime .env into the public static document root"
fi
rg -q "\\[userfront-runtime\\] BACKEND_URL configured" docker/docker-compose.staging.template.yaml docker/staging_pull_compose.template.yaml || fail "userfront runtime config presence must be logged server-side only"
rg -q "Cache-Control.*immutable" userfront/nginx.conf || fail "versioned static assets must use immutable cache"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
cat > "$tmp_dir/flutter_bootstrap.js" <<'BOOTSTRAP'
const serviceWorkerVersion = "e2e-policy";
_flutter.buildConfig = {
builds: [
{
mainJsPath: "main.dart.js",
mainWasmPath: "main.dart.wasm",
jsSupportRuntimePath: "main.dart.mjs",
},
],
};
_flutter.loader.load({
serviceWorkerSettings: {
serviceWorkerVersion: serviceWorkerVersion,
}
});
BOOTSTRAP
cat > "$tmp_dir/index.html" <<'HTML'
<!doctype html>
<html>
<head></head>
<body><script src="flutter_bootstrap.js"></script></body>
</html>
HTML
printf 'console.log("js");' > "$tmp_dir/main.dart.js"
printf 'console.log("mjs");' > "$tmp_dir/main.dart.mjs"
printf 'wasm' > "$tmp_dir/main.dart.wasm"
mkdir -p "$tmp_dir/canvaskit/chromium"
printf 'console.log("skwasm");' > "$tmp_dir/canvaskit/skwasm.js"
printf 'skwasm' > "$tmp_dir/canvaskit/skwasm.wasm"
printf 'console.log("canvaskit");' > "$tmp_dir/canvaskit/canvaskit.js"
printf 'canvaskit' > "$tmp_dir/canvaskit/canvaskit.wasm"
printf 'console.log("chromium canvaskit");' > "$tmp_dir/canvaskit/chromium/canvaskit.js"
printf 'chromium canvaskit' > "$tmp_dir/canvaskit/chromium/canvaskit.wasm"
printf 'console.log("skwasm heavy");' > "$tmp_dir/canvaskit/skwasm_heavy.js"
printf 'skwasm heavy' > "$tmp_dir/canvaskit/skwasm_heavy.wasm"
node userfront/scripts/optimize-web-build.mjs "$tmp_dir" >/dev/null
node userfront/scripts/optimize-web-build.mjs "$tmp_dir" >/dev/null
rg -q "serviceWorkerSettings" "$tmp_dir/flutter_bootstrap.js" || fail "optimized bootstrap must keep Flutter service worker settings"
rg -q "serviceWorkerUrl" "$tmp_dir/flutter_bootstrap.js" || fail "optimized bootstrap must register the Flutter service worker on new clients"
rg -q "canvasKitBaseUrl:\"canvaskit/\"" "$tmp_dir/flutter_bootstrap.js" || fail "optimized bootstrap must keep local CanvasKit config"
rg -q "caches\\.open" "$tmp_dir/flutter_service_worker.js" || fail "optimized service worker must cache built assets"
rg -q "networkFirst" "$tmp_dir/flutter_service_worker.js" || fail "optimized service worker must revalidate app shell assets"
if rg -n "unregister\\(" "$tmp_dir/flutter_service_worker.js"; then
fail "optimized service worker must not unregister itself"
fi
test "$(rg -o "serviceWorkerUrl" "$tmp_dir/flutter_bootstrap.js" | wc -l)" -eq 1 || fail "optimized bootstrap must not duplicate serviceWorkerUrl"
test "$(rg -o "config:\\{canvasKitBaseUrl" "$tmp_dir/flutter_bootstrap.js" | wc -l)" -eq 1 || fail "optimized bootstrap must not duplicate loader config"
rg -q "main\\.dart\\.[0-9a-f]{12}\\.mjs" "$tmp_dir/index.html" || fail "optimized index must preload hashed module entrypoint"
if rg -n '<link rel="preload" href="main\.dart\.[^"]+\.js" as="script" />' "$tmp_dir/index.html"; then
fail "WASM-capable builds must not preload the JS fallback entrypoint"
fi
test ! -e "$tmp_dir/main.dart.mjs" || fail "plain module entrypoint must be renamed after hashing"
test "$(find "$tmp_dir" -maxdepth 1 -name 'main.dart.*.mjs' | wc -l)" -eq 1 || fail "exactly one hashed module entrypoint must be produced"
if rg -n '"/canvaskit/[^"]+"' "$tmp_dir/flutter_service_worker.js"; then
fail "service worker install cache must not precache Flutter renderer assets"
fi
if rg -n '"/main\.dart\.[^"]+"' "$tmp_dir/flutter_service_worker.js"; then
fail "service worker install cache must not duplicate Flutter app entrypoint downloads"
fi