Skip to content

Commit 68ff3b9

Browse files
authored
Merge pull request #14 from beyondwin/codex/release-v2.0.0
chore(release): publish v2.0.0
2 parents 0cd590a + 35c85da commit 68ff3b9

152 files changed

Lines changed: 15393 additions & 2332 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ READMATES_BFF_SECRET=<shared-bff-secret>
3434
# Optional rotation list (comma-separated, primary first). Takes priority over READMATES_BFF_SECRET when set.
3535
READMATES_BFF_SECRETS=
3636
READMATES_BFF_SECRET_REQUIRED=true
37+
# Production release gate: only the current browser bundle + Pages BFF may mutate /api/host/**.
38+
# Keep false for direct local API work; sync-config pins production to true.
39+
READMATES_HOST_WRITE_CLIENT_CONTRACT_REQUIRED=false
3740
# rotation-only: 회전 중 secondary 사용도 감사. all: 모든 인증을 감사.
3841
READMATES_SECURITY_BFF_AUDIT_MODE=rotation-only
3942
READMATES_AUTH_SESSION_COOKIE_SECURE=true

.github/workflows/ci.yml

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,35 +56,65 @@ jobs:
5656
run: python3 -B scripts/check-agent-guidance.py
5757

5858
- name: Install script tools
59+
if: ${{ always() }}
5960
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends ripgrep shellcheck
6061

62+
- name: Collect tracked shell scripts
63+
if: ${{ always() }}
64+
shell: bash
65+
run: |
66+
shell_script_list="$RUNNER_TEMP/readmates-tracked-shell-scripts.zlist"
67+
git ls-files -z -- \
68+
':(glob)scripts/**/*.sh' \
69+
':(glob)deploy/oci/**/*.sh' \
70+
> "$shell_script_list"
71+
if [[ ! -s "$shell_script_list" ]]; then
72+
echo "No tracked shell scripts found under scripts or deploy/oci." >&2
73+
exit 1
74+
fi
75+
printf 'READMATES_TRACKED_SHELL_SCRIPTS=%s\n' "$shell_script_list" >> "$GITHUB_ENV"
76+
6177
- name: Bash syntax
78+
if: ${{ always() }}
6279
run: |
63-
for f in scripts/*.sh deploy/oci/*.sh; do
64-
bash -n "$f"
65-
done
80+
shell_script_list="${READMATES_TRACKED_SHELL_SCRIPTS:?tracked shell-script list is unavailable}"
81+
while IFS= read -r -d '' shell_script; do
82+
bash -n "$shell_script"
83+
done < "$shell_script_list"
6684
6785
- name: ShellCheck
68-
run: shellcheck scripts/*.sh deploy/oci/*.sh
86+
if: ${{ always() }}
87+
run: |
88+
shell_script_list="${READMATES_TRACKED_SHELL_SCRIPTS:?tracked shell-script list is unavailable}"
89+
mapfile -d '' -t shell_scripts < "$shell_script_list"
90+
shellcheck "${shell_scripts[@]}"
6991
7092
- name: aigen PII regression check
93+
if: ${{ always() }}
7194
run: bash scripts/aigen-pii-check.sh
7295

7396
- name: Validate Prometheus rules
97+
if: ${{ always() }}
7498
run: ./scripts/validate-prometheus-rules.sh
7599

76100
- name: Validate Prometheus configs
101+
if: ${{ always() }}
77102
run: ./scripts/validate-prometheus-config.sh
78103

79104
- name: Validate Tempo config
105+
if: ${{ always() }}
80106
run: bash scripts/validate-tempo-config.sh
81107

82108
- name: Validate production AI config
83-
run: |
84-
bash scripts/validate-production-ai-config.sh
85-
bash scripts/verify-production-ai-config-fixtures.sh
109+
if: ${{ always() }}
110+
run: bash scripts/validate-production-ai-config.sh
111+
112+
- name: Verify production AI config fixtures
113+
if: ${{ always() }}
114+
run: bash scripts/verify-production-ai-config-fixtures.sh
86115

87116
- name: Lint Grafana dashboards
117+
if: ${{ always() }}
88118
run: ./scripts/lint-grafana-dashboards.sh
89119

90120
public-release:
@@ -294,9 +324,6 @@ jobs:
294324
- name: Server quality gates (ktlint + detekt + tests + JaCoCo + architecture)
295325
run: ./scripts/server-ci-check.sh
296326

297-
- name: Lint Grafana dashboards
298-
run: ./scripts/lint-grafana-dashboards.sh
299-
300327
- name: Upload server reports
301328
if: always()
302329
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1

.github/workflows/deploy-front.yml

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,23 @@ name: Deploy Front
22

33
on:
44
workflow_dispatch:
5-
push:
6-
tags:
7-
- "v*"
5+
inputs:
6+
release_tag:
7+
description: "Release tag to deploy after backend promotion, for example v2.0.0"
8+
required: true
9+
type: string
810

911
permissions:
1012
contents: read
1113
deployments: write
1214

1315
concurrency:
14-
group: ${{ github.workflow }}-${{ github.ref }}
16+
group: ${{ github.workflow }}-${{ inputs.release_tag }}
1517
cancel-in-progress: true
1618

1719
jobs:
1820
deploy:
1921
name: Deploy Cloudflare Pages
20-
if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') }}
2122
runs-on: ubuntu-latest
2223
environment:
2324
name: production
@@ -26,10 +27,30 @@ jobs:
2627
run:
2728
working-directory: front
2829
steps:
30+
- name: Validate release tag
31+
working-directory: .
32+
env:
33+
RELEASE_TAG: ${{ inputs.release_tag }}
34+
run: |
35+
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
36+
echo "Invalid release tag: $RELEASE_TAG" >&2
37+
exit 1
38+
fi
39+
2940
- name: Check out repository
3041
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3142
with:
32-
ref: ${{ github.ref }}
43+
ref: ${{ inputs.release_tag }}
44+
fetch-depth: 0
45+
46+
- name: Verify checked out release tag
47+
working-directory: .
48+
env:
49+
RELEASE_TAG: ${{ inputs.release_tag }}
50+
run: |
51+
test "$(git rev-list -n 1 "$RELEASE_TAG")" = "$(git rev-parse HEAD)"
52+
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_STEP_SUMMARY"
53+
echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_STEP_SUMMARY"
3354
3455
- name: Set up Node.js
3556
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0

.github/workflows/sync-config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ jobs:
4040
READMATES_ALLOWED_ORIGINS: ${{ vars.READMATES_ALLOWED_ORIGINS }}
4141
# prod 정책 — 모든 BFF 호출 secret 필수, 세션 쿠키 Secure.
4242
READMATES_BFF_SECRET_REQUIRED: "true"
43+
# backend-first 배포 중 구버전 Pages BFF의 host mutation을 fail closed.
44+
READMATES_HOST_WRITE_CLIENT_CONTRACT_REQUIRED: "true"
4345
READMATES_AUTH_SESSION_COOKIE_SECURE: "true"
4446
READMATES_AIGEN_ENABLED: ${{ vars.READMATES_AIGEN_ENABLED }}
4547
READMATES_AIGEN_ENABLED_PROVIDERS: ${{ vars.READMATES_AIGEN_ENABLED_PROVIDERS }}
@@ -132,6 +134,7 @@ jobs:
132134
printf 'READMATES_BFF_SECRET=%s\n' "$READMATES_BFF_SECRET"
133135
printf 'READMATES_BFF_SECRETS=%s\n' "$READMATES_BFF_SECRETS"
134136
printf 'READMATES_BFF_SECRET_REQUIRED=%s\n' "$READMATES_BFF_SECRET_REQUIRED"
137+
printf 'READMATES_HOST_WRITE_CLIENT_CONTRACT_REQUIRED=%s\n' "$READMATES_HOST_WRITE_CLIENT_CONTRACT_REQUIRED"
135138
printf 'READMATES_AUTH_SESSION_COOKIE_SECURE=%s\n' "$READMATES_AUTH_SESSION_COOKIE_SECURE"
136139
printf 'READMATES_IP_HASH_BASE_SECRET=%s\n' "$READMATES_IP_HASH_BASE_SECRET"
137140
printf 'SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_ID=%s\n' "$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_ID"

CHANGELOG.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ ReadMates는 Git tag와 GitHub Releases를 함께 사용합니다. 이 파일은
1010

1111
- 다음 릴리즈 후보 변경을 이 섹션에 기록합니다.
1212

13-
## v2.0.0 - 2026-07-23
13+
## v2.0.0 - 2026-07-25
1414

1515
### Highlights
1616

@@ -32,27 +32,41 @@ ReadMates는 Git tag와 GitHub Releases를 함께 사용합니다. 이 파일은
3232
- **Token/API compatibility:** 내부 비용은 non-cached input/cache-write/cache-read/output 4채널로 계산하지만 public REST response는 기존 input/cachedInput/output 3필드를 유지합니다.
3333
- **Trace/privacy/ops:** Spring Kafka observation과 Micrometer/OpenTelemetry OTLP를 활성화하고 7일 Tempo, Grafana datasource/exemplar, provider/cost-basis/circuit/exporter metric/alert를 추가했습니다. Local port는 loopback-only이고 OCI app은 `tempo:4318` internal DNS로 export하며 Tempo/OTLP host port는 publish하지 않습니다. Production config sync는 Google paid-tier 확인을 기본 `false`로 렌더링해 미확인 Gemini rollout을 fail closed합니다.
3434
- **호스트 알림 발송 분리:** 다음 책 공개, 피드백 문서·세션 기록 final live apply는 legacy 호스트 결정 ledger나 outbox row를 만들지 않습니다. 외부 JSON import와 AI commit은 draft만 저장하고 composer를 반환하거나 열지 않습니다. 이후 composer가 현재 `contentRevision`을 고정해 options → preview → confirm 순서로 수동 dispatch와 outbox를 만들고, stale/만료/중복/클럽 밖 수신자는 fail closed합니다.
35+
- **호스트 알림 발송 작업대:** `/app/host/notifications`를 서버에서 확인된 리마인더 정책과 네 운영 지표를 한 줄에 보여주는 상태 레일, `회차 → 알림 종류 → 대상과 채널` 3단계 작업대, 수신 인원 기반 preview side sheet, 최근 수동 발송 원장, 이상 상태에서만 펼치는 운영 상세로 재구성했습니다. Desktop은 오른쪽 side sheet, mobile은 닫기 가능한 bottom sheet를 사용하며 닫기·Escape·backdrop·화면 이동은 발송을 만들지 않습니다.
36+
- **호스트 운영 원장:** 호스트 대시보드를 현재 세션과 우선 행동 중심의 운영 원장으로 재구성하고, 확인 필요 세션을 중복 지표 없는 compact ledger row로 정렬해 비대칭 빈 공간과 모바일의 과도한 스크롤을 줄였습니다.
3537
- **공통 기록 초안과 적용 receipt:** 수동 편집, 외부 JSON, AI 결과는 모두 live record를 직접 바꾸지 않고 같은 staged draft에 저장됩니다. final apply는 호스트가 제공한 `applyRequestId`와 draft hash를 검증해 콘텐츠·immutable revision·idempotent apply receipt를 원자적으로 갱신하고, 그 때만 발송 가능한 composer context를 반환합니다.
38+
- **Major release host-write handshake:** 새 browser bundle은 mutating `/api/host/**`에 v2 client contract를 선언하고, 새 Pages Functions/Vite proxy는 정확한 선언만 trusted upstream header로 재생성합니다. Production Spring은 같은 header를 요구하므로 구 browser·구 BFF·새 backend가 섞이는 배포 창과 이미 열린 구 탭의 host write를 `409 HOST_CLIENT_UPGRADE_REQUIRED`로 fail closed하며, 새 browser + 새 BFF 조합에서만 다시 허용합니다.
3639
- **호스트 피드백 preview:** host-only preview는 staged draft가 아니라 `session_feedback_documents`의 최신 live 문서를 읽습니다. `OPEN` 세션도 현재 live 문서가 있으면 `FEEDBACK_DOCUMENT_PUBLISHED` manual options → preview → confirm을 사용할 수 있으며, member/public 피드백 문서는 기존 열람 권한을 따릅니다.
3740
- **알림 대상과 리마인더 정책:** 다음 책과 리마인더의 기본 대상은 `ALL_ACTIVE_MEMBERS`, 피드백 문서와 세션 기록의 기본 대상은 `CONFIRMED_ATTENDEES`이며 기본 채널은 모두 `BOTH`입니다. `SELECTED_MEMBERS`는 호스트가 명시적으로 선택해야 하고 현재 클럽의 중복 없는 활성 membership ID를 한 명 이상 요구합니다. 예약 리마인더는 클럽별 정책이 명시적으로 켜진 경우에만 scheduler가 outbox row를 만들며, 정책 row가 없으면 꺼짐입니다.
41+
- **CI 테스트 안전망 도달성:** PR CI가 `scripts/``deploy/oci/`의 tracked shell을 재귀적으로 수집해 Bash와 ShellCheck에 전달하고, 관측·AI 설정 validator를 앞선 실패와 독립적으로 실행합니다. 중복 Grafana lint는 단일 job 소유권으로 통합했으며 제품 동작, 배포 계약, worker·retry·timeout·coverage 기준은 바꾸지 않았습니다.
42+
- **Google GenAI contract test 안정화:** full suite 부하에서 mock 429/500 응답보다 먼저 테스트용 2초 timeout이 발생하던 경합을 제거했습니다. 실제 timeout 시나리오의 30ms 경계와 production 4분 timeout, SDK/Spring AI 단일 요청·무재시도 계약은 그대로 유지합니다.
3843
- **의존성 보안:** 전이 의존성 `brace-expansion`을 DoS 취약점이 수정된 `5.0.7` 이상으로 강제합니다.
3944

4045
### Database
4146

42-
- **Flyway V37:** content-free `ai_generation_commit_receipts`(`job_id + revision` unique)를 추가하고 `ai_generation_audit_log`에 pipeline/turn/speaker/grounding/review aggregate column을 추가했습니다. Rolling deploy 동안 구 server가 읽지 못하는 값으로 기존 row를 재작성하지 않으며, 새 server가 저장된 `gemini-3-flash` 기본값을 canonical `gemini-3-flash-preview`로 해석합니다. Transcript, member name, result, evidence/excerpt는 MySQL에 저장하지 않습니다.
47+
- **Flyway V37:** content-free `ai_generation_commit_receipts`(`job_id + revision` unique)를 추가하고 `ai_generation_audit_log`에 pipeline/turn/speaker/grounding/review aggregate column을 추가했습니다. Rolling deploy 동안 구 server가 읽지 못하는 값으로 기존 row를 재작성하지 않으며, 새 server가 저장된 `gemini-3-flash` 기본값을 canonical `gemini-3-flash-preview`로 해석합니다. Transcript, member name, 검토 전 provider result, evidence/excerpt는 AI audit/receipt에 저장하지 않습니다. V39 이후 호스트가 commit한 검토 완료 snapshot만 공통 staged draft에 저장됩니다.
4348
- **Flyway V38:** `ai_generation_audit_log`에 nullable trace ID, provider attempt ordinal/call mode, non-null cost basis와 cache-write input token을 additive하게 추가했습니다. 기존 business-audit identity는 유지하지만 prompt/completion/transcript/evidence/raw error는 저장하지 않습니다.
4449
- **Flyway V39–V41:** 세션 기록 draft/revision, metadata-only 변경 audit, 호스트 작업 preview/decision ledger를 추가하고 AI commit receipt와 draft를 session metadata 및 revision에 안전하게 결속합니다. 저장되는 audit/ledger는 변경 필드·식별자·집계값 중심이며 transcript, AI evidence, 이메일 원문, credential은 저장하지 않습니다.
4550
- **Flyway V42:** idempotent `session_record_apply_receipts`와 opt-in `club_notification_policies`를 추가하고, 수동 dispatch에 nullable `content_revision``SELECTED_MEMBERS` audience를 forward-only로 확장합니다. V39–V41은 변경하지 않으며 기존 수동 dispatch row는 nullable revision으로 호환됩니다.
4651

4752
### Deployment Notes
4853

49-
- `v2.0.0` major release 준비입니다. 기존 SEND/SKIP mutation 계약을 제거한 새 frontend/server API 조합은 이전 frontend 또는 server와 장기간 혼용할 수 없으므로 호환 쌍을 같은 release commit에서 배포해야 합니다. 이 섹션은 2026-07-23 로컬 문서 준비이며 tag, GitHub Release, push, production deploy가 수행됐다는 뜻이 아닙니다.
54+
- 기존 SEND/SKIP mutation 계약을 제거한 새 frontend/server API 조합은 이전 frontend 또는 server와 host write를 혼용할 수 없으므로 `v2.0.0` tag push는 `Deploy Server Image`만 시작합니다. Scan/promote 뒤 `sync-config(restart_api=false, dry_run=false)``READMATES_HOST_WRITE_CLIENT_CONTRACT_REQUIRED=true`를 포함한 fail-closed runtime을 준비하고, GHCR image를 OCI Compose에 반영해 V37–V42 Flyway 성공과 backend health를 확인합니다. 이 시점부터 구 Pages BFF의 host mutation은 409로 잠시 동결됩니다. 마지막으로 `Deploy Front``release_tag=v2.0.0`으로 수동 실행해 같은 tag의 browser bundle과 Pages Functions를 함께 배포하면 v2 handshake가 성립해 host write가 재개됩니다.
5055
- Legacy/direct provider 실행과 runtime selector는 없습니다. 활성화 전에 provider allowlist/key, capability, Google paid-tier retention 확인, mock-wire/E2E, single-node Redis 전제를 검증합니다. Kafka max poll interval 기본값은 세 번의 4분 요청과 bounded delay/validation margin을 포함해 16분입니다.
51-
- Transcript/turns/result/evidence는 6시간 Redis payload로만 유지하고 commit/cancel 후 삭제합니다. MySQL receipt와 Redis revision/lease로 crash window를 복구하며 `COMMITTED + cleanupPending`DB write를 반복하지 않고 cleanup만 재시도합니다. Platform admin은 metadata-only 복구만 수행합니다.
56+
- Transcript/turns/evidence와 검토 전 result는 최대 6시간 Redis payload로 유지하고 commit/cancel에서 즉시 정리를 시도합니다. MySQL receipt와 Redis revision/lease로 crash window를 복구하며 `COMMITTED + cleanupPending`staged-draft write를 반복하지 않고 cleanup만 재시도하고 TTL을 최종 backstop으로 사용합니다. Platform admin은 metadata-only 복구만 수행합니다.
5257
- Rollback은 AI/consumer를 먼저 끄고 6시간 AI TTL을 기다린 뒤 이전 image로 복원합니다. 승인된 job namespace cleanup만 허용하며 Redis 전체 flush나 V38 destructive rollback은 금지합니다.
53-
- Private transcript를 live provider로 보내는 품질 평가, production mode 변경, deploy는 별도 명시 승인 없이 실행하지 않습니다.
58+
- Private transcript를 live provider로 보내는 품질 평가는 별도 명시 승인 없이는 실행하지 않습니다. Production AI provider는 현재 운영 allowlist, key, retention 확인 상태를 그대로 유지하며 이 릴리스 과정에서 임의로 활성화하거나 범위를 넓히지 않습니다.
5459
- `readmates.host-action-confirmation.required`는 staged session-record capability 노출만 제어하며 알림 dispatch를 다시 결합하지 않습니다. 알림은 호스트의 manual composer confirm 또는 명시적으로 켠 클럽 리마인더 정책에서만 outbox에 기록됩니다.
5560
- V42는 forward-only migration입니다. DB backup과 migration diff를 확인하고 V42 적용을 완료한 뒤 호환 server/frontend를 배포하며, rollback은 V42 schema를 남긴 채 이전 호환 image로 전환합니다. 이 변경 자체는 production 정책을 켜거나 실제 알림을 발송하지 않습니다.
61+
- 배포 후 `/internal/health`, anonymous BFF auth, OAuth start redirect, 공개 앱 응답을 확인하고, 인증된 호스트 화면에서는 운영 원장과 알림 작업대가 로드되는지, preview를 닫았을 때 dispatch/event/outbox가 생기지 않는지, confirmed send만 최근 발송 원장에 나타나는지 확인합니다. 실제 멤버 정보나 알림 본문은 릴리스 증거에 기록하지 않습니다.
62+
- Frontend만 이전 tag로 rollback하면 v2 backend가 구 browser/BFF의 host write를 계속 409로 동결하는 것이 정상입니다. 쓰기를 복구하려면 v2 frontend를 다시 배포하거나 backend도 스키마를 보존한 호환 image로 rollback/forward-fix합니다.
63+
64+
### Verification
65+
66+
- Full local release gate (2026-07-25): `./scripts/pre-push-check.sh --full --release` passed. It covered agent guidance, frontend lint and coverage (192 files, 1,536 tests; 82.36% statements, 77.34% branches, 83.01% functions, 83.04% lines), production build, Zod fixture freshness, server PR quality, AI privacy/production config validators, public release candidate/gitleaks, Testcontainers integration, 92 Playwright E2E tests with the production host-write contract gate enabled, and observability config validation.
67+
- Route-critical visual regression (2026-07-25): `corepack pnpm --dir front test:ct:docker` passed 7/7 component tests.
68+
- Workflow contract (2026-07-25): `actionlint .github/workflows/deploy-front.yml .github/workflows/deploy-server.yml .github/workflows/sync-config.yml` reported no findings. `Deploy Front` is manual and validates an exact `vMAJOR.MINOR.PATCH` input/tag checkout after backend promotion.
69+
- Live provider quality calls were skipped because they require billable credentials and separate retention approval. Production deploy, secret/config sync, tag-triggered server image publication, manual frontend dispatch and production smoke are release-operation evidence performed after `main` publication.
5670

5771
## v1.17.3 - 2026-07-12
5872

0 commit comments

Comments
 (0)