docs(contract): declare the publish recovery routes - #1009
Conversation
GET and DELETE /builds/{run_id}/publish/receipt, POST .../publish/reconcile
and GET .../publish/audit have answered since contract 1.19.0 and 1.20.0
but were only in the contract's prose. Declare them with their schemas
(1.76.0 -> 1.77.0, additive), add them to the route and status tables,
and check their real responses against the schemas.
Refs #994
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
리뷰 메모 — 리베이스 때 고쳐야 할 것. 계약 버전이 겹칩니다. 이 PR 은 스키마 본문(약 400줄)은 |
|
이 PR 이 #994 의 나머지 중 둘( |
…503 a stable code (#1023) Closes #1000 > **#1022(#996) 위에 쌓은 PR 이다.** base 를 그 브랜치로 잡아 이 PR 의 diff 만 보이게 했다. #1022 가 머지되면 base 를 `main` 으로 바꾼다. ## 문제 일부 에러 본문에 `code` 가 없어서 클라이언트가 문장으로 분기해야 한다. 특히 `POST /builds` 의 큐 초과 429 는 `auth_throttled` 429 와 문장으로만 구분됐다. ## 변경 내용 — 이슈의 네 가지 | 경우 | 상태 | `code` | |---|---|---| | async build 큐 초과 | 429 | `build_queue_full` | | 인증 실패 | 401 | `unauthorized`, 토큰의 `exp` 가 지났으면 `token_expired` | | JWKS 를 가져오지 못함 | 503 | `auth_unavailable` | | 과부하 (요청을 읽기 전 소켓 응답) | 503 | `server_overloaded` | | 재시작으로 중단된 run | — | `credentials_required` — #1022 에서 | - 본문의 `error` 문장은 그대로 둔다. `code` 만 추가된다. - 401 본문은 이슈가 "확인하지 않은 것"으로 남긴 부분이다. 확인: `{"error": <AuthError.reason>}` 이었고 `code` 가 없었다. `AuthError.code` 프로퍼티가 상태와 사유에서 코드를 정한다. - `token_expired` 를 따로 둔 이유: 클라이언트가 할 일이 다르다(토큰을 새로 받아 재시도). 나머지 401 은 `unauthorized` 하나다 — 어떤 검증이 실패했는지까지 코드로 나누지 않았다. - 계약 1.78.0 → **1.79.0** (additive): `Error.code` 설명에 코드 목록, `Unauthorized` 응답의 예시와 설명, `submitBuild` 429 예시. `Error` 스키마에는 이미 선택적 `code` 가 있어 스키마 구조는 바뀌지 않는다. ## 검증 - `tests/unit/test_stable_error_codes.py`(큐 초과 본문, 과부하 본문), `test_oidc_auth.py::TestStableAuthCodes`(거부·JWKS 불가·만료 토큰이 실제 응답 본문에 싣는 코드). - 인증·서비스·계약 fixture·job 관련 테스트 파일: `721 passed`. 전체 스위트는 돌리지 않았다 — CI 가 확인한다. - `check_contract_compat.py`: `origin/main` 대비 `1.77.0 -> 1.79.0` 호환. ruff, mypy 통과. ## 겹치는 것 #1009 가 계약의 같은 영역(공용 응답, 버전 줄)을 고친다. 나중에 머지되는 쪽에서 버전 줄을 맞춰야 한다. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Eomdahyeon <213566566+Eomdahyeon@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…03, and the request id (#1024) Refs #994 > **#1023(#1000) 위에 쌓은 PR 이다** (그 PR 은 #1022 위에 있다). base 를 그 브랜치로 잡아 이 PR 의 diff 만 보이게 했다. 과부하 503 의 `code` 가 #1023 에서 생기기 때문이다. ## 이 PR 이 하는 것 — #1009 가 "이 PR 에 없는 것"으로 남긴 세 항목 중 둘 1. **`X-Provider-Key` 를 header 파라미터로 선언** — `components.parameters.ProviderKey`. 프로바이더를 호출하는 다섯 operation(`previewBuild`, `createBuild`, `submitBuild`, `getProviderStatus`, `testProviderConnection`)에 참조를 달았다. Studio 가 이 헤더를 보내는 호출과 같은 다섯이다. 2. **429 `auth_throttled`, 과부하 503, `X-Request-ID`** — 어느 operation 에서나 나올 수 있으므로 `SignupNotApproved` 와 같은 방식(`x-status` 를 가진 공용 응답)으로 선언했다: `components.responses.AuthThrottled`, `ServerOverloaded`, 그리고 `components.headers.RequestId`. 계약 1.79.0 → **1.80.0**. wire 는 바뀌지 않는다(선언만). response fixture 를 생성기로 다시 만들었다. ## 하지 않은 것 3. **`_DISPATCH_ROUTES` 를 라우트 어댑터에서 생성** — #1009 본문이 적은 대로 라우트가 if 연쇄라 표로 바꾸는 리팩터링이 먼저다. 이 PR 범위에 넣지 않았다. 그래서 `Refs`. `X-Request-ID` 는 헤더 컴포넌트로만 선언했고 각 operation 의 응답마다 참조를 달지는 않았다 — 모든 응답(수백 개)에 다는 것은 계약을 읽기 어렵게 만든다고 판단했다. 설명에 "핸들러가 쓰는 모든 응답에 실린다"고 적었다. ## 검증 — 선언과 실제를 대조한다 `tests/unit/test_contract_shared_declarations.py`: - 파라미터의 이름이 코드의 `PROVIDER_KEY_HEADER` 와 같고, 참조한 operation 집합이 정확히 위 다섯이다 - `_overloaded_response()` 의 상태·본문·헤더가 `ServerOverloaded` 선언과 같다 - 인증 실패 한도를 넘긴 실제 응답의 상태·키·`code` 가 `AuthThrottled` 선언과 같다 - 실제 HTTP 응답에 `X-Request-ID` 가 있다 계약 관련 테스트 파일: `424 passed`. `check_contract_compat.py`: `origin/main` 대비 `1.77.0 -> 1.80.0` 호환. ruff, mypy 통과. 전체 스위트는 CI 가 돌린다. ## #1009 와의 관계 #1009 는 지금 `main` 과 충돌 상태다 — 내가 #1010 을 머지하면서 `main` 이 1.77.0 이 됐고, #1009 도 1.77.0 을 쓴다. 이 스택(#1022 → #1023 → 이 PR)은 1.78.0 ~ 1.80.0 을 쓴다. 어느 쪽이 먼저 들어가든 나중 쪽이 버전 줄을 다시 맞춰야 하니, 순서를 정해 주면 내 쪽을 그에 맞추겠다. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Eomdahyeon <213566566+Eomdahyeon@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
main 을 합쳤습니다. 충돌은 네 파일( 의존성 없이 돌릴 수 있는 것만 로컬에서 확인했습니다: 계약은 operation 68개로 읽힙니다. pytest·mypy 는 로컬에서 돌리지 못했습니다 ( |
… test (#746) Refs kpubdata-lab/kpubdata-builder#994, #728 ## 문제 kpubdata-lab/kpubdata-builder#1009 가 Builder `main` 에 들어가면서 계약이 1.81.0 이 됐고, publish 복구 라우트 네 개(`getPublishReceipt`, `resetPublishReceipt`, `reconcilePublish`, `getPublishAudit`)가 이름 붙은 예시와 함께 선언됐다. Studio 의 `Builder contract drift` 잡은 Builder `main` 의 계약을 읽고, 파싱하지도 목록에 올리지도 않은 예시가 있으면 실패한다. 그래서 지금 `main` 기준의 모든 PR 에서 이 잡이 실패한다. #744 의 잡 로그(재실행, Builder 1.81.0)가 근거다: `no Studio schema for getPublishReceipt; map it in OPERATION_SCHEMAS`, 그리고 `ERROR_READERS` 에 없는 에러 예시 5개. 순서를 제가 잘못 잡았다 — builder#1023 때처럼 Studio 가 예시를 먼저 알고 있어야 했는데 builder#1009 를 먼저 머지했다. ## 변경 `src/shared/lib/contractDrift.test.ts` 만 바꾼다. Studio 는 `main` 에서 네 라우트 중 어느 것도 부르지 않는다. - `OPERATION_SCHEMAS`: 네 operation 을 `skip` 으로 (부르지 않으므로 파싱할 본문이 없다). - `ERROR_READERS`: 에러 예시 5개를 `notHandled` 로, `code`(`receipt_not_found`, `reconcile_unavailable`)와 `since: "1.81.0"` 과 함께. - CHANGELOG `[Unreleased]`. #728(#744)이 `reconcilePublish`·`resetPublishReceipt` 를 부르기 시작하면 그 둘의 `skip` 과 `notHandled` 항목을 실제 스키마·reader 로 바꿔야 한다. ## 검증 **로컬에서 테스트를 돌리지 못했다** — 이 환경에서 `npm ci` 가 실패한다. 이 PR 의 `Builder contract drift` 잡이 Builder `main`(1.81.0)으로 도는 것이 첫 실행이자 판정이다. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lish page (#744) Closes #728 ## 문제 publish 가 `publish_state_unknown` 으로 끝나면 Studio 는 "자동 재시도하지 마세요, 운영자에게 요청하세요"만 보여 준다. Builder 에는 복구 경로(`reconcile`, receipt `reset`)가 있지만 Studio 가 부르지 않는다. 사용자가 UI 에서 할 수 있는 일이 없다. ## 변경 내용 - `builderApi.reconcilePublish`(`POST …/publish/reconcile`), `builderApi.resetPublishReceipt`(`DELETE …/publish/receipt?target&destination`). 둘 다 재시도 없음. 자격 증명은 `X-Publish-Credential` 헤더로만. - 응답 스키마 `publishReconcileResponseSchema`, `publishReceiptResetSchema` — 계약의 `PublishReconcileResponse`, `PublishReceiptReset` 와 이름으로 짝지어진다. - 게시 실패 카드에 **복구 패널**(`PublishRecoveryPanel`), `publish_state_unknown` 일 때만: - **원격 확인** → 있으면 "게시는 이루어졌습니다"(다시 보낼 버튼 없음), 없으면 기록이 지워지고 "다시 게시 준비" 버튼. - **기록 초기화** → 두 번째 클릭("초기화 확정")이 있어야 실행되고, "이미 올라간 것은 지워지지 않는다"는 경고를 먼저 보여 준다. - 원격을 읽지 못하면(503) "아무것도 바뀌지 않았다"고 말하고 재게시 버튼을 주지 않는다. - 기록이 없으면(404 `receipt_not_found`) 정리할 것이 없다고 말한다. - 요청에서 자격 증명을 받는 배포에서는 원격 확인에 토큰이 필요하다. Studio 는 게시 시작 때 토큰을 버리므로(#615) 다시 입력하게 하고, 그 한 번의 요청에만 쓰고 다시 버린다. - **어느 것도 자동으로 실행되지 않는다.** "다시 게시 준비"도 사용자가 누르고, 누른 뒤에는 원래의 확인 단계부터 다시 거친다. - ko/en 문구, CHANGELOG, `EXPECTED_OPERATIONS` 에 두 호출 추가. ## 의존 — kpubdata-lab/kpubdata-builder#1009 네 라우트는 Builder 에서 1.19/1.20 부터 응답해 왔지만 계약 `paths` 에는 #1009 가 처음 선언한다. 그 PR 이 Builder `main` 에 들어가기 전까지: - 이 PR 의 드리프트 검사는 두 스키마를 대조할 대상이 없어 그냥 지나간다(실패하지 않는다). - #743(클라이언트의 모든 경로가 계약에 있는지 검사)이 먼저 머지되면, 이 PR 은 그 검사에서 **실패한다** — 두 경로가 아직 계약에 없으므로. #1009 가 먼저 들어가야 한다. #1009 의 계약 파일로 드리프트 검사를 로컬에서 돌려 봤다: 스키마 대조 461개 통과. 실패한 2개는 계약 파일만 복사하고 fixture 디렉터리를 같이 두지 않아서 난 "fixture 가 옆에 있는가" 검사다 — 스키마와는 무관하다. ## 검증 - `__tests__/publishRecovery.test.tsx`(16개): 요청의 method·path·body·헤더, 토큰이 URL·body 에 없음, 404/503 분류와 재시도 없음, 다른 run 의 응답 불신, Builder 오류 문구를 그대로 보여 주지 않음, 패널이 누르기 전에는 아무것도 부르지 않음, reset 의 두 단계, 토큰 없이는 원격 확인을 하지 않음. - `npx vitest run`: `238 files passed, 2405 passed | 9 skipped`. `tsc --noEmit`, eslint 통과. - 실제 Builder 와 실제 Hugging Face 로 확인한 것은 아니다. 브라우저에서 화면을 직접 보지도 않았다. `review:R3` — 작성자가 아닌 사람의 승인이 필요하다. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Eomdahyeon <213566566+Eomdahyeon@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Refs #994 — 이 PR 은 그 이슈의 일부다 (publish 라우트 4개). 남는 것은 아래 "이 PR 에 없는 것".
문제
GET·DELETE /builds/{run_id}/publish/receipt,POST …/publish/reconcile,GET …/publish/audit는 계약 1.19.0 / 1.20.0 부터 응답해 왔지만contract/builder-api.yaml에는info.description의 산문으로만 있었다.paths에 없으니 클라이언트가 대조할 스키마가 없고,_DISPATCH_ROUTES에도 없어서 라우트 테스트가 이 넷을 몰랐다. Studio 가publish_state_unknown복구 UI 를 붙이려면 (kpubdata-lab/kpubdata-studio#728) 이 선언이 먼저다.변경 내용
contract/builder-api.yaml1.76.0 → 1.77.0 (additive): 네 operation (getPublishReceipt,resetPublishReceipt,reconcilePublish,getPublishAudit) 과 스키마PublishReceipt,PublishReconcileRequest,PublishReconcileResponse,PublishReceiptReset,PublishAuditLog,PublishAuditEntry. 형태는service/publish_api.py와service/routes/publish.py가 실제로 돌려주는 것을 읽어 적었다. wire 는 바뀌지 않는다.service/app.py:API_CONTRACT_VERSION = "1.77.0".contract/fixtures/responses.json: 생성기로 다시 만들었다 (2xx 54 → 59, error 37 → 42).tests/unit/test_service_contract.py:_DISPATCH_ROUTES,_REQUIRED_OPERATIONS,_IMPLEMENTED_OPERATIONS,_OPERATION_STATUS_CODES에 넷을 추가.tests/unit/test_service_publish.py::TestPublishRecoveryConformance: 실제 요청의 응답을 선언한 스키마와 대조한다 — unknown receipt, succeeded receipt(result포함),receipt_not_found404 (세 라우트), 쿼리 누락 400, reconcile 세 갈래(원격 있음 / 없음 / 읽을 수 없음 503), 모르는 필드 400, reset 과 그 뒤의 audit. 마지막 테스트는 부정 테스트다: 선언에 없는state값이 검사를 실패시키는지.이 PR 에 없는 것 (#994 에 남는다)
X-Provider-Key를 header 파라미터로 선언auth_throttled, 과부하 503,X-Request-ID응답 헤더_DISPATCH_ROUTES를 라우트 어댑터에서 생성 — 라우트가 if 연쇄라 표로 바꾸는 리팩터링이 먼저 필요하다검증
로컬에서 pytest 를 실행하지 못했다. 이 환경에서
files.pythonhosted.org에 연결되지 않아uv sync가 실패한다 (pypi.org는 200). 새 테스트는 이 PR 의 CI 가 첫 실행이다. 의존성 없이 돌릴 수 있는 것은 돌렸다:계약 YAML 은
yaml.safe_load로 읽어 operation 68개 (이전 64개) 를 확인했다.🤖 Generated with Claude Code