feat: add manual production deployment pipeline
Deploy EG-BIM QA Gateway / deploy (push) Failing after 7s
Deploy EG-BIM QA Gateway / deploy (push) Failing after 7s
This commit is contained in:
1 parent
1808f14cc6
commit
0755155368
13 files changed
+1391
-5
No files matched your search
@@ -0,0 +1,71 @@
|
||||
name: Deploy EG-BIM QA Production
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: qa-production
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy-production:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DEPLOY_REF: ${{ gitea.ref }}
|
||||
DEPLOY_COMMIT_SHA: ${{ gitea.sha }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.PROD_CLOUDFLARE_API_TOKEN }}
|
||||
SESSION_SECRET: ${{ secrets.PROD_SESSION_SECRET }}
|
||||
ABC_API_KEY: ${{ secrets.PROD_ABC_API_KEY }}
|
||||
AUTH_CLIENT_ID: ${{ vars.PROD_AUTH_CLIENT_ID }}
|
||||
ABC_API_BASE_URL: ${{ vars.PROD_ABC_API_BASE_URL }}
|
||||
ABC_PROJECT_ID: ${{ vars.PROD_ABC_PROJECT_ID }}
|
||||
ABC_CHANNEL_ID: ${{ vars.PROD_ABC_CHANNEL_ID }}
|
||||
ORG_CONTEXT_ENABLED: ${{ vars.PROD_ORG_CONTEXT_ENABLED }}
|
||||
ORG_CONTEXT_API_URL: ${{ vars.PROD_ORG_CONTEXT_API_URL }}
|
||||
ORG_CONTEXT_SCOPE_MODE: ${{ vars.PROD_ORG_CONTEXT_SCOPE_MODE }}
|
||||
ORG_CONTEXT_EXPOSURE: ${{ vars.PROD_ORG_CONTEXT_EXPOSURE }}
|
||||
ORG_CONTEXT_TENANT_IDS: ${{ vars.PROD_ORG_CONTEXT_TENANT_IDS }}
|
||||
ORG_CONTEXT_CACHE_TTL_SECONDS: ${{ vars.PROD_ORG_CONTEXT_CACHE_TTL_SECONDS }}
|
||||
ORG_CONTEXT_KEY_ID: ${{ secrets.PROD_ORG_CONTEXT_KEY_ID }}
|
||||
ORG_CONTEXT_KEY_SECRET: ${{ secrets.PROD_ORG_CONTEXT_KEY_SECRET }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Require main and the selected commit
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test "$DEPLOY_REF" = "refs/heads/main" || { echo "Production must be deployed from main"; exit 1; }
|
||||
test "$(git rev-parse HEAD)" = "$DEPLOY_COMMIT_SHA" || { echo "Checkout does not match the selected commit"; exit 1; }
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Validate production settings
|
||||
run: node scripts/deploy-production.mjs --validate-only
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --no-fund --no-audit
|
||||
|
||||
- name: Check source and tests
|
||||
run: npm run check && npm test
|
||||
|
||||
- name: Verify production organization API
|
||||
run: npm run org:verify
|
||||
|
||||
- name: Build production Worker without deploying
|
||||
run: npm run deploy:production -- --dry-run
|
||||
|
||||
- name: Upload this commit's static files
|
||||
shell: bash
|
||||
run: bash scripts/upload-r2.sh baron-qa-write wrangler.production.toml "releases/$DEPLOY_COMMIT_SHA"
|
||||
|
||||
- name: Deploy production Worker and secrets
|
||||
run: npm run deploy:production
|
||||
|
||||
- name: Verify production HTTP endpoints
|
||||
run: npm run production:verify
|
||||
@@ -72,6 +72,8 @@ Worker는 OAuth Authorization Code + PKCE를 사용하고, callback에서 검증
|
||||
|
||||
## Gitea Actions 등록값
|
||||
|
||||
운영 `qa.baroncs.co.kr` 배포는 [production 파이프라인 안내](docs/qa-production-pipeline.md)를 따른다. 운영 workflow는 `Deploy EG-BIM QA Production`이며 `main`에서 수동으로 실행한다. 아래 기존 등록값은 스테이징 배포용이다.
|
||||
|
||||
저장소 Settings → Actions → Secrets에 아래 기본 Secret 3개를 등록합니다.
|
||||
|
||||
| 이름 | 종류 | 값 |
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# Q&A 운영 배포 체크리스트
|
||||
|
||||
대상 도메인: `https://qa.baroncs.co.kr`
|
||||
정리일: 2026-10-28
|
||||
|
||||
## 현재 저장소에서 확인한 상태
|
||||
|
||||
- 스테이징은 `baron-qa-gateway-test`, `qa-test.baroncs.co.kr`, `baron-qa-pages`를 사용한다.
|
||||
- 운영 Worker `baron-qa-gateway-prod`, custom domain `qa.baroncs.co.kr`, R2 버킷 `baron-qa-write` 생성과 연결을 사용자가 완료했다.
|
||||
- 운영 전용 `wrangler.production.toml`과 수동 배포 workflow `Deploy EG-BIM QA Production`을 추가했다.
|
||||
- 운영 등록값과 실행 방법은 [production 파이프라인 안내](qa-production-pipeline.md)를 따른다. 실제 운영 배포는 아직 수행하지 않았다.
|
||||
|
||||
## 내가 준비·확정할 항목
|
||||
|
||||
### 1. DNS와 Cloudflare
|
||||
|
||||
- [x] `baroncs.co.kr` zone이 운영 Cloudflare 계정에 있는지 확인했다.
|
||||
- [x] `qa.baroncs.co.kr`을 운영 Worker custom domain으로 연결했다.
|
||||
- [x] 스테이징과 같은 Cloudflare 계정에 운영 Worker와 버킷을 생성했다.
|
||||
- [x] 운영 정적 파일용 R2 버킷을 생성했다: `baron-qa-write`. 스테이징 `baron-qa-pages`와 분리해 사용한다.
|
||||
- [x] 배포용 Cloudflare API token에 계정 범위의 Workers Scripts Read/Write, Workers R2 Storage Read/Write, Account Settings Read 권한을 부여하고 `PROD_CLOUDFLARE_API_TOKEN` Secret에 등록했다. 사용자 등록 완료 보고 기준이며 실제 token 인증은 배포 실행 시 검증한다. 도메인 연결은 대시보드에서 완료했다. [권한 상세](qa-production-pipeline.md)
|
||||
|
||||
### 2. SSO 운영 등록
|
||||
|
||||
- [x] 운영 callback URI를 SSO에 등록했다: `https://qa.baroncs.co.kr/auth/callback`. 사용자 완료 보고 기준이다.
|
||||
- [x] 로그아웃 후 이동 URI를 SSO에 등록했다: `https://qa.baroncs.co.kr/`. 사용자 완료 보고 기준이다.
|
||||
- [x] 운영 SSO 설정에 맞는 `PROD_AUTH_CLIENT_ID` 등 관련 변수 등록을 완료했다. 실제 로그인 동작 확인은 운영 배포 후 수행한다.
|
||||
- [ ] 운영 도메인에서 실제 사내 사용자로 로그인·로그아웃할 수 있는지 확인한다.
|
||||
|
||||
### 3. ABC UserFeedback 운영 대상
|
||||
|
||||
- [ ] **보류 — 관리페이지 서버 확정 후 진행:** 운영 API 주소를 확정하고 `PROD_ABC_API_BASE_URL`에 등록한다.
|
||||
- [ ] **보류 — 관리페이지 서버 확정 후 진행:** 운영 project ID와 channel ID를 확정하고 `PROD_ABC_PROJECT_ID`, `PROD_ABC_CHANNEL_ID`에 등록한다. 현재 저장소의 ID를 운영에서 재사용해도 되는지 확인한다.
|
||||
- [ ] **보류 — 관리페이지 서버 확정 후 진행:** 운영 project/channel에 문의 생성, 첨부, 댓글 조회·등록·수정·삭제 권한이 있는 API key를 준비하고 Gitea Secret `PROD_ABC_API_KEY`에 등록한다. 이 키가 등록될 때까지 첫 운영 배포는 진행하지 않는다.
|
||||
- [ ] `requester_affiliation`, `requester_department` 필드가 운영 channel에서 허용되는지 확인한다.
|
||||
- [ ] 운영 문의를 스테이징 데이터와 분리할지 결정한다. 기존 데이터 이전이 필요하면 별도 이관 범위와 실행 계획을 정한다.
|
||||
- [ ] 운영 source namespace와 idempotency consumer를 스테이징과 구분할 값을 정한다.
|
||||
|
||||
### 4. 조직 컨텍스트 API
|
||||
|
||||
- [ ] 운영에서 조회할 기준 tenant UUID를 확정한다. 여러 개면 JSON 배열로 제공한다.
|
||||
- [ ] 운영용 `ORG_CONTEXT_KEY_ID`와 `ORG_CONTEXT_KEY_SECRET`을 준비한다. 스테이징 키 재사용이 허용되는지 API 소유자에게 확인한다.
|
||||
- [ ] 운영 키가 해당 기준 tenant에 대해 `SCOPED` / `identifiers` 조회 권한을 갖는지 확인한다.
|
||||
- [ ] 운영 설정으로 org API 사전 검증을 통과시킨다. 키와 조직 원문이 빌드 로그에 출력되지 않는지 확인한다.
|
||||
|
||||
### 5. 운영 배포 값 등록
|
||||
|
||||
스테이징에 등록한 `STAGING_*` 값은 운영 배포에 자동 적용되지 않는다. [production 파이프라인 안내의 확정된 `PROD_*` 등록 목록](qa-production-pipeline.md)을 사용해 Gitea **Actions Variables / Secrets**에 등록한다. 아래는 운영 대상 값의 요약이다.
|
||||
|
||||
- [x] 사용자 보고 기준, 관리페이지 연동 `PROD_ABC_*` 4개를 제외한 운영 Secrets와 Variables 등록을 완료했다. 실제 연결·권한은 배포 전에 검증한다.
|
||||
|
||||
**운영 설정**
|
||||
|
||||
| 설정 | 운영 값 |
|
||||
|---|---|
|
||||
| Worker 이름 | `baron-qa-gateway-prod` |
|
||||
| Worker custom domain | `qa.baroncs.co.kr` |
|
||||
| AUTH redirect URI | `https://qa.baroncs.co.kr/auth/callback` |
|
||||
| AUTH post logout URI | `https://qa.baroncs.co.kr/` |
|
||||
| AUTH client ID | 운영 SSO client ID |
|
||||
| ABC API base URL | `https://feedback.hmac.kr` 또는 운영 API 주소 |
|
||||
| ABC project ID / channel ID | 운영 대상 ID |
|
||||
| ABC source namespace / idempotency consumer | 스테이징과 구별되는 운영 값 |
|
||||
| R2 bucket | `baron-qa-write` |
|
||||
| ORG_CONTEXT_ENABLED | `true` |
|
||||
| ORG_CONTEXT_API_URL | `https://sadmin.hmac.kr/api/v1/integrations/org-context` |
|
||||
| ORG_CONTEXT_SCOPE_MODE / EXPOSURE | `SCOPED` / `identifiers` |
|
||||
| ORG_CONTEXT_TENANT_IDS | 운영 기준 tenant UUID JSON 배열 |
|
||||
| ORG_CONTEXT_CACHE_TTL_SECONDS | `300` 권장 |
|
||||
|
||||
**운영 Secrets 용도**
|
||||
|
||||
| Secret | 용도 |
|
||||
|---|---|
|
||||
| Cloudflare API token | 운영 Worker와 정적 파일 배포 |
|
||||
| `SESSION_SECRET` | 운영 세션 서명용. 스테이징과 별도 값 |
|
||||
| `ABC_API_KEY` | 운영 ABC project/channel API 접근 |
|
||||
| `ORG_CONTEXT_KEY_ID` | 조직 컨텍스트 연동 Key ID |
|
||||
| `ORG_CONTEXT_KEY_SECRET` | 조직 컨텍스트 연동 Key Secret |
|
||||
|
||||
Secret 원문은 소스, Markdown, 채팅, workflow 로그에 넣지 않는다.
|
||||
|
||||
## 개발 측에서 필요한 변경
|
||||
|
||||
- [x] 스테이징 설정을 보존하면서 production 전용 Wrangler 설정을 추가했다.
|
||||
- [x] Worker 이름, domain, OAuth URL, R2 bucket, ABC 대상, 조직 API 값을 환경별로 분리했다.
|
||||
- [x] 별도 운영 배포 스크립트를 추가하고, 운영 배포 전에 `npm run org:verify`를 수행하도록 연결했다.
|
||||
- [x] production 수동 배포 workflow에 `PROD_*` 매핑과 필수값 검증을 추가했다.
|
||||
- [x] 운영 정적 파일을 운영 버킷의 커밋별 release 경로에 업로드하도록 연결했다.
|
||||
- [ ] 배포 실행 전에 변경 범위와 대상 도메인을 검토하고, staging 배포와 product 배포가 섞이지 않도록 확인한다.
|
||||
|
||||
## 운영 배포 후 확인
|
||||
|
||||
- [ ] `https://qa.baroncs.co.kr`의 TLS 인증서와 정적 페이지 응답을 확인한다.
|
||||
- [ ] SSO 로그인·로그아웃 및 세션 쿠키 동작을 확인한다.
|
||||
- [ ] 회사·부서가 조직 API 기준 정보와 일치하는지 확인한다.
|
||||
- [ ] 텍스트 문의와 이미지 첨부 문의를 각각 1건 등록하고, 운영 ABC 화면의 requester 정보·회사·부서 값을 확인한다.
|
||||
- [ ] 목록, 상세, 댓글 조회·등록을 확인하고 댓글 수정·삭제 권한을 작성자 기준으로 확인한다.
|
||||
- [ ] 브라우저 Network 탭과 Cloudflare Worker logs에 API key, 연동 secret, 사용자 전화번호 원문이 노출되지 않는지 확인한다.
|
||||
- [ ] 오류 시 이전 Worker 버전으로 되돌리는 방법과 담당자를 기록한다.
|
||||
|
||||
## 진행 순서
|
||||
|
||||
1. 사용자가 DNS/SSO/ABC/Cloudflare 운영 값과 데이터 분리 방침을 확정한다.
|
||||
2. 개발 측에서 production 설정·workflow를 분리하고, 운영 변수·secret 이름을 확정해 공유한다.
|
||||
3. 사용자가 운영용 Gitea Variables/Secrets를 등록한다.
|
||||
4. production 조직 API 사전 검증과 배포 전 점검을 통과시킨다.
|
||||
5. production 배포 승인 후 `qa.baroncs.co.kr`에서 위 운영 확인 항목을 완료한다.
|
||||
@@ -0,0 +1,117 @@
|
||||
# Q&A production 배포 파이프라인
|
||||
|
||||
## 배포 대상과 실행 방식
|
||||
|
||||
| 항목 | 운영 값 |
|
||||
|---|---|
|
||||
| Workflow | `.gitea/workflows/deploy-production.yml` |
|
||||
| Actions 표시 이름 | `Deploy EG-BIM QA Production` |
|
||||
| 실행 | Gitea Actions에서 `main`을 선택해 수동 실행 |
|
||||
| Worker | `baron-qa-gateway-prod` |
|
||||
| Custom Domain | `https://qa.baroncs.co.kr` |
|
||||
| Cloudflare account ID | `81fa2d48964d31dd0da9558f9ce601d1` |
|
||||
| 정적 파일 R2 bucket | `baron-qa-write` |
|
||||
| R2 binding 이름 | `QA_BUCKET` |
|
||||
| 정적 파일 저장 경로 | `releases/<전체 커밋 SHA>/egbim/...`, `releases/<전체 커밋 SHA>/assets/...` |
|
||||
|
||||
운영 Worker와 도메인 연결, 버킷 생성은 사용자가 Cloudflare 대시보드에서 완료했다. 실제 Q&A 코드 배포는 이 workflow로 수행한다. `main` push는 기존 스테이징 workflow를 실행하며, 운영 workflow는 수동 실행만 지원한다. 운영 실행도 `main`의 선택된 커밋과 checkout 결과가 같아야 진행한다.
|
||||
|
||||
배포 파일은 `wrangler.production.toml`을 사용한다. 운영 세션 쿠키·source namespace는 별도로 지정했고 `workers.dev`와 preview URL은 비활성화한다. 운영 접속 주소는 `qa.baroncs.co.kr`이다. 이미 연결한 custom domain은 Cloudflare 대시보드에서 관리한다. Wrangler 설정에는 `route`와 `routes`를 지정하지 않아 배포 시 기존 도메인 연결을 유지하며 zone 라우팅을 변경하지 않는다.
|
||||
|
||||
## Gitea에 등록할 Secrets
|
||||
|
||||
저장소 **Settings → Actions → Secrets**에 등록한다. 기존 기본 Secret이나 `STAGING_*` 값을 자동으로 가져오지 않는다. 기존 키가 운영 리소스에도 권한이 있다면 값은 재사용할 수 있지만, 아래 운영 설정 이름으로 등록해야 한다. `SESSION_SECRET`은 운영 전용 값을 사용한다.
|
||||
|
||||
| 이름 | 값 |
|
||||
|---|---|
|
||||
| `PROD_CLOUDFLARE_API_TOKEN` | 기존 운영 Worker를 배포하고 R2 파일을 업로드할 수 있는 Cloudflare token |
|
||||
| `PROD_SESSION_SECRET` | 최소 32자 운영 세션 서명키. 예: `openssl rand -hex 32`로 생성한 값 |
|
||||
| `PROD_ABC_API_KEY` | **등록 보류:** 관리페이지 서버 확정 후 운영 ABC project/channel API key 등록 |
|
||||
| `PROD_ORG_CONTEXT_KEY_ID` | 운영 조직 컨텍스트 연동 Key ID |
|
||||
| `PROD_ORG_CONTEXT_KEY_SECRET` | 운영 조직 컨텍스트 연동 Key Secret |
|
||||
|
||||
Cloudflare 프로필 **API Tokens → Create Token → Create Custom Token**에서 운영 배포 token을 생성하거나 기존 token의 권한을 확인한다.
|
||||
|
||||
| 범위 | 권한 | 수준 | 용도 |
|
||||
|---|---|---|---|
|
||||
| Account / Workers product | Workers | Editor | 기존 Worker 코드·Secret·binding 배포. 레거시 Workers Scripts Edit도 허용 |
|
||||
| Account | Workers R2 Storage | Edit / Write | 정적 파일을 R2에 업로드 |
|
||||
| Account | Account Settings | Read | Wrangler 계정 정보 조회 |
|
||||
|
||||
**Account Resources**는 `바른기술개발센터` 계정으로 지정한다. 생성한 token 값은 `PROD_CLOUDFLARE_API_TOKEN` Secret에 넣는다. 도메인 연결은 대시보드에서 이미 완료했으므로 운영 배포 token에는 Workers Routes Write와 Zone Read를 요구하지 않는다. 현재 파이프라인은 Wrangler의 Cloudflare REST API를 사용하므로 R2 S3 Access Key ID / Secret Access Key는 필요하지 않다.
|
||||
|
||||
R2 권한은 계정 범위의 **Workers R2 Storage Write/Edit**를 선택한다. `R2 → baron-qa-write` 범위의 **Workers R2 Storage Bucket Item Read/Write**는 S3 객체 접근용이므로 현재 Wrangler REST 업로드에 필요한 권한을 대신하지 않는다. 운영 배포에는 Zone Write 정책도 필요하지 않다.
|
||||
|
||||
근거: [Workers 권한](https://developers.cloudflare.com/workers/authorization/workers/), [R2 token 권한](https://developers.cloudflare.com/r2/api/tokens/), [대시보드에서 route를 관리하는 Wrangler 설정](https://developers.cloudflare.com/workers/wrangler/configuration/#source-of-truth).
|
||||
|
||||
## Gitea에 등록할 Variables
|
||||
|
||||
저장소 **Settings → Actions → Variables**에 등록한다.
|
||||
|
||||
| 이름 | 필수 | 값 |
|
||||
|---|---|---|
|
||||
| `PROD_AUTH_CLIENT_ID` | 필수 | 운영 callback URI가 허용된 SSO client ID |
|
||||
| `PROD_ABC_API_BASE_URL` | 필수 / 등록 보류 | 관리페이지 서버 확정 후 운영 ABC HTTPS origin 등록. `/api` 경로를 붙이지 않는다. |
|
||||
| `PROD_ABC_PROJECT_ID` | 필수 / 등록 보류 | 관리페이지 서버 확정 후 운영 ABC project UUID 등록 |
|
||||
| `PROD_ABC_CHANNEL_ID` | 필수 / 등록 보류 | 관리페이지 서버 확정 후 운영 ABC channel UUID 등록 |
|
||||
| `PROD_ORG_CONTEXT_ENABLED` | 필수 | `true` |
|
||||
| `PROD_ORG_CONTEXT_TENANT_IDS` | 필수 | 기준 tenant UUID JSON 배열. 예: `["tenant-UUID"]` |
|
||||
| `PROD_ORG_CONTEXT_API_URL` | 선택 | 기본 `https://sadmin.hmac.kr/api/v1/integrations/org-context` |
|
||||
| `PROD_ORG_CONTEXT_SCOPE_MODE` | 선택 | 기본 `SCOPED` |
|
||||
| `PROD_ORG_CONTEXT_EXPOSURE` | 선택 | 기본 `identifiers` |
|
||||
| `PROD_ORG_CONTEXT_CACHE_TTL_SECONDS` | 선택 | 기본 `300`, 허용 범위 1~3600 |
|
||||
|
||||
SSO에는 `https://qa.baroncs.co.kr/auth/callback`을 허용하고, 로그아웃 후 이동 URI로 `https://qa.baroncs.co.kr/`을 확인한다. 등록된 SSO client나 ABC project/channel이 운영 대상인지 확정한 후 배포한다.
|
||||
|
||||
지금 준비할 Variables는 `PROD_AUTH_CLIENT_ID`와 `PROD_ORG_CONTEXT_*` 6개다. `PROD_AUTH_CLIENT_ID`의 참고값은 기존 스테이징 client `e16898a7-acd2-420a-a224-cdcd89ff66cd`이며, 이 client에 운영 callback이 허용된 경우에만 운영에서도 재사용한다. 별도 운영 client를 만들었다면 그 ID를 등록한다. 기준 tenant UUID와 조직 연동 키는 실제 발급받은 값을 사용한다. 기존 `STAGING_*` 등록은 `PROD_*` 등록으로 간주하지 않는다.
|
||||
|
||||
회사·부서 조회용 sadmin 조직 컨텍스트 API 설정은 ABC 관리페이지 연동 설정과 별개다. 조직 API 주소·기준 tenant·키의 운영 사용이 확정된 경우 미리 등록할 수 있다. Secret 값이나 테스트 fixture UUID를 실제 설정값으로 문서에 기록하지 않는다.
|
||||
|
||||
Worker runtime에는 `PROD_`를 제거한 이름으로 주입한다. `DEPLOY_COMMIT_SHA`는 workflow가 선택된 커밋에서 자동으로 전달하므로 Gitea에 따로 등록하지 않는다.
|
||||
|
||||
## Workflow 순서
|
||||
|
||||
1. 소스를 checkout하고 `main` 브랜치·커밋 일치를 확인한다.
|
||||
2. 운영 필수 설정, UUID, 조직 API 설정과 세션 서명키 길이를 검사한다. 누락 시 배포 전에 중단한다.
|
||||
3. `npm ci`로 고정된 의존성을 설치한다.
|
||||
4. `npm run check`와 `npm test`를 실행한다.
|
||||
5. `npm run org:verify`로 실제 운영 조직 API의 인증·범위·구성원 응답을 검증한다.
|
||||
6. `npm run deploy:production -- --dry-run`으로 Worker 번들을 만든다. 이 단계는 Cloudflare 리소스를 변경하지 않는다.
|
||||
7. 해당 커밋의 정적 파일을 `baron-qa-write/releases/<SHA>/...`에 올린다.
|
||||
8. `npm run deploy:production`으로 코드·runtime 변수·Secret을 같은 Worker 배포에 반영한다.
|
||||
9. `npm run production:verify`로 익명 세션, CSS 응답·커밋 일치, 비로그인 글쓰기 redirect를 확인한다. 전파 지연에 대비해 최대 5회 확인한다.
|
||||
|
||||
Secret 파일은 임시 디렉터리에 접근 권한 `0600`으로 만들고 배포 명령 종료 후 삭제한다. Secret 값은 CLI 인자나 로그로 출력하지 않는다.
|
||||
|
||||
## 첫 운영 배포 실행
|
||||
|
||||
현재 `PROD_ABC_API_KEY`, `PROD_ABC_API_BASE_URL`, `PROD_ABC_PROJECT_ID`, `PROD_ABC_CHANNEL_ID` 등록은 관리페이지 서버 확정까지 보류한다. 이 값들이 없으면 필수 설정 검증에서 배포를 중단한다. SSO·조직 컨텍스트·Cloudflare·세션 서명키 설정은 먼저 준비할 수 있다.
|
||||
|
||||
- [x] 관리페이지 연동 `PROD_ABC_*` 4개를 제외한 운영 Secrets와 Variables 등록을 완료했다. 사용자 보고 기준이며 실제 값·권한 검증은 아직 수행하지 않았다.
|
||||
- [ ] 관리페이지 서버 확정 후 `PROD_ABC_API_KEY`, `PROD_ABC_API_BASE_URL`, `PROD_ABC_PROJECT_ID`, `PROD_ABC_CHANNEL_ID`를 등록한다.
|
||||
- [x] 운영 SSO callback과 관련 변수 등록을 완료했다. 사용자 완료 보고 기준이다.
|
||||
- [ ] ABC project/channel과 조직 API의 실제 접근 권한을 확인한다.
|
||||
- [ ] 이 변경을 `main`에 반영한다.
|
||||
- [ ] Gitea **Actions → Deploy EG-BIM QA Production → Run workflow**에서 `main`을 선택한다.
|
||||
- [ ] 설정 검증·조직 API 검증·정적 파일 업로드·Worker 배포·HTTP 확인이 모두 성공하는지 확인한다.
|
||||
- [ ] 실제 계정으로 로그인하고 문의·첨부·댓글 기능과 저장된 회사·부서를 확인한다. 자동 HTTP 확인은 로그인 이후의 실제 사용자 작업까지 검증하지 않는다.
|
||||
|
||||
## 실패와 롤백
|
||||
|
||||
- 설정·조직 API·빌드 검증 실패는 운영 정적 파일 업로드와 Worker 배포 전에 중단된다.
|
||||
- 정적 파일 업로드가 실패해도 기존 Worker는 이전 커밋 경로를 계속 읽는다. 다시 실행하면 같은 커밋 경로로 업로드한다.
|
||||
- Worker 배포 후 HTTP 확인이 실패하면 workflow는 실패로 표시되지만 이미 반영한 배포를 자동으로 되돌리지는 않는다. 마지막 정상 Worker 버전으로 명시적으로 롤백한다.
|
||||
- Cloudflare의 `baron-qa-gateway-prod` **Deployments**에서 마지막 정상 버전으로 rollback하면 해당 버전의 `QA_ASSET_PREFIX`와 정적 파일을 다시 사용한다. 이전 release 경로는 보관한다.
|
||||
- 첫 배포의 이전 버전은 기본 Hello World일 수 있다. 첫 배포 실패 시에는 설정을 수정해 다시 실행하고, 필요하면 기본 버전으로 되돌릴 수 있다.
|
||||
- R2 release 파일 정리 시 활성 버전과 롤백에 필요한 버전의 prefix를 삭제하지 않는다. 정적 파일 롤백은 ABC에 저장된 문의·댓글 데이터를 되돌리지 않는다.
|
||||
|
||||
## 로컬 확인
|
||||
|
||||
```sh
|
||||
npm run check
|
||||
npm test
|
||||
node scripts/deploy-production.mjs --validate-only
|
||||
npm run deploy:production -- --dry-run
|
||||
```
|
||||
|
||||
마지막 두 명령은 위 runtime 이름의 운영 설정과 `DEPLOY_COMMIT_SHA`가 실행 환경에 필요하다. `--validate-only`는 설정 형식만 확인하고, `--dry-run`은 로컬 번들만 확인한다. 실제 자격증명과 API의 유효성은 Gitea workflow의 조직 API 검증과 실제 배포에서 확인한다.
|
||||
Generated
+756
@@ -0,0 +1,756 @@
|
||||
{
|
||||
"name": "baron-qa-gateway-test",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "baron-qa-gateway-test",
|
||||
"devDependencies": {
|
||||
"wrangler": "4.134.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudflare/kv-asset-handler": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
|
||||
"integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudflare/unenv-preset": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz",
|
||||
"integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"unenv": "2.0.0-rc.24",
|
||||
"workerd": ">1.20260305.0 <2.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"workerd": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudflare/workerd-linux-64": {
|
||||
"version": "1.20260917.1",
|
||||
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260917.1.tgz",
|
||||
"integrity": "sha512-t981nh4Ol5mjzkH6x7x7Oev/UVmF5n3zjm07btl58BxJ1IgUSUPW0XJz07DR7rWss/EmGey7UlRMUM0qRcWkvg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz",
|
||||
"integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
||||
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@poppinss/colors": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
|
||||
"integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"kleur": "^4.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@poppinss/dumper": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz",
|
||||
"integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@poppinss/colors": "^4.1.5",
|
||||
"@sindresorhus/is": "^7.0.2",
|
||||
"supports-color": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@poppinss/exception": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz",
|
||||
"integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",
|
||||
"integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@speed-highlight/core": {
|
||||
"version": "1.2.24",
|
||||
"resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz",
|
||||
"integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/blake3-wasm": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
|
||||
"integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/error-stack-parser-es": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
|
||||
"integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/aix-ppc64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/android-arm": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/android-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/android-x64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/darwin-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/darwin-x64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/freebsd-x64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/linux-arm": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/linux-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/linux-ia32": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/linux-loong64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/linux-mips64el": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/linux-ppc64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/linux-riscv64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/linux-s390x": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/netbsd-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/netbsd-x64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/openbsd-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/openbsd-x64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/openharmony-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/sunos-x64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/win32-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/win32-ia32": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/win32-x64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/miniflare": {
|
||||
"version": "5.20260917.0-alpha",
|
||||
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260917.0-alpha.tgz",
|
||||
"integrity": "sha512-NpZyBR+h/nonVA/YO/+1/UJpIl8lTwCgpFPVp/KVzAhb8JuCRImPDfyrft040659Zna1RS3Lt2pzTs5Vwy5QSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "0.8.1",
|
||||
"sharp": "0.35.4",
|
||||
"undici": "7.29.0",
|
||||
"workerd": "1.20260917.1",
|
||||
"ws": "8.21.0",
|
||||
"youch": "4.1.0-beta.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
|
||||
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz",
|
||||
"integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.1.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.8.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.35.4",
|
||||
"@img/sharp-darwin-x64": "0.35.4",
|
||||
"@img/sharp-freebsd-wasm32": "0.35.4",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.3",
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-arm": "1.3.3",
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.3",
|
||||
"@img/sharp-libvips-linux-x64": "1.3.3",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.3",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.3",
|
||||
"@img/sharp-linux-arm": "0.35.4",
|
||||
"@img/sharp-linux-arm64": "0.35.4",
|
||||
"@img/sharp-linux-ppc64": "0.35.4",
|
||||
"@img/sharp-linux-riscv64": "0.35.4",
|
||||
"@img/sharp-linux-s390x": "0.35.4",
|
||||
"@img/sharp-linux-x64": "0.35.4",
|
||||
"@img/sharp-linuxmusl-arm64": "0.35.4",
|
||||
"@img/sharp-linuxmusl-x64": "0.35.4",
|
||||
"@img/sharp-webcontainers-wasm32": "0.35.4",
|
||||
"@img/sharp-win32-arm64": "0.35.4",
|
||||
"@img/sharp-win32-ia32": "0.35.4",
|
||||
"@img/sharp-win32-x64": "0.35.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-darwin-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-darwin-x64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-freebsd-wasm32": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-linux-arm": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-linux-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-linux-ppc64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-linux-riscv64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-linux-s390x": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-webcontainers-wasm32": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-win32-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-win32-ia32": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sharp/node_modules/@img/sharp-win32-x64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
|
||||
"integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
|
||||
"integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/unenv": {
|
||||
"version": "2.0.0-rc.24",
|
||||
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
|
||||
"integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/workerd": {
|
||||
"version": "1.20260917.1",
|
||||
"resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260917.1.tgz",
|
||||
"integrity": "sha512-k072RxsZfRz2cnyAq1Titt+0VpNfnFizSXUkT3r15CDJECBfBXj6JeKK0Bk5CrBLAHGP+dvOHjI//TP7GHXDEw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"workerd": "bin/workerd"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@cloudflare/workerd-darwin-64": "1.20260917.1",
|
||||
"@cloudflare/workerd-darwin-arm64": "1.20260917.1",
|
||||
"@cloudflare/workerd-linux-64": "1.20260917.1",
|
||||
"@cloudflare/workerd-linux-arm64": "1.20260917.1",
|
||||
"@cloudflare/workerd-windows-64": "1.20260917.1"
|
||||
}
|
||||
},
|
||||
"node_modules/workerd/node_modules/@cloudflare/workerd-darwin-64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/workerd/node_modules/@cloudflare/workerd-darwin-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/workerd/node_modules/@cloudflare/workerd-linux-arm64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/workerd/node_modules/@cloudflare/workerd-windows-64": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/wrangler": {
|
||||
"version": "4.134.0",
|
||||
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.134.0.tgz",
|
||||
"integrity": "sha512-65JbYVGSt0XpPH8O/y6pNuS0t+AvvwVv+VRPbqYsNI5NMF9oFiJxVoS5GYU47ooamF9ucSXWvGyQ6QnAtXLgRA==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@cloudflare/kv-asset-handler": "0.5.0",
|
||||
"@cloudflare/unenv-preset": "2.16.1",
|
||||
"blake3-wasm": "2.1.5",
|
||||
"esbuild": "0.28.1",
|
||||
"miniflare": "5.20260917.0-alpha",
|
||||
"path-to-regexp": "6.3.0",
|
||||
"unenv": "2.0.0-rc.24",
|
||||
"workerd": "1.20260917.1"
|
||||
},
|
||||
"bin": {
|
||||
"cf-wrangler": "bin/cf-wrangler.js",
|
||||
"wrangler": "bin/wrangler.js",
|
||||
"wrangler2": "bin/wrangler.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cloudflare/workers-types": "^5.20260917.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@cloudflare/workers-types": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/wrangler/node_modules/fsevents": {
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/youch": {
|
||||
"version": "4.1.0-beta.10",
|
||||
"resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",
|
||||
"integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@poppinss/colors": "^4.1.5",
|
||||
"@poppinss/dumper": "^0.6.4",
|
||||
"@speed-highlight/core": "^1.2.7",
|
||||
"cookie": "^1.0.2",
|
||||
"youch-core": "^0.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/youch-core": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz",
|
||||
"integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@poppinss/exception": "^1.2.2",
|
||||
"error-stack-parser-es": "^1.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -4,10 +4,12 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"deploy:production": "node scripts/deploy-production.mjs",
|
||||
"r2:upload": "bash scripts/upload-r2.sh",
|
||||
"check": "node --check src/index.js && node --check src/org-context.js && node --check egbim/app.js",
|
||||
"check": "node --check src/index.js && node --check src/org-context.js && node --check egbim/app.js && node --check scripts/production-settings.mjs && node --check scripts/deploy-production.mjs && node --check scripts/verify-production.mjs && bash -n scripts/upload-r2.sh",
|
||||
"test": "node --test --test-isolation=none tests/*.test.js",
|
||||
"org:verify": "node scripts/verify-org-context.mjs"
|
||||
"org:verify": "node scripts/verify-org-context.mjs",
|
||||
"production:verify": "node scripts/verify-production.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "4.134.0"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { productionSettings } from './production-settings.mjs';
|
||||
|
||||
function execute(args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('npx', ['--no-install', 'wrangler', ...args], { stdio: 'inherit' });
|
||||
child.once('error', () => reject(new Error('Production Worker command could not start')));
|
||||
child.once('exit', (code) => code === 0 ? resolve() : reject(new Error('Production Worker deployment failed')));
|
||||
});
|
||||
}
|
||||
|
||||
let secretDirectory;
|
||||
try {
|
||||
const options = process.argv.slice(2);
|
||||
if (options.some((option) => !['--dry-run', '--validate-only'].includes(option))) throw new Error('Unknown production deployment option');
|
||||
const settings = productionSettings(process.env);
|
||||
console.log(JSON.stringify({ worker: settings.worker, bucket: settings.bucket, origin: settings.origin, assets: settings.vars.QA_ASSET_PREFIX }));
|
||||
if (!options.includes('--validate-only')) {
|
||||
const args = ['deploy', '--config', settings.config, '--name', settings.worker];
|
||||
for (const [name, value] of Object.entries(settings.vars)) args.push('--var', name + ':' + value);
|
||||
if (options.includes('--dry-run')) {
|
||||
args.push('--dry-run');
|
||||
} else {
|
||||
// Deploy code and secrets together; keep secret values out of arguments and logs.
|
||||
secretDirectory = await mkdtemp(join(tmpdir(), 'qa-production-secrets-'));
|
||||
const secretPath = join(secretDirectory, 'secrets.json');
|
||||
await writeFile(secretPath, JSON.stringify(settings.secrets), { mode: 0o600 });
|
||||
args.push('--secrets-file', secretPath);
|
||||
}
|
||||
await execute(args);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (secretDirectory) await rm(secretDirectory, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { orgContextConfig } from '../src/org-context.js';
|
||||
import { deploymentOrgSettings } from './org-context-settings.mjs';
|
||||
|
||||
export const productionTarget = Object.freeze({
|
||||
worker: 'baron-qa-gateway-prod',
|
||||
config: 'wrangler.production.toml',
|
||||
bucket: 'baron-qa-write',
|
||||
origin: 'https://qa.baroncs.co.kr'
|
||||
});
|
||||
|
||||
function required(env, name) {
|
||||
const value = String(env[name] || '').trim();
|
||||
if (!value) throw new Error(name + ' is required for production');
|
||||
return value;
|
||||
}
|
||||
|
||||
function httpsBase(value, name) {
|
||||
let url;
|
||||
try { url = new URL(value); } catch { throw new Error(name + ' must be an HTTPS origin'); }
|
||||
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') {
|
||||
throw new Error(name + ' must be an HTTPS origin');
|
||||
}
|
||||
return url.origin;
|
||||
}
|
||||
|
||||
export function productionSettings(env) {
|
||||
// These names are mapped exclusively from PROD_* settings in the production workflow.
|
||||
required(env, 'CLOUDFLARE_API_TOKEN');
|
||||
const secrets = {
|
||||
SESSION_SECRET: required(env, 'SESSION_SECRET'),
|
||||
ABC_API_KEY: required(env, 'ABC_API_KEY'),
|
||||
ORG_CONTEXT_KEY_ID: required(env, 'ORG_CONTEXT_KEY_ID'),
|
||||
ORG_CONTEXT_KEY_SECRET: required(env, 'ORG_CONTEXT_KEY_SECRET')
|
||||
};
|
||||
if (secrets.SESSION_SECRET.length < 32) throw new Error('SESSION_SECRET must contain at least 32 characters');
|
||||
if (required(env, 'ORG_CONTEXT_ENABLED') !== 'true') throw new Error('ORG_CONTEXT_ENABLED must be true for production');
|
||||
const organization = deploymentOrgSettings(env);
|
||||
orgContextConfig({ ...organization.vars, ...secrets });
|
||||
|
||||
const vars = {
|
||||
...organization.vars,
|
||||
AUTH_CLIENT_ID: required(env, 'AUTH_CLIENT_ID'),
|
||||
ABC_API_BASE_URL: httpsBase(required(env, 'ABC_API_BASE_URL'), 'ABC_API_BASE_URL'),
|
||||
ABC_PROJECT_ID: required(env, 'ABC_PROJECT_ID'),
|
||||
ABC_CHANNEL_ID: required(env, 'ABC_CHANNEL_ID')
|
||||
};
|
||||
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
for (const name of ['ABC_PROJECT_ID', 'ABC_CHANNEL_ID']) {
|
||||
if (!uuid.test(vars[name])) throw new Error(name + ' must be a UUID');
|
||||
}
|
||||
const revision = required(env, 'DEPLOY_COMMIT_SHA');
|
||||
if (!/^[0-9a-f]{40}$/.test(revision)) throw new Error('DEPLOY_COMMIT_SHA must be a full lowercase Git commit SHA');
|
||||
vars.QA_ASSET_PREFIX = 'releases/' + revision;
|
||||
return { ...productionTarget, vars, secrets };
|
||||
}
|
||||
+11
-2
@@ -2,12 +2,21 @@
|
||||
set -euo pipefail
|
||||
|
||||
bucket="${1:-baron-qa-pages}"
|
||||
config="${2:-wrangler.toml}"
|
||||
prefix="${3:-}"
|
||||
|
||||
if [ "$bucket" = "baron-qa-write" ]; then
|
||||
if [ "$config" != "wrangler.production.toml" ] || [[ ! "$prefix" =~ ^releases/[0-9a-f]{40}$ ]]; then
|
||||
echo "Production uploads require the production config and a full commit release prefix" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
upload() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
local key="${prefix:+$prefix/}$2"
|
||||
local content_type="$3"
|
||||
npx wrangler r2 object put "$bucket/$key" --file "$file" --content-type "$content_type" --remote
|
||||
npx --no-install wrangler r2 object put "$bucket/$key" --config "$config" --file "$file" --content-type "$content_type" --remote
|
||||
}
|
||||
|
||||
upload egbim/index.html egbim/index.html text/html
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { productionTarget } from './production-settings.mjs';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { setTimeout as wait } from 'node:timers/promises';
|
||||
|
||||
export async function verifyProduction(fetchImpl = fetch, revision = process.env.DEPLOY_COMMIT_SHA) {
|
||||
const origin = productionTarget.origin;
|
||||
const request = (path) => fetchImpl(origin + path, { redirect: 'manual', signal: AbortSignal.timeout(15000), headers: { 'cache-control': 'no-cache' } });
|
||||
const session = await request('/auth/session');
|
||||
if (!session.ok) throw new Error('Production session endpoint failed');
|
||||
const body = await session.json();
|
||||
if (body.authenticated !== false || body.user !== null) throw new Error('Production session endpoint returned an unexpected anonymous session');
|
||||
const asset = await request('/assets/styles.css');
|
||||
if (!asset.ok || !String(asset.headers.get('content-type')).includes('text/css') || !(await asset.text()).trim()) {
|
||||
throw new Error('Production stylesheet is unavailable');
|
||||
}
|
||||
if (revision && asset.headers.get('x-qa-release') !== 'releases/' + revision) {
|
||||
throw new Error('Production stylesheet does not match the deployed commit');
|
||||
}
|
||||
const protectedPage = await request('/egbim/write.html');
|
||||
const login = new URL(protectedPage.headers.get('location') || '/', origin);
|
||||
if (protectedPage.status !== 302 || login.origin !== origin || login.pathname !== '/auth/login') {
|
||||
throw new Error('Production write page must redirect anonymous visitors to login');
|
||||
}
|
||||
console.log('Production HTTP checks passed: session, stylesheet, protected write page.');
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
for (let attempt = 1; attempt <= 5; attempt++) {
|
||||
try { await verifyProduction(); break; } catch (error) {
|
||||
if (attempt === 5) {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log('Waiting for production deployment propagation (' + attempt + '/5).');
|
||||
await wait(5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -42,7 +42,8 @@ export default {
|
||||
return Response.redirect(new URL(url.pathname + '/', url), 301);
|
||||
}
|
||||
|
||||
const objectKey = objectKeyForPath(url.pathname);
|
||||
const assetPrefix = String(env.QA_ASSET_PREFIX || '').replace(/^\/+|\/+$/g, '');
|
||||
const objectKey = (assetPrefix ? assetPrefix + '/' : '') + objectKeyForPath(url.pathname);
|
||||
let object = await env.QA_BUCKET.get(objectKey);
|
||||
if (!object && objectKey.endsWith('/index.html')) {
|
||||
object = await env.QA_BUCKET.get(objectKey.replace(/\/index\.html$/, 'index.html'));
|
||||
@@ -54,6 +55,7 @@ export default {
|
||||
headers.set('etag', object.httpEtag);
|
||||
headers.set('x-content-type-options', 'nosniff');
|
||||
headers.set('referrer-policy', 'strict-origin-when-cross-origin');
|
||||
if (assetPrefix) headers.set('x-qa-release', assetPrefix);
|
||||
headers.set('cache-control', url.pathname.endsWith('.html') || url.pathname.endsWith('.js') ? 'no-cache' : 'public, max-age=3600');
|
||||
return new Response(request.method === 'HEAD' ? null : object.body, { headers });
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { productionSettings } from '../scripts/production-settings.mjs';
|
||||
import { verifyProduction } from '../scripts/verify-production.mjs';
|
||||
import worker from '../src/index.js';
|
||||
|
||||
const SHA = 'a'.repeat(40);
|
||||
const fixture = () => ({
|
||||
CLOUDFLARE_API_TOKEN: 'fixture-cloudflare-token', SESSION_SECRET: 'fixture-session-secret-'.repeat(3),
|
||||
ABC_API_KEY: 'fixture-abc-key', AUTH_CLIENT_ID: 'production-client', ABC_API_BASE_URL: 'https://feedback.example.test/',
|
||||
ABC_PROJECT_ID: '01900000-0000-7000-8000-000000000001', ABC_CHANNEL_ID: '01900000-0000-7000-8000-000000000002',
|
||||
ORG_CONTEXT_ENABLED: 'true', ORG_CONTEXT_TENANT_IDS: '["038326b6-954a-48a7-a85f-efd83f62b82a"]',
|
||||
ORG_CONTEXT_KEY_ID: 'fixture-org-key', ORG_CONTEXT_KEY_SECRET: 'fixture-org-secret', DEPLOY_COMMIT_SHA: SHA
|
||||
});
|
||||
|
||||
test('production preserves dashboard-managed domains without requesting zone routing changes', () => {
|
||||
const config = readFileSync(new URL('../wrangler.production.toml', import.meta.url), 'utf8');
|
||||
assert.doesNotMatch(config, /^\s*routes?\s*=/m);
|
||||
assert.match(config, /^workers_dev\s*=\s*false\s*$/m);
|
||||
});
|
||||
|
||||
test('production requires its own credentials, target IDs, organization lookup and immutable release', () => {
|
||||
for (const name of Object.keys(fixture())) {
|
||||
const env = fixture();
|
||||
delete env[name];
|
||||
assert.throws(() => productionSettings(env), new RegExp(name === 'ORG_CONTEXT_TENANT_IDS' ? 'org_context_invalid_tenants' : name));
|
||||
}
|
||||
assert.throws(() => productionSettings({ ...fixture(), ORG_CONTEXT_ENABLED: 'false' }), /must be true/);
|
||||
assert.throws(() => productionSettings({ ...fixture(), SESSION_SECRET: 'short' }), /at least 32/);
|
||||
assert.throws(() => productionSettings({ ...fixture(), ABC_CHANNEL_ID: '123' }), /UUID/);
|
||||
assert.throws(() => productionSettings({ ...fixture(), DEPLOY_COMMIT_SHA: '../staging' }), /full lowercase/);
|
||||
assert.throws(() => productionSettings({ ...fixture(), ABC_API_BASE_URL: 'https://secret:password@example.test' }), /HTTPS origin/);
|
||||
const settings = productionSettings(fixture());
|
||||
assert.equal(settings.worker, 'baron-qa-gateway-prod');
|
||||
assert.equal(settings.bucket, 'baron-qa-write');
|
||||
assert.equal(settings.vars.ABC_API_BASE_URL, 'https://feedback.example.test');
|
||||
assert.equal(settings.vars.QA_ASSET_PREFIX, 'releases/' + SHA);
|
||||
for (const name of Object.keys(settings.secrets)) assert.equal(settings.vars[name], undefined);
|
||||
assert.equal(settings.secrets.CLOUDFLARE_API_TOKEN, undefined);
|
||||
});
|
||||
|
||||
function withFakeWrangler(body) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'qa-deploy-test-'));
|
||||
const capture = join(dir, 'commands.jsonl');
|
||||
writeFileSync(join(dir, 'npx'), `#!/usr/bin/env node
|
||||
const fs = require('node:fs');
|
||||
const args = process.argv.slice(2);
|
||||
const index = args.indexOf('--secrets-file');
|
||||
const path = index === -1 ? null : args[index + 1];
|
||||
fs.appendFileSync(process.env.QA_TEST_CAPTURE, JSON.stringify({ args, path,
|
||||
secrets: path ? JSON.parse(fs.readFileSync(path)) : null,
|
||||
mode: path ? fs.statSync(path).mode & 0o777 : null }) + '\\n');
|
||||
process.exit(Number(process.env.QA_TEST_EXIT || 0));
|
||||
`, { mode: 0o700 });
|
||||
const env = { ...fixture(), PATH: dir + ':' + process.env.PATH, QA_TEST_CAPTURE: capture };
|
||||
try { body({ dir, env, capture }); } finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
}
|
||||
|
||||
test('production validation and dry run never register secrets or deploy remotely', () => withFakeWrangler(({ env, capture }) => {
|
||||
const script = fileURLToPath(new URL('../scripts/deploy-production.mjs', import.meta.url));
|
||||
const validation = spawnSync(process.execPath, [script, '--validate-only'], { env, encoding: 'utf8' });
|
||||
assert.equal(validation.status, 0, validation.stderr);
|
||||
assert.equal(existsSync(capture), false);
|
||||
const dry = spawnSync(process.execPath, [script, '--dry-run'], { env, encoding: 'utf8' });
|
||||
assert.equal(dry.status, 0, dry.stderr);
|
||||
const command = JSON.parse(readFileSync(capture, 'utf8'));
|
||||
assert.ok(command.args.includes('--dry-run'));
|
||||
assert.ok(command.args.includes('wrangler.production.toml'));
|
||||
assert.ok(!command.args.some((arg) => ['--route', '--routes', '--domain', '--domains'].includes(arg)));
|
||||
assert.equal(command.path, null);
|
||||
for (const name of ['SESSION_SECRET', 'ABC_API_KEY', 'ORG_CONTEXT_KEY_SECRET']) {
|
||||
assert.ok(!JSON.stringify(command.args).includes(env[name]));
|
||||
assert.ok(!validation.stdout.includes(env[name]));
|
||||
}
|
||||
}));
|
||||
|
||||
for (const exitCode of [0, 1]) {
|
||||
test(`production uploads secrets with code and removes the private temporary file after exit ${exitCode}`, () => withFakeWrangler(({ env, capture }) => {
|
||||
const result = spawnSync(process.execPath, [fileURLToPath(new URL('../scripts/deploy-production.mjs', import.meta.url))], {
|
||||
env: { ...env, QA_TEST_EXIT: String(exitCode) }, encoding: 'utf8'
|
||||
});
|
||||
assert.equal(result.status, exitCode);
|
||||
const command = JSON.parse(readFileSync(capture, 'utf8'));
|
||||
assert.equal(command.secrets.ABC_API_KEY, env.ABC_API_KEY);
|
||||
assert.equal(command.mode, 0o600);
|
||||
assert.equal(command.secrets.CLOUDFLARE_API_TOKEN, undefined);
|
||||
assert.ok(!JSON.stringify(command.args).includes(env.ORG_CONTEXT_KEY_SECRET));
|
||||
assert.equal(existsSync(command.path), false);
|
||||
}));
|
||||
}
|
||||
|
||||
test('production uploader rejects root uploads and sends every file to the selected commit path', () => withFakeWrangler(({ dir, env, capture }) => {
|
||||
const script = fileURLToPath(new URL('../scripts/upload-r2.sh', import.meta.url));
|
||||
const refused = spawnSync('bash', [script, 'baron-qa-write'], { env, cwd: dir });
|
||||
assert.equal(refused.status, 1);
|
||||
assert.equal(existsSync(capture), false);
|
||||
mkdirSync(join(dir, 'egbim'));
|
||||
mkdirSync(join(dir, 'assets'));
|
||||
for (const file of ['index.html', 'write.html', 'detail.html', 'app.js', 'config.js']) writeFileSync(join(dir, 'egbim', file), 'fixture');
|
||||
writeFileSync(join(dir, 'assets/styles.css'), 'body {}');
|
||||
const uploaded = spawnSync('bash', [script, 'baron-qa-write', 'wrangler.production.toml', 'releases/' + SHA], { env, cwd: dir, encoding: 'utf8' });
|
||||
assert.equal(uploaded.status, 0, uploaded.stderr);
|
||||
const commands = readFileSync(capture, 'utf8').trim().split('\n').map(JSON.parse);
|
||||
assert.equal(commands.length, 6);
|
||||
for (const command of commands) {
|
||||
assert.ok(command.args[5].startsWith('baron-qa-write/releases/' + SHA + '/'));
|
||||
assert.ok(command.args.includes('wrangler.production.toml'));
|
||||
assert.ok(!command.args.includes('baron-qa-pages'));
|
||||
}
|
||||
writeFileSync(capture, '');
|
||||
const staging = spawnSync('bash', [script], { env, cwd: dir, encoding: 'utf8' });
|
||||
assert.equal(staging.status, 0, staging.stderr);
|
||||
for (const command of readFileSync(capture, 'utf8').trim().split('\n').map(JSON.parse)) {
|
||||
assert.ok(command.args[5].startsWith('baron-qa-pages/'));
|
||||
assert.ok(!command.args[5].includes('releases/'));
|
||||
assert.ok(command.args.includes('wrangler.toml'));
|
||||
}
|
||||
}));
|
||||
|
||||
for (const prefix of ['', 'releases/' + SHA]) {
|
||||
test(`Worker uses ${prefix ? 'release-specific' : 'legacy staging'} asset keys`, async () => {
|
||||
const response = await worker.fetch(new Request('https://qa.example.test/assets/styles.css'), {
|
||||
QA_ASSET_PREFIX: prefix, QA_BUCKET: { async get(key) {
|
||||
assert.equal(key, (prefix ? prefix + '/' : '') + 'assets/styles.css');
|
||||
return { body: 'body {}', httpEtag: 'fixture', writeHttpMetadata(headers) { headers.set('content-type', 'text/css'); } };
|
||||
} }
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.headers.get('x-qa-release'), prefix || null);
|
||||
});
|
||||
}
|
||||
|
||||
test('post-deploy verification rejects Hello World, old releases and unprotected write pages', async () => {
|
||||
await assert.rejects(verifyProduction(async () => new Response('Hello World!'), SHA));
|
||||
const http = (oldRelease = false, unprotected = false) => async (url) => {
|
||||
if (url.endsWith('/auth/session')) return Response.json({ authenticated: false, user: null });
|
||||
if (url.endsWith('/assets/styles.css')) return new Response('body {}', { headers: { 'content-type': 'text/css', 'x-qa-release': oldRelease ? 'releases/old' : 'releases/' + SHA } });
|
||||
return new Response('', { status: unprotected ? 200 : 302, headers: { location: '/auth/login?return_url=write' } });
|
||||
};
|
||||
await assert.rejects(verifyProduction(http(true), SHA), /deployed commit/);
|
||||
await assert.rejects(verifyProduction(http(false, true), SHA), /redirect/);
|
||||
await verifyProduction(http(), SHA);
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
name = "baron-qa-gateway-prod"
|
||||
main = "src/index.js"
|
||||
compatibility_date = "2026-09-18"
|
||||
account_id = "81fa2d48964d31dd0da9558f9ce601d1"
|
||||
workers_dev = false
|
||||
preview_urls = false
|
||||
|
||||
# qa.baroncs.co.kr is already attached to this Worker in the Cloudflare dashboard.
|
||||
# Keep route/routes absent so deployments preserve that connection without zone writes.
|
||||
|
||||
[vars]
|
||||
AUTH_REDIRECT_URI = "https://qa.baroncs.co.kr/auth/callback"
|
||||
AUTH_POST_LOGOUT_REDIRECT_URI = "https://qa.baroncs.co.kr/"
|
||||
AUTH_SCOPE = "openid tenants profile email"
|
||||
AUTH_AUTHORIZE_URL = "https://sso.hmac.kr/oidc/oauth2/auth"
|
||||
AUTH_TOKEN_URL = "https://sso.hmac.kr/oidc/oauth2/token"
|
||||
AUTH_USERINFO_URL = "https://sso.hmac.kr/oidc/userinfo"
|
||||
SESSION_COOKIE_NAME = "baron_qa_prod_session_v1"
|
||||
OAUTH_COOKIE_NAME = "baron_qa_prod_oauth_v1"
|
||||
SESSION_TTL_SECONDS = "3600"
|
||||
ABC_SOURCE_NAMESPACE = "EGBIM_QA_PROD"
|
||||
ABC_IDEMPOTENCY_CONSUMER = "EGBIM_QA_PROD"
|
||||
PUBLIC_EXACT_PATHS = "/,/index.html,/favicon.ico"
|
||||
PUBLIC_PREFIXES = "/egbim/,/tova/,/gaia/,/shared/,/assets/,/auth/"
|
||||
PROTECTED_PREFIXES = "/egbim/,/tova/,/gaia/"
|
||||
PROTECTED_EXACT_PATHS = "/egbim/write.html,/egbim/detail.html"
|
||||
INTERNAL_ONLY_PREFIXES = "/protected/"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "QA_BUCKET"
|
||||
bucket_name = "baron-qa-write"
|
||||
|
||||
[observability]
|
||||
enabled = true
|
||||
|
||||
[observability.logs]
|
||||
enabled = true
|
||||
Reference in new issue
Block a user