release: dev → main — Cloud Run 자동 배포 전환 + 신규 스킬 다수 (#276)

* docs(flight-ticket-search): register skill in README table and add feature guide

PR #224 머지 시 README "어떤 걸 할 수 있나" 표와 "포함된 기능" 리스트, 그리고
docs/features/flight-ticket-search.md 가이드가 등록되지 않아 main에 있는 다른
모든 스킬과 달리 사용자/에이전트가 README만 봐서는 이 스킬을 발견할 수 없는
상태였다. 누락분을 hotfix로 보강한다.

- README 표에 `flight-ticket-search` 행 추가 (마이리얼트립 옆 항공 클러스터)
- README "포함된 기능" 리스트에 가이드 링크 추가
- docs/features/flight-ticket-search.md 신규 작성:
  · 사용 시나리오, 구현 표면(fast-flights==2.2, 사용자 venv 격리)
  · search / compare-month / compare-range / compare-years CLI 예시
  · 응답 필드, IATA 입력 가이드, 예약 링크 정책
  · 검증된 노선 목록, 실패 모드, 비범위, 출처

검증:
- node --test scripts/skill-docs.test.js → 138/138 pass
- ./scripts/validate-skills.sh → skill layout looks valid

코드 변경 없음 → changeset 불필요.

* feat(daiso-product-search): replace blocked-API fallback with Bearer token auth

selStrPkupStck는 더 이상 차단 상태가 아니며, /api/auth/request로 비로그인 JWT를
발급받아 AES-128-CBC(키: PRE_AUTH_ENC_KEY)로 암호화한 Bearer 토큰으로 접근한다.
403 응답 시 토큰을 재발급해 1회 재시도한다. pickupEligibility(selPkupStr) 폴백
로직은 제거했다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Preserve Daiso pickup answers when Bearer auth degrades

Keep exact stock lookup on the official Bearer-token path while restoring the public selPkupStr fallback for repeated auth blocks.

Constraint: PR #250 review required Bearer auth to remain primary without removing the resilient pickup eligibility API.

Rejected: Throwing after the retry | it collapses callers back to a brittle single upstream-auth dependency.

Confidence: high

Scope-risk: narrow

Directive: Keep pickupStock quantity semantics separate from pickupEligibility yes/no fallback.

Tested: node --test packages/daiso-product-search/test/index.test.js; npm test --workspace daiso-product-search; npm run lint --workspace daiso-product-search; npm run ci; live lookupStoreProductAvailability smoke for 강남역2호점 / VT 리들샷 100.

Not-tested: Live forced 403 from Daiso upstream; covered with injected fetch regression tests.

* Prove Daiso stock retry sends auth headers

Strengthen the retry regression so the Bearer-token contract cannot regress while still returning success from mocked stock responses.\n\nConstraint: PR #250 review requested explicit Authorization, X-DM-UID, and request body assertions on the retry path.\nRejected: Counting requests only | it allowed header/body regressions to pass.\nConfidence: high\nScope-risk: narrow\nDirective: Keep auth-header assertions on both initial and retry stock requests when editing this flow.\nTested: node --test packages/daiso-product-search/test/index.test.js; npm test --workspace daiso-product-search; npm run lint --workspace daiso-product-search; npm run ci; live lookupStoreProductAvailability smoke for 강남역2호점 / VT 리들샷 100; repeated-403 fixture probe.\nNot-tested: Live repeated upstream 403 because forcing Daiso production auth failure is not available without changing upstream state.

* Preserve Daiso caller headers through Bearer stock lookup

Keep advanced caller headers on the authenticated stock endpoint while generated Bearer and X-DM-UID values remain authoritative. Document the degraded selPkupStr fallback order in skill and source docs so the public workflow matches the restored API surface.\n\nConstraint: PR #250 review required resilient Bearer-primary stock lookup plus selPkupStr fallback and header/body contract coverage.\nRejected: Replacing caller headers with only auth headers | It regressed tracing/test-control header pass-through.\nConfidence: high\nScope-risk: narrow\nDirective: Keep Authorization and X-DM-UID generated by the auth flow even when callers provide same-named headers.\nTested: node --test packages/daiso-product-search/test/index.test.js; npm test --workspace daiso-product-search; npm run lint --workspace daiso-product-search; node --test scripts/skill-docs.test.js; npm run ci; live lookupStoreProductAvailability smoke for 강남역2호점 / VT 리들샷 100.\nNot-tested: Forced live upstream repeated 403; covered by injected fixture tests.

* fix(danawa-price-search): capture .ico.* payment-condition badges and surface as row labels

PR #226 row 파서에 결제조건 배지(`.ico.cash`/`.ico.point`/`.ico.coupon`/`.ico.card`) selector가 누락돼, 카드 결제 불가능한 현금/쿠폰/포인트 전용가가 일반 최저가로 노출되는 결함을 고친다.

- `offers()` row 파싱부에 결제조건 배지 화이트리스트 캡처 블록 추가 (클래스 `cash`/`point`/`coupon`/`discount`/`card`/`membership` 또는 텍스트 `현금`/`포인트`/`쿠폰`/`할인`만 인정 — 빠른배송/안내/상품리뷰 노이즈 차단)
- row dict 신규 필드 6개: `payment_badges`, `cash_only`, `point_only`, `coupon_only`, `card_only_badge`, `is_conditional_price`
- 반환 dict에 `normal_count`, `conditional_count` 추가
- `SKILL.md` / `docs/features/danawa-price-search.md` 갱신 (Output shape · Response style · Workflow · Failure modes에 결제조건 정책과 표 예시 명시)

정렬 정책은 그대로 `total_price` 단일 기준이며, 결제조건은 row 단위 플래그/라벨로만 노출해 호출자가 결제수단에 맞춰 직접 판단하도록 한다.

회귀 (pcode=75001853, 갤럭시 S25 256GB 자급제 `offers --limit 5`):
- 1위 킴스클럽 979,000원 / `cash_only=True` / `payment_badges=["현금"]`
- 2위 롯데ON 1,072,080원 / `cash_only=False` / `payment_badges=[]`
- 3~5위 일반가 row 모두 `payment_badges` 빈 리스트 (노이즈 0건)

Closes #252

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Ensure captured Danawa payment badges stay conditional

Classify every whitelisted payment badge into normalized condition types so callers cannot count captured discount, membership, or text-only card rows as normal prices.

Constraint: PR #253 review required TDD follow-up on feature/#252 without changing total_price sorting.\nRejected: Removing discount and membership from the whitelist | would lose Danawa condition labels already captured by the parser.\nConfidence: high\nScope-risk: narrow\nDirective: Keep payment_badge whitelist and payment_condition_types in sync whenever adding new badge classes or text keywords.\nTested: PYTHONPATH=.:scripts python3 -m unittest scripts.test_danawa_price_search; live offers 75001853 --limit 5; npm run lint; npm run typecheck; npm run test; architect verification CLEAR.\nNot-tested: Danawa markup variants not represented by current live page or synthetic badge fixtures.

* Keep icon-only Danawa payment badges visible

Class-only Danawa payment icons can carry eligibility information without visible text, so synthesize display labels from the same normalized condition map used for types and booleans. This keeps raw row labels, condition fields, and returned-window counts aligned for downstream table renderers.\n\nConstraint: PR #253 review follow-up requires TDD coverage before parser changes.\nRejected: Leaving payment_badges text-only | icon-only conditional rows would still render without visible payment labels.\nConfidence: high\nScope-risk: narrow\nDirective: Derive future payment badge labels, types, and booleans from one canonical mapping.\nTested: python3 -m py_compile danawa-price-search/scripts/danawa_search.py scripts/test_danawa_price_search.py; PYTHONPATH=.:scripts python3 -m unittest scripts.test_danawa_price_search; python3 danawa-price-search/scripts/danawa_search.py offers 75001853 --limit 5; npm run lint; npm run typecheck; npm run test\nNot-tested: Danawa icon-only markup was verified with synthetic fixtures rather than a live page snapshot.

* Merge pull request #249 from NomaDamas/feature/#248

Feature/#248

* Restore SH notice lookup without proxy policy drift

Reintroduce SH notice search as a direct public HTML client so the skill complies with the free-API proxy boundary while preserving verifiable keyword, pagination, and attachment behavior.

Constraint: i-sh.co.kr board is public unauthenticated HTML, so k-skill-proxy must not host the scraper.\nRejected: Re-adding /v1/sh-notice proxy routes | public HTML scraping in proxy violates repository policy.\nConfidence: high\nScope-risk: moderate\nDirective: Keep SH public HTML access local/direct unless a key-required official free API is discovered and documented.\nTested: npm run ci; npm run lint --workspace sh-notice-search; npm test --workspace sh-notice-search; live SH smoke for 행복주택, 매입임대, 신혼희망타운, page 1/page 5, 1/6/9/11/0 attachment details.\nNot-tested: authenticated SH flows, 청약 application/submission, direct attachment downloads.

* Preserve public SH helper semantics

Route exported URL builders through the same normalization as the CLI/API so natural category aliases cannot bypass srchTp title narrowing or category mapping.\n\nConstraint: PR #254 review found exported helper callers could pass Korean/English public category inputs and get broken or broadened SH URLs.\nRejected: Keep normalized-only fast paths | exported helpers are public API and must protect natural inputs.\nConfidence: high\nScope-risk: narrow\nDirective: Keep exported helper behavior aligned with normalizeSearchOptions and normalizeDetailOptions when adding new public aliases.\nTested: npm test --workspace sh-notice-search; npm run lint --workspace sh-notice-search; npm run typecheck; npm run ci; node helper smoke for 임대 search/detail URLs.\nNot-tested: Live SH network smoke was not rerun for this helper-only change.

* Preserve SH parser helper aliases

Route exported parser helpers through the same public normalizers used by the SH fetch and URL-builder APIs so natural category aliases stay consistent across the package surface.

Constraint: PR #254 Round 2 review found parser helpers still treated raw category aliases as pre-normalized inputs.
Rejected: Keep parser helpers normalized-only | inconsistent with exported URL builders and public helper ergonomics.
Confidence: high
Scope-risk: narrow
Directive: Keep exported SH helper entry points on canonical normalizeSearchOptions/normalizeDetailOptions unless a separate internal-only API is introduced.
Tested: npm test --workspace sh-notice-search; npm run lint --workspace sh-notice-search; npm run typecheck; npm pack --workspace sh-notice-search --dry-run; npm run ci; parser smoke for Korean 임대 list/detail helpers; Ralph architect verification CLEAR; post-deslop regression npm run ci
Not-tested: Live SH network smoke for this follow-up; fixture and injected-fetch coverage exercised the helper contract.

* Make SH parser failures explicit

Warn when SH returns block or maintenance HTML without the expected public board markup, and constrain exposed preview links to the SH converter origin/path.\n\nConstraint: Round 3 review required TDD coverage for block/maintenance HTML and untrusted preview URLs.\nRejected: Throwing on unexpected HTML | Existing parser helpers return partial fixture-friendly results, so warnings preserve compatibility while exposing failure evidence.\nConfidence: high\nScope-risk: narrow\nDirective: Keep SH public HTML lookup direct; do not add proxy routing unless a key-required official free API is adopted.\nTested: npm run lint --workspace sh-notice-search; npm test --workspace sh-notice-search; npm run typecheck; npm pack --workspace sh-notice-search --dry-run; npm run ci; Node smoke for blocked HTML warnings and external preview filtering.\nNot-tested: Live blocked/NetFunnel SH response, because no live blocked page was available during implementation.

* ci: install beautifulsoup4 so danawa price search tests can import bs4

The new scripts/test_danawa_price_search.py imports danawa_search.py,
which requires beautifulsoup4. CI only runs npm ci, so the bs4 import
fails with 'beautifulsoup4 is required: python -m pip install
beautifulsoup4' and the validate job exits with code 1.

Install beautifulsoup4 via pip before running npm run ci so the
Python test suite can import danawa_search and run the new payment
badge regression tests.

* Revert "ci: install beautifulsoup4 so danawa price search tests can import bs4"

This reverts commit 8330e5adf7.

* test: install beautifulsoup4 inside npm test before Python tests

The new scripts/test_danawa_price_search.py imports danawa_search.py,
which requires beautifulsoup4. CI runs npm ci + npm run ci and does
not install Python packages, so the bs4 import fails at module load.

Install beautifulsoup4 via 'pip install --user' as the first step of
the test script so it is available when Python unittests import the
danawa helper. Local dev environments are unaffected because pip
install is idempotent and quiet.

* feat(qa-bot): add k-skill-qa-bot under tools/

External macOS daemon that clones NomaDamas/k-skill main every 3 days, runs
each skill through codex exec, has an LLM judge grade pass/fail/skip via
codex exec --output-schema, and files dedup'd GitHub issues for true failures.

Layout:
- install.sh copies tools/k-skill-qa-bot/ to ~/.local/share/k-skill-qa-bot/
  and registers a LaunchAgent at ~/Library/LaunchAgents/.
- update-clone.sh has a hard guard: refuses any K_SKILL_CLONE outside
  K_QA_HOME/k-skill-clone unless ALLOW_EXTERNAL_CLONE_TARGET=1.
- Force-skip 10 destructive/login-required skills (ktx-booking, srt-booking,
  catchtable-sniper, kakaotalk-mac, hipass-receipt, toss-securities, etc.)
  so the bot never triggers reservation abuse.
- Deprecated skills (strike-through + 지원 중단 in README) auto-detected
  and skipped, never failed.
- First-run safety: CREATE_ISSUES=false by default.
- mkdir-based concurrency lock with atomic stale reclaim.
- Issue dedup: sha1(skill_name + symptom_class)[:12] body marker.
- Deterministic gates override LLM judge to FAIL on exit_code != 0, missing
  VERDICT line, or near-timeout duration.

* Support nearby ER status checks

Add an E-Gen based emergency-room skill that resolves a user location, queries the public nearby emergency-room list, and reports operation flags while documenting that exact remaining bed counts are not exposed by this surface.

Constraint: Issue #255 requested NEMC emergency bed status using public monitoring/E-Gen surfaces.
Rejected: Scraping private monitoring dashboards or claiming exact bed utilization | public endpoints expose operation flags, not per-hospital remaining bed counts.
Confidence: high
Scope-risk: narrow
Directive: Preserve the public-data limitation text unless a verified official bed-count endpoint is added.
Tested: npm run lint --workspace emergency-room-beds; npm test --workspace emergency-room-beds; node --test scripts/skill-docs.test.js; npm run typecheck; npm pack --workspace emergency-room-beds --dry-run; ./scripts/validate-skills.sh; live E-Gen coordinate smoke.
Not-tested: npm run ci end-to-end due local Python 3.14 pip/pyexpat import error before tests.

* Prevent ER status ambiguity from reaching users

Constraint: Health-adjacent public E-Gen/Kakao data can be absent, delayed, schema-drifted, or partially unknown.

Rejected: Mapping all non-Y operation flags to false | It misrepresents missing upstream data as a negative operating status.

Rejected: Treating unknown E-Gen payloads as empty results | It hides upstream failure behind a false no-results response.

Confidence: high

Scope-risk: narrow

Directive: Keep unknown health availability data explicit and preserve upstream failure evidence.

Tested: npm run lint --workspace emergency-room-beds; npm test --workspace emergency-room-beds; node --test scripts/skill-docs.test.js; npm run typecheck; npm pack --workspace emergency-room-beds --dry-run; ./scripts/validate-skills.sh; direct Node smoke for tri-state/schema/coordinate guards.

Not-tested: npm run ci due pre-existing local Python 3.14 pyexpat/libexpat bootstrap failure noted on PR.

Co-authored-by: OmX <omx@oh-my-codex.dev>

* fix(ci): exclude tools/ from skill validator

The tools/ directory hosts repo tooling (e.g. k-skill-qa-bot), not
skills, so validate-skills.sh should skip it like other non-skill
top-level directories.

* 영화관 검색 스킬 추가 (#260)

* Add korean cinema search skill

* Document playDate for cinema skill

* feat(kstartup-search): 창업진흥원 K-Startup 조회 스킬 + 프록시 라우트 4종 (#259)

* feat(kstartup-search): 창업진흥원 K-Startup 조회 스킬과 프록시 라우트 추가

공공데이터포털 dataset 15125364 (창업진흥원_K-Startup(사업소개,사업공고,콘텐츠 등)_조회서비스) 의
4개 endpoint 를 k-skill-proxy 경유로 조회하는 스킬을 추가한다.

- 신규 라우트: GET /v1/kstartup/{business-info,announcements,contents,statistics}
  - 각각 getBusinessInformation01/getAnnouncementInformation01/getContentInformation01/
    getStatisticalInformation01 으로 중계
  - ServiceKey 는 서버 측 DATA_GO_KR_API_KEY 로 주입, returnType=json 강제
  - 정상 응답만 캐시, data.go.kr 에러 envelope (resultCode != "00", errMsg 등) 은 캐시 우회
- helper: kstartup-search/scripts/run_kstartup.py (stdlib only)
  - 일반 조회는 hosted proxy 사용 → 사용자 키 불필요
  - --direct 옵션은 사용자가 본인 KSKILL_KSTARTUP_API_KEY (혹은 DATA_GO_KR_API_KEY) 로
    upstream 직접 호출 + --dry-run 시 키 redact
- 입력 검증: page/perPage 정수·범위, YYYYMMDD 날짜 + 시작일 ≤ 종료일, Y/N 대문자화,
  텍스트 필드 길이 상한, biz_yr 4자리
- 테스트: k-skill-proxy 서버 테스트 10건 신규 (normalizer, 라우트, 캐시 분리,
  returnType=json 강제, 503/400/502, 키 누수 회귀), Python unittest 13건
- 문서: SKILL.md, docs/features/kstartup-search.md, README 표/리스트,
  docs/sources.md, .changeset/kstartup-search.md (k-skill-proxy minor)

* docs(kstartup-search): docs/setup·security·k-skill-setup·proxy README 에 K-Startup 항목 추가

seoul-density · KOSIS · NTS 선례와 동일한 위치·문구로 다음을 보강한다.

- docs/setup.md: dotenv 예시에 KSKILL_KSTARTUP_API_KEY 추가, credential 표에 K-Startup 행 추가, "다음에 볼 문서" 리스트 추가
- docs/security-and-secrets.md: standard variable names 에 KSKILL_KSTARTUP_API_KEY 추가, hosted proxy 사용 스킬 목록·proxy 운영 prose 에 K-Startup 추가, dotenv 예시 추가
- k-skill-setup/SKILL.md: credential resolution prose 와 시크릿 요약 표에 K-Startup 안내 추가
- packages/k-skill-proxy/README.md: 라우트 목록에 /v1/kstartup/{business-info,announcements,contents,statistics} 추가
- docs/features/k-skill-proxy.md: 라우트 목록에 같은 4개 추가

* fix(kstartup-search): strict calendar-date validation in Python helper

validate_yyyymmdd() previously only checked month in [1,12] and day in [1,31],
which accepted impossible dates like 20240230 or 20240431 in --direct mode.
The proxy-side normalizer in packages/k-skill-proxy/src/kstartup.js already
uses Date.UTC() to reject such inputs, so this aligns the --direct path with
the proxy path and eliminates validator drift.

Uses datetime.date(year, month, day) and raises HelperError on ValueError.

Adds regression test covering impossible calendar dates (Feb 30, Apr 31,
month 13, day 0) and the leap-year boundary (2024-02-29 valid, 2023-02-29
not).

---------

Co-authored-by: Jeffrey (Dongkyu) Kim <vkehfdl1@gmail.com>

* fix(qa-bot): upgrade judge to gpt-5.5 and run codex with sandbox bypass

PR #257 follow-up. Two changes:

1. JUDGE_MODEL default: gpt-5.4-mini -> gpt-5.5

   The cheaper judge was misclassifying every wrong-output verdict because
   the offline matcher fell through to the dumb 'VERDICT: FAIL in transcript'
   check. Re-running the same 10 historical fail cases with gpt-5.5 +
   real LLM judge correctly reclassified 7 of them as pass (the codex agent
   actually accomplished the skill goal) and the remaining 3 as
   network-error / partial-success / skip with accurate reasons.

2. Drop -s read-only, add --dangerously-bypass-approvals-and-sandbox

   The read-only codex sandbox was triggering spurious DNS resolution
   failures inside the test runs (host blocked at the syscall level even
   for legitimate proxy / public-API calls). Live re-test with the bypass
   flag and provider pin produced clean transcripts: cheap-gas-nearby,
   daangn-realty-search, han-river-water-level, naver-news-search,
   naver-shopping-search, seoul-density, seoul-subway-arrival all PASS.
   The QA bot is sandboxed externally by launchd anyway.

3. New CODEX_PROVIDER env (default: openai)

   Lets users pin the codex model_provider explicitly so the bot does not
   accidentally route through a private OpenAI-compatible proxy that may
   not have keys registered for all model names.

* Add Ohou today deal skill

* fix spacing in package.json

* fix(qa-bot): per-skill test_prompt overrides and smarter judge

11 skills that need specific inputs (not just a 'demonstrate' query) now
ship with a hardcoded test_prompt in config/skill-overrides.yml:

  flight-ticket-search           ICN -> NRT, 2026-08-20 one-way
  nts-business-registration      124-81-00998 (Samsung Electronics)
  korean-stock-search            005930 Samsung 5-day quote
  joseon-sillok-search           키워드 훈민정음
  korean-law-search              산업안전보건법 제5조
  library-book-search            코스모스 칼 세이건
  lotto-results                  latest round
  k-schoollunch-menu             서울특별시교육청 초등학교 오늘 식단
  delivery-tracking              CJ dummy invoice (negative case ok)
  ticket-availability            YES24 / 인터파크 sample
  zipcode-search                 서울특별시 강남구 테헤란로 152

These were previously synthesized from the SKILL.md first When-to-use bullet,
which is a one-line teaser without concrete inputs. The agent would then
either ask the user for the missing input (partial-success) or fall back
to a generic demo (often producing a VERDICT: FAIL response). Both got
mis-classified as fail by the judge.

qa_utils.synthesize_test_prompt now honors default_inputs.test_prompt as a
verbatim override (only appending the VERDICT line if the override does not
already include it).

Two additional fixes for negative-case correctness:

1. judge-prompt.md: explicitly tells the judge that the agent's literal
   VERDICT: PASS / VERDICT: FAIL is just a hint, not binding. A skill that
   correctly returns 'no such business number' or 'invoice not found' for
   a deliberately invalid input is PASS, not fail.

2. judge-skill.py: drop the deterministic gate that flipped pass to fail
   when 'VERDICT: PASS' literal was missing from the transcript. That gate
   was producing false fails for negative-case tests where the agent
   correctly responded with VERDICT: FAIL because the skill rejected an
   invalid input. The judge LLM (gpt-5.5) is now trusted to evaluate the
   transcript against the SKILL.md 'Done when' criteria.

Verified live:

- nts-business-registration with valid number  -> pass/success (0.99)
- nts-business-registration with fake number   -> pass/success (0.99)
- flight-ticket-search ICN->NRT 2026-08-20     -> pass/success (0.99)

* fix(ohou-today-deal): address PR #264 review (live UA, explicit feed selection, argv validators)

- HIGH: switch fetch_html() to well-formed bot UA with contact URL
  (k-skill-ohou-today-deal/1.0 (+https://github.com/NomaDamas/k-skill)).
  ohou.se Akamai bot manager 403s anonymous UAs but allows identified
  bot UAs that include a contact URL. Live default workflow now returns
  74 deals end-to-end instead of failing with HTTP 403.
- MEDIUM: extract_deals() now explicitly selects React Query entries with
  queryKey == ['today-deal-feed'] or ['special-today-deal-feed'] and
  reads only state.data.todayDealFeed.slots[type=='DEAL']. Unrelated
  DEAL-shaped nodes from navigation/banner modules are excluded.
  Legacy fixture/JSON-payload fallback path preserved for tests that
  construct simplified payloads.
- LOW: --limit now requires a positive integer; --min-discount is
  constrained to 0..100. Both validated via argparse.ArgumentTypeError
  so users get a clear CLI error instead of silent slicing or nonsensical
  thresholds.
- Tests: add 9 new unit tests covering explicit feed selection,
  navigation/GOODS exclusion, fallback compatibility, and argv validators.
  Strengthen skill-docs.test.js to lock the special-today-deal-feed
  surface and well-formed UA signature.
- Docs: update SKILL.md and feature doc to document the explicit
  today-deal-feed + special-today-deal-feed extraction boundary and the
  Akamai UA policy.

* Merge pull request #263 from NomaDamas/feature/#257

Feature/#257

* Feature/#256 (#266)

* Enable public local-election candidate lookups

Add an NEC integrated-search skill and helper package so agents can answer 지방선거 후보자 lookup requests without credentials or proxy routes.

Constraint: Issue #256 requested TDD, Ralph completion, branch feature/#256, and PR targeting dev.

Rejected: k-skill-proxy route | NEC integrated candidate search is public and requires no API key.

Confidence: high

Scope-risk: moderate

Directive: Keep the helper read-only and do not automate NEC login, CAPTCHA, filing, or privileged election workflows.

Tested: git diff --check; node --test packages/local-election-candidate-search/test/index.test.js; npm run lint --workspace local-election-candidate-search; npm run test --workspace local-election-candidate-search; npm pack --workspace local-election-candidate-search --dry-run; node packages/local-election-candidate-search/src/cli.js 오세훈 --election 시도지사 --region 서울 --limit 1; PATH=/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/Users/jeffrey/.codex/tmp/arg0/codex-arg0a6JueA:/opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/path:/Users/jeffrey/.cmuxterm/omo-bin:/opt/homebrew/share/android-commandlinetools/platform-tools:/opt/homebrew/share/android-commandlinetools/emulator:/opt/homebrew/share/android-commandlinetools/cmdline-tools/latest/bin:/Users/jeffrey/.local/bin:/Users/jeffrey/.bun/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/opt/openjdk@21/bin:/opt/homebrew/opt/postgresql@18/bin:/Users/jeffrey/.jenv/shims:/Users/jeffrey/.jenv/bin:/opt/homebrew/opt/imagemagick/bin:/opt/homebrew/Cellar/pyenv-virtualenv/1.4.0/shims:/Users/jeffrey/.pyenv/shims:/opt/homebrew/opt/openssl@3/bin:/Users/jeffrey/.rbenv/shims:/Users/jeffrey/.rbenv/bin:/Users/jeffrey/google-cloud-sdk/bin:/Applications/cmux.app/Contents/Resources/bin:/Users/jeffrey/Library/pnpm:/Users/jeffrey/.nvm/versions/node/v24.13.0/bin:/Users/jeffrey/.cops/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/pmk/env/global/bin:/Library/Apple/usr/bin:/Library/TeX/texbin:/Users/jeffrey/.cargo/bin:/Users/jeffrey/Library/Application Support/JetBrains/Toolbox/scripts:/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin:/Users/jeffrey/xcode-projects/marshroom/cli npm run ci

Not-tested: Exhaustive NEC markup variants for every historical election type.

Co-authored-by: OmX <omx@oh-my-codex.dev>

* Enforce fail-closed candidate identity parsing

Constraint: PR #266 review required exact candidate-name matching and CLI help regression coverage.\nRejected: fallback-to-query-name on missing upstream markup | it can mislabel unrelated candidates as exact matches.\nConfidence: high\nScope-risk: narrow\nDirective: Keep NEC parser changes fail-closed when candidate identity cannot be parsed.\nTested: git diff --check; node --test packages/local-election-candidate-search/test/index.test.js; npm run lint --workspace local-election-candidate-search; npm run test --workspace local-election-candidate-search; npm pack --workspace local-election-candidate-search --dry-run; live CLI smoke for 오세훈; CLI --help smoke.\nNot-tested: repo-wide npm run ci remains blocked by pre-existing missing SKILL.md: ohou-today-deal.

* Preserve unique candidate lookup results

Deduplicate parsed NEC candidate/election rows before applying user limits, and make expected CLI validation failures concise by default while keeping an explicit debug stack escape hatch.

Constraint: PR #266 round-2 follow-up requested TDD fixes for duplicate NEC rows and CLI validation UX.\nRejected: Deduplicating after limit | would still allow duplicates to crowd out unique rows.\nRejected: Always printing stack traces | exposes local paths for normal user-input failures.\nConfidence: high\nScope-risk: narrow\nDirective: Keep dedupe keys stable enough to avoid collapsing legitimately distinct historical election rows.\nTested: git diff --check; node --test packages/local-election-candidate-search/test/index.test.js; npm run lint --workspace local-election-candidate-search; npm run test --workspace local-election-candidate-search; npm pack --workspace local-election-candidate-search --dry-run; live 오세훈 smoke; live 김동연 duplicate repro; CLI no-args/help.\nNot-tested: Full npm run ci remains blocked by pre-existing missing SKILL.md: ohou-today-deal.

* Prevent filtered NEC lookup false negatives

Fix the candidate parser so documented education-superintendent and filtered local-election lookups return bounded, evidence-backed results instead of silently dropping valid rows.

Constraint: PR #266 round-3 review required TDD, Ralph verification, and branch update for issue #256.

Rejected: Full NEC pagination in this follow-up | broader than the approved change; bounded 100-row fetch now avoids user-limit false negatives and warns when capped.

Confidence: high

Scope-risk: narrow

Directive: Preserve exact-name fail-closed parsing and count raw parsed upstream rows before cap-warning decisions.

Tested: git diff --check; node --test packages/local-election-candidate-search/test/index.test.js; npm run lint --workspace local-election-candidate-search; npm run test --workspace local-election-candidate-search; npm pack --workspace local-election-candidate-search --dry-run; live CLI smokes for 오세훈, 조희연, 김동연; CLI help/no-args checks; architect verification CLEAR.

Not-tested: Full npm run ci remains blocked by pre-existing repo-wide missing SKILL.md: ohou-today-deal.

---------

Co-authored-by: OmX <omx@oh-my-codex.dev>

* chore(changesets): rename daiso bearer-auth changeset to avoid name collision with consumed main release

PR #245 already consumed .changeset/issue-207-daiso-pickup-eligibility.md
into daiso-product-search v0.3.0 on main. The dev branch later modified that
same changeset file in d7263a5 to describe the newer Bearer-auth fix, which
collides with main's deletion on the next dev→main sync.

Renaming the still-unreleased Bearer-auth note to
issue-207-daiso-bearer-auth.md preserves the release entry for the next
version-packages run and clears the modify/delete conflict on PR #271
without losing the changelog content.

* fix(kstartup-search): implement promised client-side filter to deliver on SKILL.md L121

Live data revealed two unmet contracts in the kstartup-search helper:

1. SKILL.md L121 promised the helper re-applies supt_regin / aply_trgt /
   biz_enyy filters on the client side because K-Startup upstream ignores
   them server-side. The helper had no such logic — calling
   `--supt-regin 서울특별시 --rcrt-prgs-yn Y` returned 경북/충북/충남
   announcements as-is, silently misleading callers.

2. The upstream `supt_regin` field is stored as the short form
   (`서울`, `경기`, `충북`, ...) but every CLI example in the skill used
   the standard 광역지자체 long form (`서울특별시`), which would never
   substring-match even after a client filter was added.

Add `apply_client_filters()` that runs after `urlopen` returns. It honors
the SKILL.md contract literally: substring match per token, AND-joined
across comma-separated user values, with a 17-region (+`전국`) shortname
normalisation table so both `--supt-regin 서울특별시` and
`--supt-regin 서울` resolve to upstream's `서울`. Filtered responses
expose a new `client_filter: {fields, upstream_returned, after_filter}`
metadata block so callers can detect "first page was depleted by filter"
and page through.

Tests: 9 new ClientFilterTests + 2 normalisation tests on top of the
existing 14 (25 total, all passing).

Live smoke (against a dev proxy with DATA_GO_KR_API_KEY activated for
dataset 15125364): `--supt-regin 서울특별시 --rcrt-prgs-yn Y --per-page 10`
now returns 4 actual 서울 announcements (upstream returned 10 mixed-region
rows; client filter narrowed to 4), with detl_pg_url to k-startup.go.kr.

Confidence: high. Scope-risk: narrow — purely additive on the response
path; other endpoints (business-info / contents / statistics) pass
through unchanged.

* ci(k-skill-proxy): replace local pm2+cloudflared with Cloud Run auto-deploy via GitHub Actions

main에 머지되면 GitHub Actions가 자동으로 Workload Identity Federation으로 GCP 인증 후
Artifact Registry에 컨테이너 이미지를 빌드/푸시하고 Cloud Run(asia-northeast1) 서비스
k-skill-proxy를 재배포한다. 시크릿은 GCP Secret Manager에서 런타임에 주입된다.

- add .github/workflows/deploy-k-skill-proxy.yml (WIF, on push to main)
- add packages/k-skill-proxy/Dockerfile (multi-stage node:20-alpine, port bridge)
- add docs/deploy-k-skill-proxy.md (1회성 GCP 셋업 + 운영 점검 절차)
- remove ecosystem.config.cjs (PM2 root config)
- remove scripts/run-k-skill-proxy.sh (local secrets.env source + node launcher)
- remove wrangler devDependency (unused Cloudflare Workers CLI)
- update AGENTS.md, CLAUDE.md, CONTRIBUTING.md, docs/features/k-skill-proxy.md,
  packages/k-skill-proxy/README.md to describe the new Cloud Run + GHA flow
- clean dead k-skill-proxy-cloudrun entries from .gitignore

* docs(AGENTS): proxy 운영 전반(회전·롤백·비상 수동 배포 포함) docs/deploy-k-skill-proxy.md 참고 명시

* test(skill-docs): update stale CONTRIBUTING.md assertion for Cloud Run migration

80e7805(ci(k-skill-proxy): replace local pm2+cloudflared with Cloud Run auto-deploy)
가 CONTRIBUTING.md의 '프록시 서버 개발과 배포' 섹션을 Cloud Run + GCP Secret
Manager 흐름으로 다시 썼는데, 같은 섹션을 검증하는 skill-docs.test.js의 어서션은
구버전(`~/.local/share/k-skill-proxy`) 그대로였다. PR #276 CI에서 이 stale
어서션이 fail하여 머지를 막고 있었다.

기존 한 줄 regex(localhost 시크릿 경로)를 새 사실에 맞춰 두 개의 어서션으로 교체:

1. 프로덕션이 Google Cloud Run(asia-northeast1) + k-skill-proxy.nomadamas.org에서
   운영된다는 문구를 강제한다.
2. 시크릿이 GCP Secret Manager에 있고 운영 점검 절차가
   docs/deploy-k-skill-proxy.md에 있다는 문구를 강제한다.

이렇게 하면 문서가 다시 옛 로컬 흐름으로 돌아가거나 운영 가이드 링크가 빠지는
회귀가 발생할 때 CI가 잡아준다.

---------

Co-authored-by: arnold714 <arnold714@naver.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: chanmin <cmju@cowave.kr>
Co-authored-by: OmX <omx@oh-my-codex.dev>
Co-authored-by: hmmhmmhm/ <hmmhmmhm@naver.com>
Co-authored-by: 배기민 <53887180+BAEM1N@users.noreply.github.com>
Co-authored-by: lee-ji-hong <zhffktkdlekghksxk@naver.com>
This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-05-22 11:40:40 +09:00 committed by GitHub
commit 01cd887579
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 498 additions and 72 deletions

View file

@ -0,0 +1,5 @@
---
"daiso-product-search": minor
---
Restore Daiso store pickup stock quantities through the official non-login Bearer flow (`/api/auth/request` + AES-128-CBC token) while keeping the resilient `selPkupStr` fallback API. `getStorePickupStock()` now retries once with a fresh token on 401/403 and returns structured `retrievalStatus: "blocked"` after repeated auth blocks instead of throwing. `getStorePickupEligibility()` remains public, and `lookupStoreProductAvailability()` fills `pickupEligibility` when exact pickup stock remains unavailable.

View file

@ -0,0 +1,5 @@
---
"emergency-room-beds": minor
---
Add an E-Gen based nearby emergency-room status skill and package.

View file

@ -0,0 +1,5 @@
---
"k-skill-proxy": minor
---
Add `/v1/kstartup/{business-info,announcements,contents,statistics}` routes that wrap the data.go.kr `15125364` (창업진흥원_K-Startup) Open API. The routes inject `DATA_GO_KR_API_KEY` server-side, return 503 when the key (or the per-dataset 활용신청) is missing, and cache successful JSON responses while bypassing the cache for upstream error envelopes (`resultCode != "00"`).

View file

@ -0,0 +1,5 @@
---
"local-election-candidate-search": minor
---
Add a public NEC local election candidate lookup skill and helper CLI.

View file

@ -0,0 +1,5 @@
---
"sh-notice-search": minor
---
Add a policy-compliant SH public notice search skill and direct HTML lookup client.

38
.dockerignore Normal file
View file

@ -0,0 +1,38 @@
.git
.github
.gitignore
.DS_Store
.omx
.sisyphus
.venv
.env
.env.*
*.dec
*.plaintext
__pycache__
**/__pycache__
**/node_modules
**/dist
**/.next
**/.cache
docs
tests
test
**/test
**/tests
**/*.test.js
**/*.test.py
**/*.spec.js
scripts/build-manus-bundle.js
*.md
LICENSE
CONTRIBUTING.md
CHANGELOG.md
**/CHANGELOG.md
**/README.md
**/*.test.js
.changeset
.claude
.agents
.cursor
.kiro

View file

@ -0,0 +1,147 @@
name: Deploy k-skill-proxy to Cloud Run
# Live: https://k-skill-proxy.nomadamas.org
# GCP project: k-skill-proxy, region: asia-northeast1
# Auth: Workload Identity Federation. Setup: docs/deploy-k-skill-proxy.md
on:
push:
branches: [main]
workflow_dispatch: {}
permissions:
contents: read
id-token: write
concurrency:
group: deploy-k-skill-proxy
cancel-in-progress: false
env:
GCP_PROJECT_ID: k-skill-proxy
GCP_REGION: asia-northeast1
AR_REPO: k-skill
SERVICE_NAME: k-skill-proxy
IMAGE_NAME: k-skill-proxy
jobs:
deploy:
name: Build and deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Authenticate to Google Cloud (Workload Identity Federation)
id: auth
uses: google-github-actions/auth@v3
with:
project_id: ${{ env.GCP_PROJECT_ID }}
workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}
service_account: ${{ secrets.GCP_DEPLOY_SERVICE_ACCOUNT }}
token_format: access_token
- name: Set up gcloud CLI
uses: google-github-actions/setup-gcloud@v2
with:
project_id: ${{ env.GCP_PROJECT_ID }}
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker ${{ env.GCP_REGION }}-docker.pkg.dev --quiet
- name: Resolve image URI
id: image
run: |
IMAGE_URI="${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/${AR_REPO}/${IMAGE_NAME}:${GITHUB_SHA}"
echo "uri=${IMAGE_URI}" >> "$GITHUB_OUTPUT"
echo "Image: ${IMAGE_URI}"
- name: Build container image
run: |
docker build \
--tag "${{ steps.image.outputs.uri }}" \
--file packages/k-skill-proxy/Dockerfile \
.
- name: Push image to Artifact Registry
run: docker push "${{ steps.image.outputs.uri }}"
- name: Deploy to Cloud Run
id: deploy
uses: google-github-actions/deploy-cloudrun@v3
with:
service: ${{ env.SERVICE_NAME }}
region: ${{ env.GCP_REGION }}
image: ${{ steps.image.outputs.uri }}
secrets: |-
AIR_KOREA_OPEN_API_KEY=AIR_KOREA_OPEN_API_KEY:latest
KMA_OPEN_API_KEY=KMA_OPEN_API_KEY:latest
SEOUL_OPEN_API_KEY=SEOUL_OPEN_API_KEY:latest
HRFCO_OPEN_API_KEY=HRFCO_OPEN_API_KEY:latest
OPINET_API_KEY=OPINET_API_KEY:latest
BLUE_RIBBON_SESSION_ID=BLUE_RIBBON_SESSION_ID:latest
DATA_GO_KR_API_KEY=DATA_GO_KR_API_KEY:latest
KEDU_INFO_KEY=KEDU_INFO_KEY:latest
DATA4LIBRARY_AUTH_KEY=DATA4LIBRARY_AUTH_KEY:latest
FOODSAFETYKOREA_API_KEY=FOODSAFETYKOREA_API_KEY:latest
KAKAO_REST_API_KEY=KAKAO_REST_API_KEY:latest
KRX_API_KEY=KRX_API_KEY:latest
KOSIS_API_KEY=KOSIS_API_KEY:latest
NAVER_SEARCH_CLIENT_ID=NAVER_SEARCH_CLIENT_ID:latest
NAVER_SEARCH_CLIENT_SECRET=NAVER_SEARCH_CLIENT_SECRET:latest
env_vars: |-
KSKILL_PROXY_HOST=0.0.0.0
KSKILL_PROXY_NAME=k-skill-proxy
KSKILL_PROXY_CACHE_TTL_MS=300000
KSKILL_PROXY_RATE_LIMIT_WINDOW_MS=60000
KSKILL_PROXY_RATE_LIMIT_MAX=60
flags: >-
--platform=managed
--allow-unauthenticated
--cpu=1
--memory=512Mi
--min-instances=0
--max-instances=3
--concurrency=80
--timeout=60
--execution-environment=gen2
--cpu-boost
- name: Smoke test /health on the new revision
env:
SERVICE_URL: ${{ steps.deploy.outputs.url }}
run: |
set -euo pipefail
echo "Service URL: ${SERVICE_URL}"
for attempt in 1 2 3 4 5; do
if curl -fsS --max-time 15 "${SERVICE_URL}/health" >/tmp/health.json; then
break
fi
echo "Health probe attempt ${attempt} failed, retrying in 5s..."
sleep 5
done
python3 -c "
import json, sys
data = json.load(open('/tmp/health.json'))
if not data.get('ok'):
print('Health response is not ok:', data)
sys.exit(1)
missing = [k for k, v in data.get('upstreams', {}).items() if k.endswith('Configured') and v is not True]
if missing:
print('Upstreams not configured:', missing)
sys.exit(1)
print('Health OK. All upstreams configured.')
"
- name: Smoke test custom domain (k-skill-proxy.nomadamas.org)
run: |
set -euo pipefail
if curl -fsS --max-time 15 https://k-skill-proxy.nomadamas.org/health >/tmp/prod-health.json; then
python3 -c "
import json
data = json.load(open('/tmp/prod-health.json'))
print('Prod /health ok:', data.get('ok'))
"
else
echo "::warning::Custom domain /health probe failed; revision may need traffic split or DNS warm-up."
fi

2
.gitignore vendored
View file

@ -9,3 +9,5 @@ node_modules/
__pycache__/
dist/
.sisyphus/
.agents/

View file

@ -47,10 +47,12 @@ These rules are repo-specific and apply to everything under this directory.
## Proxy server development
- 개발 repo (`dev` 브랜치)에서 proxy 코드를 수정하고, main에 merge하면 프로덕션에 반영된다.
- 프로덕션 배포본은 `~/.local/share/k-skill-proxy`에 main 브랜치 단독 clone으로 존재한다.
- cron job (`0 * * * *`)이 매시 정각에 `~/.local/share/k-skill-proxy/scripts/auto-update-proxy.sh`를 실행해 origin/main fetch → fast-forward pull → package-lock 변경 시 npm ci → pm2 restart 순서로 자동 배포한다.
- 로그: `/tmp/k-skill-proxy-update.log`
- 프로덕션 배포 대상은 **Google Cloud Run** (`asia-northeast1`, GCP project `k-skill-proxy`)이며, 커스텀 도메인 `k-skill-proxy.nomadamas.org`로 노출된다.
- `main` 브랜치에 merge되면 `.github/workflows/deploy-k-skill-proxy.yml`이 Workload Identity Federation으로 GCP 인증 → Artifact Registry로 image build/push → Cloud Run 재배포 → `/health` smoke test까지 자동으로 수행한다.
- 따라서 **dev에서 route를 추가/수정한 뒤 main에 merge되기 전까지는 프로덕션 proxy에 반영되지 않는다.**
- proxy 서버 코드: `packages/k-skill-proxy/src/server.js`
- 컨테이너 이미지 빌드 정의: `packages/k-skill-proxy/Dockerfile`
- proxy 서버 테스트: `packages/k-skill-proxy/test/server.test.js`
- proxy 환경변수(API key 등)는 `~/.config/k-skill/secrets.env`에 넣고, `scripts/run-k-skill-proxy.sh`가 source한다.
- **dev에서 route를 추가/수정한 뒤 main에 merge되기 전까지는 프로덕션 proxy에 반영되지 않는다.** 로컬 테스트는 `node packages/k-skill-proxy/src/server.js`로 직접 실행한다.
- 로컬 테스트: `node packages/k-skill-proxy/src/server.js` (환경변수는 `~/.config/k-skill/secrets.env` 등에서 직접 export해서 띄운다)
- 프로덕션 시크릿은 GCP Secret Manager에 보관되고 Cloud Run runtime에 주입된다.
- **운영 관련 모든 절차는 [`docs/deploy-k-skill-proxy.md`](docs/deploy-k-skill-proxy.md)에 정리되어 있다.** 새 maintainer 인계를 위한 1회성 GCP/WIF 셋업, GitHub repository secrets 등록, upstream API 키 회전(rotation), 자동 배포 상태/로그/이미지 태그 확인, Cloud Run 트래픽 롤백, GitHub Actions 장애 시 로컬에서 동일한 배포를 수동으로 돌리는 비상 명령까지 전부 거기서 본다. proxy 운영 관련 어떤 질문이 들어와도 먼저 그 문서를 확인한다.

View file

@ -14,9 +14,9 @@
## Proxy server development
- 개발 repo: `/Users/jeffrey/Projects/k-skill` (이 디렉토리, `dev` 브랜치)
- 프로덕션 배포본: `~/.local/share/k-skill-proxy` (main 브랜치 단독 clone)
- **cron job** 이 매시 정각에 `origin/main` fetch → fast-forward pull → pm2 restart 실행
- 개발 repo: 이 디렉토리, `dev` 브랜치
- 프로덕션 배포 대상: **Google Cloud Run** (project `k-skill-proxy`, region `asia-northeast1`, custom domain `k-skill-proxy.nomadamas.org`)
- `main` 브랜치에 merge되면 `.github/workflows/deploy-k-skill-proxy.yml`이 자동으로 Cloud Run 재배포를 수행한다. 인증은 Workload Identity Federation, 이미지 빌드 정의는 `packages/k-skill-proxy/Dockerfile`, 시크릿은 GCP Secret Manager에서 주입된다. WIF/Secret Manager 셋업은 `docs/deploy-k-skill-proxy.md` 참고.
- 따라서 proxy route 변경은 **main에 merge되어야 프로덕션에 반영**된다. dev에서 코드를 바꿔도 프로덕션 proxy에는 영향 없음.
- 로컬 테스트는 `node packages/k-skill-proxy/src/server.js` 로 직접 실행하거나 `node --test packages/k-skill-proxy/test/server.test.js` 로 확인.
- **Proxy 편입 규칙**: k-skill-proxy에 route를 추가하려면 upstream이 API 키를 필요로 해야 한다. 공개 엔드포인트(키 불필요)는 skill 코드에서 직접 호출하고 프록시를 거치지 않는다.

View file

@ -60,11 +60,11 @@
- 프록시 서버 코드: `packages/k-skill-proxy/src/server.js`
- 프록시 서버 테스트: `packages/k-skill-proxy/test/server.test.js`
- 로컬 테스트: `node packages/k-skill-proxy/src/server.js`
- 프록시 환경변수와 API key는 `~/.config/k-skill/secrets.env`에 두고, `scripts/run-k-skill-proxy.sh`가 source합니다.
- 프로덕션 프록시는 `~/.local/share/k-skill-proxy`에 있는 `main` 브랜치 단독 clone입니다.
- cron job은 매시 정각 `scripts/auto-update-proxy.sh`를 실행해 `origin/main` fetch → fast-forward pull → `package-lock` 변경 시 `npm ci``pm2 restart` 순서로 배포합니다.
- 배포 로그는 `/tmp/k-skill-proxy-update.log`에서 확인합니다.
- 컨테이너 이미지 정의: `packages/k-skill-proxy/Dockerfile`
- 로컬 테스트: 필요한 upstream 환경변수를 export한 상태에서 `node packages/k-skill-proxy/src/server.js`. 로컬에서 시크릿을 모아두는 표준 위치는 `~/.config/k-skill/secrets.env`니다.
- 프로덕션 프록시는 **Google Cloud Run** (project `k-skill-proxy`, region `asia-northeast1`)에서 운영하며 `k-skill-proxy.nomadamas.org` 도메인에 매핑되어 있습니다.
- `main` 브랜치에 머지되면 `.github/workflows/deploy-k-skill-proxy.yml` 워크플로가 Workload Identity Federation으로 GCP 인증 → Artifact Registry로 이미지 빌드/푸시 → Cloud Run 재배포 → `/health` smoke test까지 자동 수행합니다.
- 프로덕션 시크릿은 GCP Secret Manager에 보관되고 Cloud Run 런타임에 주입됩니다. 프록시 운영자(maintainer)가 한 번 수행해야 하는 WIF/Secret Manager 셋업과 운영 점검 절차는 [`docs/deploy-k-skill-proxy.md`](docs/deploy-k-skill-proxy.md)에 정리되어 있습니다.
- `dev`에서 route를 추가하거나 수정해도 `main`에 머지되기 전까지는 프로덕션 프록시에 반영되지 않습니다.
## 검증

View file

@ -0,0 +1,211 @@
# k-skill-proxy 배포 가이드 (Cloud Run + GitHub Actions)
`k-skill-proxy`는 Google Cloud Run에서 운영되고, `main` 브랜치에 머지되면 GitHub Actions가 자동으로 재배포합니다.
이 문서는 그 자동 배포 파이프라인의 **1회성 셋업 절차**와 **운영 점검 절차**를 정리합니다. 일반 contributor는 읽지 않아도 되며, 프록시 운영을 담당하는 maintainer(현재 `jeffrey@markr.ai`)가 인프라를 처음 만들거나 수리할 때 참고합니다.
## 운영 사실
| 항목 | 값 |
| --- | --- |
| GCP project ID | `k-skill-proxy` |
| Region | `asia-northeast1` (도쿄) |
| Cloud Run service | `k-skill-proxy` |
| Artifact Registry repo | `asia-northeast1-docker.pkg.dev/k-skill-proxy/k-skill` |
| 공개 도메인 | `https://k-skill-proxy.nomadamas.org` (Cloud Run domain mapping) |
| 컨테이너 이미지 정의 | `packages/k-skill-proxy/Dockerfile` |
| 워크플로 | `.github/workflows/deploy-k-skill-proxy.yml` |
| 인증 | Workload Identity Federation (long-lived JSON key 없음) |
| 시크릿 저장소 | GCP Secret Manager (이름 = 환경변수 이름) |
## 배포 흐름
1. `dev` 브랜치에서 작업, PR을 `dev`에 보낸다.
2. `dev``main` 머지 PR이 `@vkehfdl1`에 의해 머지된다.
3. `main` push가 `.github/workflows/deploy-k-skill-proxy.yml`을 트리거한다.
4. 워크플로가:
- WIF로 `${GCP_DEPLOY_SERVICE_ACCOUNT}`로 impersonate
- `packages/k-skill-proxy/Dockerfile`로 컨테이너 빌드
- Artifact Registry에 `:${GITHUB_SHA}` 태그로 push
- Cloud Run `k-skill-proxy` 서비스를 새 이미지로 재배포 (Secret Manager 시크릿 + 런타임 env 주입)
- 새 revision의 `*.run.app` URL과 `https://k-skill-proxy.nomadamas.org/health`에 smoke test
5. 실패 시 GitHub Actions 페이지에서 로그 확인. Cloud Run 자체는 마지막 healthy revision에 트래픽을 유지한다.
## 1회성 GCP 셋업
> 이미 한 번 셋업되어 있다면 다시 실행할 필요 없음. 새 maintainer가 인계받거나 SA를 새로 만들 때만 사용.
```bash
export PROJECT_ID="k-skill-proxy"
export PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')"
export GH_REPO="NomaDamas/k-skill" # owner/repo
export POOL_ID="github-actions-pool"
export PROVIDER_ID="github-actions-provider"
export DEPLOY_SA="k-skill-proxy-deploy"
export DEPLOY_SA_EMAIL="${DEPLOY_SA}@${PROJECT_ID}.iam.gserviceaccount.com"
```
### 1) 필요한 API 활성화
```bash
gcloud services enable \
iamcredentials.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
secretmanager.googleapis.com \
--project="$PROJECT_ID"
```
### 2) Workload Identity Pool + GitHub OIDC provider
```bash
gcloud iam workload-identity-pools create "$POOL_ID" \
--project="$PROJECT_ID" \
--location=global \
--display-name="GitHub Actions"
gcloud iam workload-identity-pools providers create-oidc "$PROVIDER_ID" \
--project="$PROJECT_ID" \
--location=global \
--workload-identity-pool="$POOL_ID" \
--display-name="GitHub OIDC" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
--attribute-condition="assertion.repository == '${GH_REPO}'"
```
> `attribute-condition`은 토큰 발급 단계에서 우리 저장소만 허용해 풀 자체를 좁힙니다. 임의의 다른 repo가 같은 풀을 통해 SA를 impersonate하지 못하게 막는 핵심 가드입니다.
### 3) Deploy service account 생성
```bash
gcloud iam service-accounts create "$DEPLOY_SA" \
--project="$PROJECT_ID" \
--display-name="GitHub Actions k-skill-proxy deployer"
```
### 4) 풀 → service account impersonation 허용
```bash
gcloud iam service-accounts add-iam-policy-binding "$DEPLOY_SA_EMAIL" \
--project="$PROJECT_ID" \
--role=roles/iam.workloadIdentityUser \
--member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/attribute.repository/${GH_REPO}"
```
### 5) deploy SA에 필요한 권한 부여
```bash
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${DEPLOY_SA_EMAIL}" \
--role=roles/run.admin
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${DEPLOY_SA_EMAIL}" \
--role=roles/artifactregistry.writer
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${DEPLOY_SA_EMAIL}" \
--role=roles/iam.serviceAccountUser
```
`iam.serviceAccountUser`는 Cloud Run의 런타임 service account(`${PROJECT_NUMBER}-compute@developer.gserviceaccount.com`)를 deploy SA가 대신 지정할 수 있게 하기 위함입니다.
### 6) Cloud Run 런타임 SA에 Secret Manager accessor 부여
```bash
RUNTIME_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
for s in \
AIR_KOREA_OPEN_API_KEY KMA_OPEN_API_KEY SEOUL_OPEN_API_KEY HRFCO_OPEN_API_KEY \
OPINET_API_KEY BLUE_RIBBON_SESSION_ID DATA_GO_KR_API_KEY KEDU_INFO_KEY \
DATA4LIBRARY_AUTH_KEY FOODSAFETYKOREA_API_KEY KAKAO_REST_API_KEY KRX_API_KEY \
KOSIS_API_KEY NAVER_SEARCH_CLIENT_ID NAVER_SEARCH_CLIENT_SECRET; do
gcloud secrets add-iam-policy-binding "$s" \
--project="$PROJECT_ID" \
--member="serviceAccount:${RUNTIME_SA}" \
--role=roles/secretmanager.secretAccessor \
--condition=None >/dev/null
done
```
### 7) WIF provider 리소스 이름 확인
```bash
gcloud iam workload-identity-pools providers describe "$PROVIDER_ID" \
--project="$PROJECT_ID" \
--location=global \
--workload-identity-pool="$POOL_ID" \
--format='value(name)'
# 예: projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
```
이 값과 `${DEPLOY_SA_EMAIL}`을 GitHub에 등록합니다.
## GitHub repository secrets
다음 두 개의 **secret**을 `Settings → Secrets and variables → Actions → Repository secrets`에 등록합니다.
| Name | Value |
| --- | --- |
| `GCP_WIF_PROVIDER` | 위 7번에서 얻은 provider 리소스 전체 이름 |
| `GCP_DEPLOY_SERVICE_ACCOUNT` | `k-skill-proxy-deploy@k-skill-proxy.iam.gserviceaccount.com` |
> 값 자체가 민감하진 않지만, 외부에 노출되면 reconnaissance에 도움이 될 수 있으므로 secret으로 둡니다. variable로 옮겨도 동작은 동일합니다.
## Secret Manager에 upstream key 업로드
```bash
KEYS=(
AIR_KOREA_OPEN_API_KEY KMA_OPEN_API_KEY SEOUL_OPEN_API_KEY HRFCO_OPEN_API_KEY
OPINET_API_KEY BLUE_RIBBON_SESSION_ID DATA_GO_KR_API_KEY KEDU_INFO_KEY
DATA4LIBRARY_AUTH_KEY FOODSAFETYKOREA_API_KEY KAKAO_REST_API_KEY KRX_API_KEY
KOSIS_API_KEY NAVER_SEARCH_CLIENT_ID NAVER_SEARCH_CLIENT_SECRET
)
set -a; source ~/.config/k-skill/secrets.env; set +a
for k in "${KEYS[@]}"; do
value="${!k:-}"
[[ -z "$value" ]] && { echo "skip $k (empty)"; continue; }
if gcloud secrets describe "$k" --project="$PROJECT_ID" >/dev/null 2>&1; then
printf '%s' "$value" | gcloud secrets versions add "$k" --data-file=- --project="$PROJECT_ID"
else
printf '%s' "$value" | gcloud secrets create "$k" --data-file=- --replication-policy=automatic --project="$PROJECT_ID"
fi
done
```
키 값을 회전(rotate)할 때도 같은 명령을 다시 실행하면 새 version이 추가됩니다. Cloud Run은 `:latest`로 바인딩되어 있어 다음 배포부터 자동 반영됩니다(즉시 적용이 필요하면 새 revision을 한 번 더 deploy).
## 운영 점검 절차
- 자동 배포 상태: GitHub `Actions` 탭의 "Deploy k-skill-proxy to Cloud Run" 워크플로
- 라이브 헬스체크: `curl -fsS https://k-skill-proxy.nomadamas.org/health`
- Cloud Run revision/로그: GCP Console → Cloud Run → `k-skill-proxy` (`asia-northeast1`)
- 이미지 태그: `asia-northeast1-docker.pkg.dev/k-skill-proxy/k-skill/k-skill-proxy:<commit-sha>`
- 트래픽 롤백: 이전 revision으로 traffic split을 100% 되돌리거나, 직전 commit을 revert해서 main에 머지 → 워크플로가 다시 돈다.
## 로컬에서 동일한 배포를 수동으로 돌리고 싶을 때
`gcloud auth login`으로 maintainer 계정에 로그인된 상태에서:
```bash
SHA="$(git rev-parse HEAD)"
IMAGE_URI="asia-northeast1-docker.pkg.dev/k-skill-proxy/k-skill/k-skill-proxy:${SHA}"
gcloud auth configure-docker asia-northeast1-docker.pkg.dev --quiet
docker build -t "$IMAGE_URI" -f packages/k-skill-proxy/Dockerfile .
docker push "$IMAGE_URI"
gcloud run deploy k-skill-proxy \
--image="$IMAGE_URI" \
--region=asia-northeast1 \
--platform=managed \
--allow-unauthenticated \
--execution-environment=gen2 \
--cpu=1 --memory=512Mi --min-instances=0 --max-instances=3 \
--concurrency=80 --timeout=60 --cpu-boost \
--project=k-skill-proxy
```
이 명령은 평상시에는 필요 없습니다. GitHub Actions가 같은 일을 하기 때문입니다.

View file

@ -66,38 +66,32 @@ client/skill -> k-skill-proxy -> upstream public API
## 프로덕션 배포 구조
프로덕션 proxy 서버는 개발 repo와 분리된 별도 clone으로 운영한다.
프로덕션 proxy 서버는 **Google Cloud Run**에서 운영한다.
- 배포 디렉토리: `~/.local/share/k-skill-proxy` (main 브랜치 단독 clone)
- PM2 프로세스: `k-skill-proxy`
- Cloudflare Tunnel ingress: `k-skill-proxy.nomadamas.org -> http://localhost:4020`
- GCP project: `k-skill-proxy`
- Region: `asia-northeast1` (도쿄)
- Cloud Run service: `k-skill-proxy`
- 공개 도메인: `k-skill-proxy.nomadamas.org` (Cloud Run domain mapping)
- 컨테이너 이미지 정의: `packages/k-skill-proxy/Dockerfile`
- 시크릿(upstream API key): GCP Secret Manager에 보관, Cloud Run runtime에 주입
### 자동 배포 (cron)
### 자동 배포 (GitHub Actions)
`~/.local/share/k-skill-proxy/scripts/auto-update-proxy.sh`가 매시 정각에 실행된다.
`main` 브랜치에 push/merge되면 `.github/workflows/deploy-k-skill-proxy.yml` 워크플로가 실행되어 다음 순서로 동작한다.
```
0 * * * * PATH=/usr/bin:/opt/homebrew/bin:/opt/homebrew/lib/node_modules/.bin:$PATH ~/.local/share/k-skill-proxy/scripts/auto-update-proxy.sh >> /tmp/k-skill-proxy-update.log 2>&1
```
동작 순서:
1. `git fetch origin main`
2. local SHA == remote SHA 이면 종료 (up-to-date)
3. `git pull --ff-only`
4. `package-lock.json` 변경 시 `npm ci`
5. `pm2 restart k-skill-proxy --update-env`
1. Workload Identity Federation으로 GCP 인증
2. `packages/k-skill-proxy/Dockerfile`로 이미지 빌드
3. Artifact Registry (`asia-northeast1-docker.pkg.dev/k-skill-proxy/k-skill/k-skill-proxy:<sha>`)에 push
4. Cloud Run service `k-skill-proxy` 재배포 (Secret Manager 시크릿 + 런타임 환경변수 주입)
5. 직접 Cloud Run URL과 `https://k-skill-proxy.nomadamas.org/health` smoke test
따라서 **main에 merge되어야 프로덕션에 반영**된다. dev 브랜치 변경은 프로덕션에 영향 없음.
로그: `/tmp/k-skill-proxy-update.log`
배포 상태와 로그는 GitHub Actions의 "Deploy k-skill-proxy to Cloud Run" 워크플로 실행 페이지와 GCP Console의 Cloud Run revision/log에서 확인한다.
### 초기 설정 (PM2 + cloudflared)
### 초기 셋업 (운영자 1회 수행)
1. `pm2 start ecosystem.config.cjs`
2. `pm2 save`
3. `pm2 startup` 출력대로 launchd 등록
4. Cloudflare Tunnel ingress 에 `k-skill-proxy.nomadamas.org -> http://localhost:4020` 추가
WIF pool/provider, deploy service account, Secret Manager 시크릿 생성 등 1회성 GCP 셋업 절차와 GitHub repository secrets/variables 등록 방법은 [`docs/deploy-k-skill-proxy.md`](../deploy-k-skill-proxy.md)에 정리되어 있다.
## 기본 공개 정책

View file

@ -1,16 +0,0 @@
module.exports = {
apps: [
{
name: "k-skill-proxy",
cwd: __dirname,
script: "./scripts/run-k-skill-proxy.sh",
interpreter: "/bin/bash",
exec_mode: "fork",
autorestart: true,
watch: false,
env: {
NODE_ENV: "production"
}
}
]
};

View file

@ -10,7 +10,7 @@
"scripts": {
"build": "npm run build --workspaces --if-present",
"build:manus-bundle": "node scripts/build-manus-bundle.js",
"lint": "node --check scripts/skill-docs.test.js scripts/korean_character_count.js scripts/test_korean_character_count.js scripts/build-manus-bundle.js scripts/test_build_manus_bundle.js && python3 -m py_compile scripts/k_skill_cleaner.py scripts/test_k_skill_cleaner.py corporate-registration-consulting/scripts/fill_official_hwp.py k-skill-cleaner/scripts/k_skill_cleaner.py scripts/fine_dust.py scripts/test_fine_dust.py scripts/ktx_booking.py scripts/test_ktx_booking.py scripts/sillok_search.py scripts/test_sillok_search.py scripts/korean_spell_check.py scripts/test_korean_spell_check.py scripts/patent_search.py scripts/test_patent_search.py scripts/mfds_drug_safety.py scripts/test_mfds_drug_safety.py scripts/nts_business_registration.py scripts/test_nts_business_registration.py scripts/mfds_food_safety.py scripts/test_mfds_food_safety.py scripts/zipcode_search.py scripts/test_zipcode_search.py scripts/subway_lost_property.py scripts/test_subway_lost_property.py scripts/geeknews_search.py scripts/test_geeknews_search.py nts-business-registration/scripts/nts_business_registration.py scripts/test_naver_blog_search.py scripts/test_korean_slang_writing.py scripts/kakaotalk_mac.py scripts/test_kakaotalk_mac.py scripts/test_coupang_partners_mcp_wrapper.py scripts/test_ohou_today_deal.py scripts/ticket_availability.py scripts/test_ticket_availability.py scripts/test_danawa_price_search.py ticket-availability/scripts/ticket_availability.py coupang-product-search/scripts/coupang_partners_mcp.py ohou-today-deal/scripts/ohou_today_deal.py kakaotalk-mac/scripts/kakaotalk_mac.py naver-blog-research/scripts/_naver_http.py naver-blog-research/scripts/naver_search.py naver-blog-research/scripts/naver_read.py naver-blog-research/scripts/naver_download_images.py korean-slang-writing/scripts/_slang_http.py korean-slang-writing/scripts/slang_search.py korean-slang-writing/scripts/slang_lookup.py korean-scholarship-search/scripts/scholarship_filter.py korean-scholarship-search/scripts/test_scholarship_filter.py korean-scholarship-search/scripts/university_search_plan.py danawa-price-search/scripts/danawa_search.py kosis-stats/scripts/run_kosis_stats.py kosis-stats/tests/test_run_kosis_stats.py kstartup-search/scripts/run_kstartup.py kstartup-search/tests/test_run_kstartup.py intercity-bus-booking/scripts/intercity_bus_search.py daangn-used-goods-search/scripts/daangn_used_goods.py daangn-realty-search/scripts/daangn_realty.py daangn-jobs-search/scripts/daangn_jobs.py daangn-cars-search/scripts/daangn_cars.py && npm run lint --workspaces --if-present && ./scripts/validate-skills.sh",
"lint": "node --check scripts/skill-docs.test.js scripts/korean_character_count.js scripts/test_korean_character_count.js scripts/build-manus-bundle.js scripts/test_build_manus_bundle.js && python3 -m py_compile scripts/k_skill_cleaner.py scripts/test_k_skill_cleaner.py corporate-registration-consulting/scripts/fill_official_hwp.py k-skill-cleaner/scripts/k_skill_cleaner.py scripts/fine_dust.py scripts/test_fine_dust.py scripts/ktx_booking.py scripts/test_ktx_booking.py scripts/sillok_search.py scripts/test_sillok_search.py scripts/korean_spell_check.py scripts/test_korean_spell_check.py scripts/patent_search.py scripts/test_patent_search.py scripts/mfds_drug_safety.py scripts/test_mfds_drug_safety.py scripts/nts_business_registration.py scripts/test_nts_business_registration.py scripts/mfds_food_safety.py scripts/test_mfds_food_safety.py scripts/zipcode_search.py scripts/test_zipcode_search.py scripts/subway_lost_property.py scripts/test_subway_lost_property.py scripts/geeknews_search.py scripts/test_geeknews_search.py nts-business-registration/scripts/nts_business_registration.py scripts/test_naver_blog_search.py scripts/test_korean_slang_writing.py scripts/kakaotalk_mac.py scripts/test_kakaotalk_mac.py scripts/test_coupang_partners_mcp_wrapper.py scripts/test_ohou_today_deal.py scripts/ticket_availability.py scripts/test_ticket_availability.py scripts/test_danawa_price_search.py ticket-availability/scripts/ticket_availability.py coupang-product-search/scripts/coupang_partners_mcp.py ohou-today-deal/scripts/ohou_today_deal.py kakaotalk-mac/scripts/kakaotalk_mac.py naver-blog-research/scripts/_naver_http.py naver-blog-research/scripts/naver_search.py naver-blog-research/scripts/naver_read.py naver-blog-research/scripts/naver_download_images.py korean-slang-writing/scripts/_slang_http.py korean-slang-writing/scripts/slang_search.py korean-slang-writing/scripts/slang_lookup.py korean-scholarship-search/scripts/scholarship_filter.py korean-scholarship-search/scripts/test_scholarship_filter.py korean-scholarship-search/scripts/university_search_plan.py danawa-price-search/scripts/danawa_search.py kosis-stats/scripts/run_kosis_stats.py kosis-stats/tests/test_run_kosis_stats.py kstartup-search/scripts/run_kstartup.py kstartup-search/tests/test_run_kstartup.py intercity-bus-booking/scripts/intercity_bus_search.py daangn-used-goods-search/scripts/daangn_used_goods.py daangn-realty-search/scripts/daangn_realty.py daangn-jobs-search/scripts/daangn_jobs.py daangn-cars-search/scripts/daangn_cars.py && npm run lint --workspaces --if-present && ./scripts/validate-skills.sh",
"typecheck": "tsc --noEmit",
"test": "python3 -m pip install --user --quiet beautifulsoup4 && node --test scripts/skill-docs.test.js scripts/test_korean_character_count.js scripts/test_build_manus_bundle.js && PYTHONPATH=.:scripts python3 -m unittest scripts.test_k_skill_cleaner scripts.test_fine_dust scripts.test_ktx_booking scripts.test_sillok_search scripts.test_korean_spell_check scripts.test_patent_search scripts.test_mfds_drug_safety scripts.test_nts_business_registration scripts.test_mfds_food_safety scripts.test_zipcode_search scripts.test_subway_lost_property scripts.test_geeknews_search scripts.test_naver_blog_search scripts.test_korean_slang_writing scripts.test_kakaotalk_mac scripts.test_coupang_partners_mcp_wrapper scripts.test_ohou_today_deal scripts.test_ticket_availability scripts.test_danawa_price_search && PYTHONPATH=.:scripts:korean-scholarship-search/scripts python3 -m unittest discover -s korean-scholarship-search/scripts -p 'test_scholarship_filter.py' && PYTHONPATH=.:scripts:kosis-stats/scripts python3 -m unittest discover -s kosis-stats/tests -p 'test_run_kosis_stats.py' && PYTHONPATH=.:scripts:kstartup-search/scripts python3 -m unittest discover -s kstartup-search/tests -p 'test_run_kstartup.py' && npm run test --workspaces --if-present && ./scripts/validate-skills.sh",
"pack:dry-run": "npm pack --workspace k-lotto --dry-run && npm pack --workspace daiso-product-search --dry-run && npm pack --workspace market-kurly-search --dry-run && npm pack --workspace blue-ribbon-nearby --dry-run && npm pack --workspace kakao-bar-nearby --dry-run && npm pack --workspace cheap-gas-nearby --dry-run && npm pack --workspace public-restroom-nearby --dry-run && npm pack --workspace parking-lot-search --dry-run && npm pack --workspace court-auction-notice-search --dry-run && npm pack --workspace donation-place-search --dry-run && npm pack --workspace gongsijiga-search --dry-run && npm pack --workspace kbl-results --dry-run && npm pack --workspace kleague-results --dry-run && npm pack --workspace lck-analytics --dry-run && npm pack --workspace toss-securities --dry-run && npm pack --workspace hipass-receipt --dry-run && npm pack --workspace used-car-price-search --dry-run && npm pack --workspace k-skill-rhwp --dry-run && npm pack --workspace korean-marathon-schedule --dry-run && npm pack --workspace gangnamunni-clinic-search --dry-run && npm pack --workspace daishin-report-search --dry-run && npm pack --workspace sh-notice-search --dry-run && npm pack --workspace emergency-room-beds --dry-run && npm pack --workspace local-election-candidate-search --dry-run",
@ -19,8 +19,8 @@
"release:npm": "changeset publish"
},
"devDependencies": {
"@types/node": "^22.14.1",
"@changesets/cli": "^2.29.5",
"@types/node": "^22.14.1",
"typescript": "^5.8.2"
}
}

View file

@ -0,0 +1,26 @@
# Build context MUST be repo root.
# packages/k-skill-proxy/src/parking-lots.js does:
# require("../../parking-lot-search/src/parse")
# so this image keeps both packages under /workspace/packages/ to preserve
# that relative require path. Do not flatten the layout.
FROM node:20-alpine AS deps
WORKDIR /app
COPY packages/k-skill-proxy/package.json ./package.json
RUN npm install --omit=dev --no-audit --no-fund \
&& npm cache clean --force
FROM node:20-alpine AS runtime
WORKDIR /workspace
ENV NODE_ENV=production \
KSKILL_PROXY_HOST=0.0.0.0 \
KSKILL_PROXY_PORT=8080
COPY --from=deps /app/node_modules ./packages/k-skill-proxy/node_modules
COPY --from=deps /app/package.json ./packages/k-skill-proxy/package.json
COPY packages/k-skill-proxy/src ./packages/k-skill-proxy/src
COPY packages/parking-lot-search/src ./packages/parking-lot-search/src
COPY packages/parking-lot-search/package.json ./packages/parking-lot-search/package.json
WORKDIR /workspace/packages/k-skill-proxy
EXPOSE 8080
USER node
# Cloud Run injects PORT; the server reads KSKILL_PROXY_PORT. Forward it.
CMD ["sh", "-c", "KSKILL_PROXY_PORT=${PORT:-8080} node src/server.js"]

View file

@ -262,6 +262,11 @@ curl -fsS --get 'http://127.0.0.1:4020/v1/kakao-local/geocode' \
```
## PM2 실행
## 프로덕션 배포
루트의 `ecosystem.config.cjs` + `scripts/run-k-skill-proxy.sh` 조합을 사용하면 재부팅 이후에도 같은 환경변수로 다시 올라옵니다.
프로덕션 프록시는 **Google Cloud Run** (`asia-northeast1`, GCP project `k-skill-proxy`)에서 운영되며, `k-skill-proxy.nomadamas.org` 도메인에 매핑되어 있습니다.
- 컨테이너 이미지: `packages/k-skill-proxy/Dockerfile`
- 자동 배포: `main` 브랜치 머지 시 `.github/workflows/deploy-k-skill-proxy.yml`이 Workload Identity Federation으로 GCP 인증 후 Artifact Registry로 이미지 빌드/푸시 → Cloud Run 재배포 → `/health` smoke test까지 수행합니다.
- 시크릿: GCP Secret Manager에서 Cloud Run runtime에 주입됩니다.
- 운영자 1회 셋업(WIF, Secret Manager, GitHub secrets) 절차는 [`docs/deploy-k-skill-proxy.md`](../../docs/deploy-k-skill-proxy.md) 참고.

View file

@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SECRETS_FILE="${KSKILL_SECRETS_FILE:-$HOME/.config/k-skill/secrets.env}"
if [[ -f "$SECRETS_FILE" ]]; then
set -a
# shellcheck disable=SC1090
source "$SECRETS_FILE"
set +a
fi
cd "$ROOT_DIR"
exec node packages/k-skill-proxy/src/server.js

View file

@ -177,7 +177,14 @@ test("repository publishes Korean contribution guidance for external contributor
assert.match(contributing, /릴리스나 패키징 관련 변경은 `npm run ci`/);
assert.match(contributing, /`~\/\.claude\/skills\/<skill-name>`/);
assert.match(contributing, /`~\/\.agents\/skills\/<skill-name>`/);
assert.match(contributing, /프로덕션 프록시는 `~\/\.local\/share\/k-skill-proxy`/);
assert.match(
contributing,
/프로덕션 프록시는 \*\*Google Cloud Run\*\* \(project `k-skill-proxy`, region `asia-northeast1`\)에서 운영하며 `k-skill-proxy\.nomadamas\.org` 도메인에 매핑/,
);
assert.match(
contributing,
/프로덕션 시크릿은 GCP Secret Manager에 보관되고 Cloud Run 런타임에 주입됩니다\. 프록시 운영자\(maintainer\)가 한 번 수행해야 하는 WIF\/Secret Manager 셋업과 운영 점검 절차는 \[`docs\/deploy-k-skill-proxy\.md`\]\(docs\/deploy-k-skill-proxy\.md\)에 정리/,
);
});
test("README links to the contribution guide", () => {