feat: remote-run-lifecycle milestone implementation and related updates
This commit is contained in:
parent
4d3f89b1c2
commit
3a82645002
20 changed files with 2117 additions and 92 deletions
|
|
@ -0,0 +1,86 @@
|
|||
# Milestone: agent 메시지 프로토콜
|
||||
|
||||
## 위치
|
||||
|
||||
- Roadmap: `agent-roadmap/ROADMAP.md`
|
||||
- Phase: `agent-roadmap/phase/message-based-build-agent/PHASE.md`
|
||||
|
||||
## 목표
|
||||
|
||||
`oto agent` 또는 `oto daemon` 모드에서 OTO Server와 runner가 양방향 메시지 통신을 하기 위한 기본 메시지 범위와 계약을 정의한다.
|
||||
|
||||
## 상태
|
||||
|
||||
[완료]
|
||||
|
||||
## 구현 잠금
|
||||
|
||||
- 상태: 해제
|
||||
- 결정 필요: 없음
|
||||
|
||||
## 범위
|
||||
|
||||
- runner register, capabilities, run request, step event, log stream, artifact event, cancel, status, self-update 메시지의 최소 필드를 정의한다.
|
||||
- 메시지 버전, 호환성, 에러 표현 방식을 정리한다.
|
||||
- OTO runner가 OTO Server의 build/deploy 전용 실행 표면이며 generic remote shell 하위 실행물이 아니라는 경계를 명시한다.
|
||||
|
||||
## 메시지 계약 요약
|
||||
|
||||
기본 메시지 목록과 방향성은 `proto/oto/runner.proto`의 `oto.runner.v1` 패키지를 기준으로 한다.
|
||||
|
||||
| 방향 | 메시지 | 목적 |
|
||||
|------|--------|------|
|
||||
| runner -> server | `RegisterRunnerRequest`, `RegisterRunnerResponse` | enrollment token, runner id, protocol version, capability, command catalog로 runner 등록과 거절 사유를 주고받는다. |
|
||||
| runner -> server | `HeartbeatRequest`, `HeartbeatResponse` | 등록된 runner의 생존 상태를 갱신하고 unknown runner 또는 terminal state를 거절한다. |
|
||||
| server -> operator -> runner | `BootstrapCommandRequest`, `BootstrapCommandResponse` | OTO Server가 agent 설치와 등록에 필요한 bootstrap command를 발급한다. |
|
||||
| runner -> server | `JobClaimRequest`, `JobClaimResponse` | runner가 OTO Server 소유 job/execution을 claim하고 `RunRequest`를 받는다. |
|
||||
| server -> runner | `RunRequest` | YAML path/content, 변수, 허용 command type 목록으로 OTO pipeline 실행을 지시한다. |
|
||||
| runner -> server | `ExecutionReportRequest`, `ExecutionReportResponse` | 실행 성공 여부, exit code, message, step event를 보고한다. |
|
||||
| runner -> server | `LogAppendRequest`, `LogAppendResponse` | 실행 로그 라인을 OTO Server execution log로 append한다. |
|
||||
| runner -> server | `ArtifactReportRequest`, `ArtifactReportResponse` | 실행 산출물 이름과 경로를 OTO Server execution artifact로 보고한다. |
|
||||
| server -> runner | `CancelRunRequest`, `CancelRunResponse` | 실행 취소 요청과 실패 사유를 표현한다. |
|
||||
| server -> runner | `RunnerStatusRequest`, `RunnerStatusResponse` | runner 상태와 현재 execution id 조회를 표현한다. |
|
||||
| server -> runner | `SelfUpdateRequest`, `SelfUpdateResponse` | runner self-update 버전과 다운로드 위치, 실패 사유를 표현한다. |
|
||||
|
||||
제품 경계는 OTO runner를 generic remote shell의 하위 실행물이 아니라 build/deploy 전용 실행 표면으로 두는 것이다. 원격 메시지는 기존 YAML pipeline과 command catalog를 호출하는 계약이며, 로컬 CLI 실행 경로와 YAML 형식을 대체하지 않는다. 따라서 새 메시지는 `RunRequest.command_types`, `StepEventReport`, log/artifact/report 메시지처럼 OTO pipeline 실행 의미를 보존하는 범위에서만 추가한다.
|
||||
|
||||
## 기능
|
||||
|
||||
### Epic: [message-contract] Server-runner 메시지 계약
|
||||
|
||||
OTO Server와 runner가 같은 의미로 해석할 기본 메시지 범위와 호환성 기준을 묶는다.
|
||||
|
||||
- [x] [message-list] 기본 메시지 목록과 방향성이 정의되어 있다.
|
||||
- [x] [message-fields] 각 메시지의 최소 필드와 실패 응답 형식이 정의되어 있다. 검증: OTO Server와 runner 구현자가 같은 메시지 계약을 기준으로 병렬 작업을 시작할 수 있고, YAML 파이프라인과 커맨드 모델이 메시지 위에서 어떻게 호출되는지 설명된다.
|
||||
- [x] [version-capability] 프로토콜 버전과 capability 협상 기준이 정의되어 있다.
|
||||
- [x] [domain-boundary] generic node 하위 실행물이 아니라는 제품 경계가 문서화되어 있다. 검증: 메시지 추가가 기존 CLI 실행 경로를 깨지 않는다는 경계가 명확하다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 승인됨
|
||||
- 요청일: 2026-06-06
|
||||
- 완료 근거: 모든 `message-contract` Task가 충족되었다. `message-fields`는 `agent-task/archive/2026/06/m-agent-message-protocol/01_proto_contract/complete.log`, `version-capability`는 `agent-task/archive/2026/06/m-agent-message-protocol/02+01_core_compatibility/complete.log`의 Roadmap Completion으로 충족되었다. `message-list`와 `domain-boundary`는 이 문서의 `메시지 계약 요약`에 직접 정리했다.
|
||||
- 리뷰 필요:
|
||||
- [x] 사용자가 완료 결과를 확인했다
|
||||
- [x] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 2026-06-06 사용자 요청에 따라 현 Milestone을 종료하고 다음 Milestone으로 전환한다.
|
||||
|
||||
## 범위 제외
|
||||
|
||||
- 웹 UI 또는 OTO Server 관리 화면을 구현하지 않는다.
|
||||
- 기존 YAML 파이프라인 형식을 대체하지 않는다.
|
||||
- 모든 운영 보안 정책을 완성하지 않는다.
|
||||
|
||||
## 작업 컨텍스트
|
||||
|
||||
- 관련 경로: `apps/runner/lib/oto/**`, `apps/runner/test/**`, `proto/oto/**`, `services/core/**`
|
||||
- CLI 자동화 표면 정리 단계의 구조화 실행 결과와 step event 기준을 먼저 확인한다.
|
||||
- 표준선(선택): 메시지 형태는 `proto/oto`의 OTO Server/Runner 계약과 구조화 실행 결과/step event 계약을 우선 활용하고, YAML 파이프라인 실행 경로를 대체하지 않는 방향으로 잡는다.
|
||||
- 선행 Milestone: 독립 Control Plane 분리 마이그레이션
|
||||
- 후속 의존: `control-plane-separation-migration`의 `oto-owns-cicd` Task가 OTO Server의 job/execution/log/artifact 소유권을 먼저 확정해야 한다.
|
||||
- 선행 완료 근거(2026-06-06): `agent-roadmap/archive/phase/independent-control-plane/milestones/control-plane-separation-migration.md`가 `[완료]`로 archive되었고, `agent-task/archive/2026/06/m-control-plane-separation-migration/07+05,06_cicd_smoke_evidence/complete.log`의 Roadmap Completion이 `oto-owns-cicd` 완료를 기록한다.
|
||||
- 책임 경계: iop는 이 Phase의 wire/protocol 소유자가 아니며, AI provider가 필요한 후속 기능에서만 선택적으로 소비된다.
|
||||
- 책임 경계: OTO는 build/deploy runner가 노출할 capabilities, YAML run request 연결, step/log/artifact event 의미를 정리한다.
|
||||
- 프로토콜 파일이나 생성 코드가 추가되면 관련 도메인 rule 또는 새 도메인 rule 필요 여부를 검토한다.
|
||||
- 완료 동기화(2026-06-06): `message-fields`와 `version-capability`는 각각 proto contract foundation과 Core/Dart compatibility review PASS 완료 로그를 근거로 반영했다.
|
||||
- 완료 동기화(2026-06-06): `message-list`와 `domain-boundary`는 큰 구현 계획 없이 현재 Milestone 문서의 메시지 계약 요약으로 직접 반영했다.
|
||||
|
|
@ -14,10 +14,10 @@ OTO를 generic remote shell이 아니라 build/deploy 전용 runner로 노출하
|
|||
완료된 Milestone은 archive 경로를 가리키고, 검토중, 진행중, 계획 또는 보류 Milestone은 이 Phase 하위 `milestones/` 경로를 가리킨다.
|
||||
완료, 검토중, 진행중, 계획 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다.
|
||||
|
||||
- [진행중] agent 메시지 프로토콜
|
||||
- 경로: `agent-roadmap/phase/message-based-build-agent/milestones/agent-message-protocol.md`
|
||||
- [완료] agent 메시지 프로토콜
|
||||
- 경로: `agent-roadmap/archive/phase/message-based-build-agent/milestones/agent-message-protocol.md`
|
||||
- 요약: OTO Server와 runner가 공유할 capabilities, run request, event/reporting 메시지 프로토콜을 정의한다.
|
||||
- [계획] 원격 실행 생명주기
|
||||
- [진행중] 원격 실행 생명주기
|
||||
- 경로: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
|
||||
- 요약: OTO Server가 소유하는 원격 실행, 로그, artifact, cancel, status, self-update 생명주기를 완성한다.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
# Milestone: agent 메시지 프로토콜
|
||||
|
||||
## 위치
|
||||
|
||||
- Roadmap: `agent-roadmap/ROADMAP.md`
|
||||
- Phase: `agent-roadmap/phase/message-based-build-agent/PHASE.md`
|
||||
|
||||
## 목표
|
||||
|
||||
`oto agent` 또는 `oto daemon` 모드에서 OTO Server와 runner가 양방향 메시지 통신을 하기 위한 기본 메시지 범위와 계약을 정의한다.
|
||||
|
||||
## 상태
|
||||
|
||||
[진행중]
|
||||
|
||||
## 구현 잠금
|
||||
|
||||
- 상태: 해제
|
||||
- 결정 필요: 없음
|
||||
|
||||
## 범위
|
||||
|
||||
- runner register, capabilities, run request, step event, log stream, artifact event, cancel, status, self-update 메시지의 최소 필드를 정의한다.
|
||||
- 메시지 버전, 호환성, 에러 표현 방식을 정리한다.
|
||||
- OTO runner가 OTO Server의 build/deploy 전용 실행 표면이며 generic remote shell 하위 실행물이 아니라는 경계를 명시한다.
|
||||
|
||||
## 기능
|
||||
|
||||
### Epic: [message-contract] Server-runner 메시지 계약
|
||||
|
||||
OTO Server와 runner가 같은 의미로 해석할 기본 메시지 범위와 호환성 기준을 묶는다.
|
||||
|
||||
- [ ] [message-list] 기본 메시지 목록과 방향성이 정의되어 있다.
|
||||
- [ ] [message-fields] 각 메시지의 최소 필드와 실패 응답 형식이 정의되어 있다. 검증: OTO Server와 runner 구현자가 같은 메시지 계약을 기준으로 병렬 작업을 시작할 수 있고, YAML 파이프라인과 커맨드 모델이 메시지 위에서 어떻게 호출되는지 설명된다.
|
||||
- [ ] [version-capability] 프로토콜 버전과 capability 협상 기준이 정의되어 있다.
|
||||
- [ ] [domain-boundary] generic node 하위 실행물이 아니라는 제품 경계가 문서화되어 있다. 검증: 메시지 추가가 기존 CLI 실행 경로를 깨지 않는다는 경계가 명확하다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 없음
|
||||
- 요청일: 없음
|
||||
- 완료 근거: 계획 상태이며 기능 Task와 Task 안의 검증이 아직 충족되지 않았다.
|
||||
- 리뷰 필요:
|
||||
- [ ] 사용자가 완료 결과를 확인했다
|
||||
- [ ] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 없음
|
||||
|
||||
## 범위 제외
|
||||
|
||||
- 웹 UI 또는 OTO Server 관리 화면을 구현하지 않는다.
|
||||
- 기존 YAML 파이프라인 형식을 대체하지 않는다.
|
||||
- 모든 운영 보안 정책을 완성하지 않는다.
|
||||
|
||||
## 작업 컨텍스트
|
||||
|
||||
- 관련 경로: `apps/runner/lib/oto/**`, `apps/runner/test/**`, `proto/oto/**`, `services/core/**`
|
||||
- CLI 자동화 표면 정리 단계의 구조화 실행 결과와 step event 기준을 먼저 확인한다.
|
||||
- 표준선(선택): 메시지 형태는 `proto/oto`의 OTO Server/Runner 계약과 구조화 실행 결과/step event 계약을 우선 활용하고, YAML 파이프라인 실행 경로를 대체하지 않는 방향으로 잡는다.
|
||||
- 선행 Milestone: 독립 Control Plane 분리 마이그레이션
|
||||
- 후속 의존: `control-plane-separation-migration`의 `oto-owns-cicd` Task가 OTO Server의 job/execution/log/artifact 소유권을 먼저 확정해야 한다.
|
||||
- 선행 완료 근거(2026-06-06): `agent-roadmap/archive/phase/independent-control-plane/milestones/control-plane-separation-migration.md`가 `[완료]`로 archive되었고, `agent-task/archive/2026/06/m-control-plane-separation-migration/07+05,06_cicd_smoke_evidence/complete.log`의 Roadmap Completion이 `oto-owns-cicd` 완료를 기록한다.
|
||||
- 책임 경계: iop는 이 Phase의 wire/protocol 소유자가 아니며, AI provider가 필요한 후속 기능에서만 선택적으로 소비된다.
|
||||
- 책임 경계: OTO는 build/deploy runner가 노출할 capabilities, YAML run request 연결, step/log/artifact event 의미를 정리한다.
|
||||
- 프로토콜 파일이나 생성 코드가 추가되면 관련 도메인 rule 또는 새 도메인 rule 필요 여부를 검토한다.
|
||||
- 확인 필요: 구현 착수 전 `oto-owns-cicd` 완료 근거를 확인한다.
|
||||
|
|
@ -11,7 +11,7 @@ OTO Server가 runner에 파이프라인 실행을 요청하고, runner가 실행
|
|||
|
||||
## 상태
|
||||
|
||||
[계획]
|
||||
[진행중]
|
||||
|
||||
## 구현 잠금
|
||||
|
||||
|
|
@ -58,6 +58,7 @@ OTO Server의 run request가 기존 YAML 파이프라인 실행, 진행 보고,
|
|||
- agent 메시지 프로토콜과 CLI 자동화 표면 정리 단계의 구조화 출력 기준을 먼저 확인한다.
|
||||
- 표준선(선택): run request는 기존 YAML 파이프라인 입력으로 변환하고, 진행 상태는 기존 step event/log/result 표면을 우선 재사용한다. cancel/status/self-update는 OTO Server 정책이 정해지기 전까지 build/deploy runner의 최소 안전 동작으로만 정리한다.
|
||||
- 선행 Milestone: agent 메시지 프로토콜
|
||||
- 선행 Milestone 완료 근거(2026-06-06): `agent-roadmap/archive/phase/message-based-build-agent/milestones/agent-message-protocol.md`가 `[완료]`로 archive되었고, `message-contract` Epic의 모든 Task가 완료 리뷰 승인 상태로 정리되었다.
|
||||
- 선행 Task: `control-plane-separation-migration`의 `oto-owns-cicd`
|
||||
- pipeline, command, core 도메인 rule이 관련될 수 있다.
|
||||
- 확인 필요: 구현 착수 전 `oto-owns-cicd`와 `agent-message-protocol` 완료 근거를 확인한다.
|
||||
- 확인 필요: 구현 착수 전 `oto-owns-cicd` 완료 근거를 확인한다.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
<!-- task=m-remote-run-lifecycle/01_run_request_pipeline plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
|
||||
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-06
|
||||
task=m-remote-run-lifecycle/01_run_request_pipeline, plan=0, tag=API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/01_run_request_pipeline/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-remote-run-lifecycle`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Store Run Input | [x] |
|
||||
| [API-2] HTTP Job Create And Claim Contract | [x] |
|
||||
| [API-3] Dart Claim Result Parses RunRequest | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] 서버 Store가 run request 입력을 job에 저장하고 copy/snapshot에서 누락하지 않는다.
|
||||
- [x] `POST /api/v1/jobs`가 `run_request`를 받아 검증하고, `GET /jobs/{id}`가 저장된 입력을 노출한다.
|
||||
- [x] runner claim 응답이 저장된 `RunRequest`를 채우며, missing/invalid run input은 명확한 오류로 거부한다.
|
||||
- [x] Dart `JobClaimResult`가 `runRequest`를 파싱하고 기존 명시 claim 테스트를 깨지 않는다.
|
||||
- [x] Go unit/smoke와 Dart agent/server smoke 테스트를 갱신한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [x] PASS이면 active task 디렉터리 `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/`를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/01_run_request_pipeline/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [x] PASS이고 task group이 `m-remote-run-lifecycle`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-remote-run-lifecycle/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- `CreateJob`은 이제 저장 포인터가 아니라 `copyJob(job)`을 반환한다. 계획은 "copy해서 저장"만 명시했으나, store 소유 객체가 호출자에게 그대로 노출되면 `RunInput`을 통해 저장값이 외부에서 변형될 수 있어 `GetJob`/`Snapshot`과 동일하게 반환도 deep copy로 통일했다. 기존 호출부(`handleCreateJob`)는 반환값을 즉시 `jobToJSON`에만 쓰므로 영향이 없다.
|
||||
- exactly-one(`pipeline_yaml` xor `pipeline_yaml_path`) 검증을 **create가 아니라 claim 시점**에서 강제했다. 계획의 "`pipeline_yaml`와 `pipeline_yaml_path` 중 정확히 하나를 remote claim 기준으로 인정"과 테스트 이름 `TestHandleRunnerCicdRejectsInvalidRunInput`(claim 핸들러)이 claim 기준 검증을 가리키기 때문이다. create는 backward 호환을 위해 `run_request` 없이도 `201`을 유지하고 `RunInput`을 `nil`로 둔다.
|
||||
- 거부 코드는 저장된 run input이 없으면 `409 Conflict`("job has no run request"), `pipeline_yaml`/`pipeline_yaml_path`가 둘 다이거나 둘 다 없으면 `400 Bad Request`로 분리했다. 계획이 허용한 "400 또는 409" 범위 안이다.
|
||||
- 계획이 명명한 `runRequestFromJSON` 헬퍼는 raw map이 아니라 create body에서 proto json 태그로 디코드된 `*otopb.RunRequest`를 입력으로 받아 `*cicdstate.RunInput`으로 변환한다. 서버의 다른 핸들러처럼 proto의 snake_case json 태그를 재사용해 별도 키 매핑 코드를 줄였다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- **proto 의존성 격리**: store는 proto를 import하지 않고 `cicdstate.RunInput`만 보유한다. `otopb.RunRequest` <-> `RunInput` 변환은 HTTP boundary(`runRequestFromJSON`/`runRequestToProto`)에서만 수행한다.
|
||||
- **deep copy 경계**: `copyRunInput`이 `Variables` map과 `CommandTypes` slice를 복제하고, `CreateJob`(입력)·`copyJob`(출력, `GetJob`/`Snapshot` 경유)에서 모두 적용해 store 값이 호출자 측 변형으로부터 양방향 격리된다.
|
||||
- **claim 응답 id 주입**: 저장된 `RunInput`은 pipeline 입력만 보관하고, runner/job/execution id는 claim 시점에 `runRequestToProto(in, runnerID, jobID, execID)`로 채운다. 따라서 어떤 runner가 claim해도 응답 `RunRequest`의 식별자가 실제 claim 컨텍스트와 일치한다.
|
||||
- **부작용 없는 거부**: run input 검증을 `CreateExecution`/`TransitionJob` 이전에 배치해, 거부된 claim이 execution 생성이나 상태 전이를 남기지 않는다(`TestHandleRunnerCicdRejectsInvalidRunInput`에서 job이 `queued` 유지·`ExecutionID` 비어있음을 확인).
|
||||
- **Dart 양식 호환 파싱**: `_parseRunRequest`가 snake_case와 camelCase 키를 모두 받아 `oto.RunRequest`를 구성하고, 기존 명시 claim 응답(`run_request` 없음)에서는 `runRequest=null`을 반환해 기존 흐름을 깨지 않는다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `RunInput`이 proto dependency 없이 store에 저장되고 deep copy되는지 확인한다.
|
||||
- claim 응답의 `run_request`가 서버 저장값과 runner/job/execution id를 모두 포함하는지 확인한다.
|
||||
- 기존 job/log/artifact smoke가 유지되는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### API-1 중간 검증
|
||||
```bash
|
||||
$ go test ./internal/cicdstate
|
||||
ok github.com/toki/oto/services/core/internal/cicdstate 0.002s
|
||||
|
||||
# 신규 테스트 확인 (-v)
|
||||
$ go test ./internal/cicdstate -run TestStoreCreatesJobWithRunInputCopy -v
|
||||
=== RUN TestStoreCreatesJobWithRunInputCopy
|
||||
--- PASS: TestStoreCreatesJobWithRunInputCopy (0.00s)
|
||||
PASS
|
||||
ok github.com/toki/oto/services/core/internal/cicdstate 0.002s
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```bash
|
||||
$ go test ./internal/httpserver
|
||||
ok github.com/toki/oto/services/core/internal/httpserver 0.005s
|
||||
|
||||
# 신규/갱신 테스트 확인 (-v)
|
||||
$ go test ./internal/httpserver -run 'RunRequest|RejectsInvalidRunInput|ClaimReturns|ClaimReport' -v
|
||||
=== RUN TestHandleCreateJobStoresRunRequest
|
||||
--- PASS: TestHandleCreateJobStoresRunRequest (0.00s)
|
||||
=== RUN TestHandleRunnerCicdClaimReturnsRunRequest
|
||||
--- PASS: TestHandleRunnerCicdClaimReturnsRunRequest (0.00s)
|
||||
=== RUN TestHandleRunnerCicdRejectsInvalidRunInput
|
||||
--- PASS: TestHandleRunnerCicdRejectsInvalidRunInput (0.00s)
|
||||
=== RUN TestHandleRunnerCicdClaimReportLogsAndArtifacts
|
||||
--- PASS: TestHandleRunnerCicdClaimReportLogsAndArtifacts (0.00s)
|
||||
PASS
|
||||
ok github.com/toki/oto/services/core/internal/httpserver 0.005s
|
||||
```
|
||||
|
||||
### API-3 중간 검증
|
||||
```bash
|
||||
$ dart analyze lib/oto/agent/oto_server_job_client.dart test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
|
||||
Analyzing oto_server_job_client.dart, oto_agent_registration_test.dart, oto_server_connection_smoke_test.dart...
|
||||
No issues found!
|
||||
|
||||
$ dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
|
||||
... (생략) ...
|
||||
00:03 +19: All tests passed!
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ cd services/core && go test ./...
|
||||
? github.com/toki/oto/services/core/cmd/oto-core [no test files]
|
||||
ok github.com/toki/oto/services/core/internal/cicdstate (cached)
|
||||
ok github.com/toki/oto/services/core/internal/httpserver (cached)
|
||||
ok github.com/toki/oto/services/core/internal/runnerregistry (cached)
|
||||
? github.com/toki/oto/services/core/oto [no test files]
|
||||
|
||||
$ cd apps/runner && dart analyze
|
||||
Analyzing runner...
|
||||
No issues found!
|
||||
|
||||
$ cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart
|
||||
... (진행 로그 생략) ...
|
||||
00:04 +83: All tests passed!
|
||||
```
|
||||
|
||||
> 참고: `oto_application_test.dart` 진행 중 출력되는 `There are no files in path /tmp/oto_missing_*.yaml`는 기존 "missing file without LateInitializationError" 테스트가 의도적으로 검증하는 정상 경로 메시지이며 실패가 아니다(최종 `All tests passed!`).
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## Section Ownership
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
|
||||
| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify |
|
||||
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
|
||||
| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
|
||||
| 검증 결과 | Implementing agent | Paste actual command output |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Pass
|
||||
- test coverage: Pass
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- plan deviation: Pass
|
||||
- verification trust: Pass
|
||||
- 발견된 문제: 없음
|
||||
- 다음 단계: PASS - active plan/review를 로그로 아카이브하고 `complete.log` 작성 후 task directory를 archive로 이동한다.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Complete - m-remote-run-lifecycle/01_run_request_pipeline
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-06-06
|
||||
|
||||
## 요약
|
||||
|
||||
원격 실행 job의 run request 저장, HTTP create/claim 계약, Dart claim result 파싱을 1회 리뷰로 PASS 완료.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | PASS | Required/Suggested 이슈 없음; 계획된 서버 저장/claim/Dart 파싱 및 검증 통과 |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- `cicdstate.Job`에 proto 비의존 `RunInput` 저장 모델과 deep copy 경계를 추가했다.
|
||||
- job create/get 및 runner claim HTTP 계약에 `run_request` 저장, 노출, claim 응답 주입, invalid run input 거부를 연결했다.
|
||||
- Dart `JobClaimResult`가 snake_case/camelCase `run_request`를 `oto.RunRequest`로 파싱하도록 확장하고 관련 smoke 테스트를 갱신했다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `cd services/core && go test ./...` - PASS; 전체 Go package 테스트 통과.
|
||||
- `cd services/core && go test ./internal/cicdstate -run TestStoreCreatesJobWithRunInputCopy -count=1 -v` - PASS; 신규 RunInput deep copy 테스트 실제 실행 통과.
|
||||
- `cd services/core && go test ./internal/httpserver -run 'RunRequest|RejectsInvalidRunInput|ClaimReturns|ClaimReport' -count=1 -v` - PASS; 신규/갱신 HTTP run_request 테스트 실제 실행 통과.
|
||||
- `cd apps/runner && dart analyze` - PASS; `No issues found!`.
|
||||
- `cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart` - PASS; `+83: All tests passed!`.
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
<!-- task=m-remote-run-lifecycle/01_run_request_pipeline plan=0 tag=API -->
|
||||
|
||||
# Plan - API
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
구현 완료의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 주요 설계 결정을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 구현 중 사용자 전용 결정, 사용자 소유 환경/secret, 범위 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 남기고 중단한다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 만들거나, 로그 아카이브를 수행하지 않는다. 재실행으로 채울 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
현재 OTO Server는 job을 만들고 runner가 claim할 수 있지만, job에 원격 실행 입력이 저장되지 않는다. `proto/oto/runner.proto`에는 `RunRequest`와 `JobClaimResponse.run_request`가 이미 있으나 서버 claim 응답은 이를 채우지 않는다. 이 계획은 run request를 서버 job 상태에 저장하고 runner가 claim할 때 기존 YAML 파이프라인 입력으로 받을 수 있는 계약을 만든다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/common/rules-roadmap.md`
|
||||
- `agent-ops/skills/common/router.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `agent-ops/rules/project/domain/agent/rules.md`
|
||||
- `agent-ops/rules/project/domain/core/rules.md`
|
||||
- `agent-ops/rules/project/domain/pipeline/rules.md`
|
||||
- `agent-ops/rules/project/domain/command/rules.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/agent-smoke.md`
|
||||
- `agent-test/local/core-smoke.md`
|
||||
- `agent-test/local/pipeline-smoke.md`
|
||||
- `agent-test/local/command-smoke.md`
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/message-based-build-agent/PHASE.md`
|
||||
- `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
|
||||
- `.gitignore`
|
||||
- `Makefile`
|
||||
- `proto/oto/runner.proto`
|
||||
- `services/core/go.mod`
|
||||
- `services/core/internal/cicdstate/store.go`
|
||||
- `services/core/internal/cicdstate/store_test.go`
|
||||
- `services/core/internal/httpserver/server.go`
|
||||
- `services/core/internal/httpserver/server_test.go`
|
||||
- `services/core/internal/runnerregistry/registry.go`
|
||||
- `apps/runner/pubspec.yaml`
|
||||
- `apps/runner/lib/oto/agent/oto_server_job_client.dart`
|
||||
- `apps/runner/lib/oto/agent/edge_registration_client.dart`
|
||||
- `apps/runner/lib/oto/agent/agent_runner.dart`
|
||||
- `apps/runner/lib/oto/application.dart`
|
||||
- `apps/runner/lib/oto/core/build_result.dart`
|
||||
- `apps/runner/lib/oto/core/data_composer.dart`
|
||||
- `apps/runner/lib/oto/core/execution_context.dart`
|
||||
- `apps/runner/lib/oto/pipeline/pipeline.dart`
|
||||
- `apps/runner/lib/cli/commands/command_exe.dart`
|
||||
- `apps/runner/test/oto_agent_registration_test.dart`
|
||||
- `apps/runner/test/oto_server_connection_smoke_test.dart`
|
||||
- `apps/runner/test/oto_application_test.dart`
|
||||
- `apps/runner/test/oto_context_test.dart`
|
||||
- `apps/runner/test/oto_core_test.dart`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
- test_env: `local`.
|
||||
- `agent-test/local/rules.md`를 읽었다.
|
||||
- 적용 프로파일: `agent-smoke`, `core-smoke`, `pipeline-smoke`, `command-smoke`.
|
||||
- agent/core/pipeline/command 프로파일의 Dart 기준은 `dart analyze`와 관련 `dart test`다.
|
||||
- `services/core/**`와 `proto/oto/**`에 대한 agent-test 프로파일은 없다. 이 계획은 `Makefile`의 `core-test`와 `proto-go`/`proto-dart` 타깃을 fallback 검증 근거로 쓴다.
|
||||
- `<확인 필요>` 값은 없었다.
|
||||
- 사전 검증으로 `go test ./...`, `dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart`, `dart test test/oto_server_connection_smoke_test.dart`가 통과했다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- 서버 job 생성은 `id/name`만 저장하며 run request 저장 테스트가 없다.
|
||||
- runner claim 응답은 `RunRequest` 필드를 프로토콜에 노출하지만 서버 응답에 채우는 테스트가 없다.
|
||||
- queue claim 또는 서버 소유 execution id 선택 기준이 없다. 기존 client는 명시 `jobId/executionId` claim만 검증한다.
|
||||
- YAML 입력 변환 자체는 `Application.build(BuildType.file, yamlContent: ...)` 테스트가 커버하지만, 서버 claim 응답과 연결되어 있지 않다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
- renamed/removed symbol: none.
|
||||
- 변경 후보 call site: `Store.CreateJob` 호출은 `services/core/internal/httpserver/server.go`, `services/core/internal/httpserver/server_test.go`, `services/core/internal/cicdstate/store_test.go`에서 확인했다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
- split decision policy를 먼저 평가했다.
|
||||
- 공유 task group: `m-remote-run-lifecycle`.
|
||||
- `01_run_request_pipeline`: 선행 없음. 서버 job/run request 저장과 claim 응답 계약을 먼저 만든다.
|
||||
- `02+01_step_log_stream`: `01`의 active/archived `complete.log`가 아직 없으므로 predecessor `01`은 missing이다.
|
||||
- `03+01_artifact_event`: predecessor `01`은 missing이다.
|
||||
- `04+01_cancel_status_update`: predecessor `01`은 missing이다.
|
||||
- 서버 계약이 없으면 runner loop, artifact, cancel/status/self-update 구현이 추측으로 흐르므로 단일 plan보다 split이 안전하다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- 이 계획은 서버 job/run request 저장, claim 응답, Dart job client의 claim result 파싱까지만 다룬다.
|
||||
- 이 plan은 후속 구현을 가능하게 하는 선행 계약이며, PASS되어도 roadmap Task를 체크하지 않는다.
|
||||
- runner가 claim된 request를 실제 실행하는 loop는 `02+01_step_log_stream`으로 분리한다.
|
||||
- artifact 자동 수집은 `03+01_artifact_event`로 분리한다.
|
||||
- cancel/status/self-update endpoint와 상태 전이는 `04+01_cancel_status_update`로 분리한다.
|
||||
- 새 protobuf 메시지는 만들지 않는다. 기존 `RunRequest`와 `JobClaimResponse.run_request`를 사용한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
- build: `cloud-G07`. Go service 상태 저장, Dart client 파싱, cross-language protocol JSON 계약을 함께 바꾸는 작업이다.
|
||||
- review: `cloud-G07`. server/client/test가 같은 계약을 구현했는지 교차 확인해야 한다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] 서버 Store가 run request 입력을 job에 저장하고 copy/snapshot에서 누락하지 않는다.
|
||||
- [ ] `POST /api/v1/jobs`가 `run_request`를 받아 검증하고, `GET /jobs/{id}`가 저장된 입력을 노출한다.
|
||||
- [ ] runner claim 응답이 저장된 `RunRequest`를 채우며, missing/invalid run input은 명확한 오류로 거부한다.
|
||||
- [ ] Dart `JobClaimResult`가 `runRequest`를 파싱하고 기존 명시 claim 테스트를 깨지 않는다.
|
||||
- [ ] Go unit/smoke와 Dart agent/server smoke 테스트를 갱신한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [API-1] Store Run Input
|
||||
|
||||
문제: [services/core/internal/cicdstate/store.go](/config/workspace/oto/services/core/internal/cicdstate/store.go:25)의 `Job`은 execution id만 저장하고 run input을 저장하지 않는다.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
// services/core/internal/cicdstate/store.go:25
|
||||
type Job struct {
|
||||
ID string
|
||||
Name string
|
||||
State string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ExecutionID string
|
||||
}
|
||||
```
|
||||
|
||||
해결 방법: `cicdstate.RunInput`을 추가하고 `Job`에 `RunInput *RunInput`을 둔다. proto package를 store에 직접 끌어오지 말고 HTTP boundary에서 `otopb.RunRequest` <-> `cicdstate.RunInput` 변환을 수행한다. `CreateJob`은 run input pointer를 optional로 받되 copy해서 저장한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `services/core/internal/cicdstate/store.go`: `RunInput` 구조체, deep copy helper, `Job.RunInput`, `CreateJob` signature 추가.
|
||||
- [ ] `services/core/internal/cicdstate/store_test.go`: 저장/복사/변경 누수 방지 테스트 추가.
|
||||
|
||||
테스트 작성: `TestStoreCreatesJobWithRunInputCopy`를 추가한다. variables/commandTypes slice/map을 외부에서 바꿔도 store 값이 바뀌지 않아야 한다.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
go test ./internal/cicdstate
|
||||
```
|
||||
|
||||
### [API-2] HTTP Job Create And Claim Contract
|
||||
|
||||
문제: [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:485)의 job create request는 `id/name`만 받고, [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:776)의 claim response는 `RunRequest`를 비워 둔다.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
// services/core/internal/httpserver/server.go:487
|
||||
var req struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// services/core/internal/httpserver/server.go:776
|
||||
writeResponse(w, http.StatusOK, &otopb.JobClaimResponse{
|
||||
Accepted: true,
|
||||
RunnerId: runnerID,
|
||||
JobId: jobID,
|
||||
ExecutionId: execID,
|
||||
State: cicdstate.StateRunning,
|
||||
})
|
||||
```
|
||||
|
||||
해결 방법: create body에 `run_request`를 추가한다. `pipeline_yaml`와 `pipeline_yaml_path` 중 정확히 하나를 remote claim 기준으로 인정하고, `variables`와 `command_types`는 optional로 보존한다. claim 시 저장된 input이 없거나 둘 다/둘 다 없음이면 `400` 또는 `409`로 거부하고, 응답에는 runner/job/execution id를 채운 `RunRequest`를 포함한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `services/core/internal/httpserver/server.go`: `runRequestFromJSON`, `runRequestToProto`, `runInputToJSON` helper 추가.
|
||||
- [ ] `services/core/internal/httpserver/server.go`: `handleCreateJob`, `jobToJSON`, `handleRunnerClaimJob` 갱신.
|
||||
- [ ] `services/core/internal/httpserver/server_test.go`: create/get/claim run_request 테스트와 invalid input 테스트 추가.
|
||||
|
||||
테스트 작성: `TestHandleCreateJobStoresRunRequest`, `TestHandleRunnerCicdClaimReturnsRunRequest`, `TestHandleRunnerCicdRejectsInvalidRunInput`를 추가한다.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
go test ./internal/httpserver
|
||||
```
|
||||
|
||||
### [API-3] Dart Claim Result Parses RunRequest
|
||||
|
||||
문제: [apps/runner/lib/oto/agent/oto_server_job_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/oto_server_job_client.dart:9)의 `JobClaimResult`는 `run_request`를 보존하지 않는다. 이후 runner loop가 서버 입력을 실행할 수 없다.
|
||||
|
||||
Before:
|
||||
|
||||
```dart
|
||||
// apps/runner/lib/oto/agent/oto_server_job_client.dart:9
|
||||
class JobClaimResult {
|
||||
final bool accepted;
|
||||
final String? errorMessage;
|
||||
final String jobId;
|
||||
final String executionId;
|
||||
final String state;
|
||||
```
|
||||
|
||||
해결 방법: `JobClaimResult`에 `oto.RunRequest? runRequest`를 추가하고 snake_case/camelCase JSON 모두 파싱한다. `claimJob`의 기존 API는 유지하되, 후속 `02`가 쓸 `claimNextJob()` 추가 여지를 남긴다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/runner/lib/oto/agent/oto_server_job_client.dart`: run request JSON parser helper 추가.
|
||||
- [ ] `apps/runner/test/oto_agent_registration_test.dart`: claim response에 `run_request`가 있을 때 Dart 객체에 보존되는 테스트 추가.
|
||||
- [ ] `apps/runner/test/oto_server_connection_smoke_test.dart`: 서버 create job body에 inline YAML `run_request`를 넣고 claim 결과를 확인한다.
|
||||
|
||||
테스트 작성: 기존 `job client claims jobs and reports build results` 테스트를 확장한다.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
|
||||
```
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `services/core/internal/cicdstate/store.go` | API-1 |
|
||||
| `services/core/internal/cicdstate/store_test.go` | API-1 |
|
||||
| `services/core/internal/httpserver/server.go` | API-2 |
|
||||
| `services/core/internal/httpserver/server_test.go` | API-2 |
|
||||
| `apps/runner/lib/oto/agent/oto_server_job_client.dart` | API-3 |
|
||||
| `apps/runner/test/oto_agent_registration_test.dart` | API-3 |
|
||||
| `apps/runner/test/oto_server_connection_smoke_test.dart` | API-3 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd services/core && go test ./...
|
||||
cd apps/runner && dart analyze
|
||||
cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart
|
||||
```
|
||||
|
||||
예상 결과: 모든 명령이 exit code 0으로 종료한다. Go test cache 출력은 이 계획에서 허용한다. Dart test는 실제 stdout/stderr를 review stub에 붙인다.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
<!-- task=m-remote-run-lifecycle/02+01_step_log_stream plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
|
||||
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-06
|
||||
task=m-remote-run-lifecycle/02+01_step_log_stream, plan=0, tag=API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-cloud-G06.md` -> `code_review_cloud_G06_N.log`, `PLAN-local-G07.md` -> `plan_local_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/02+01_step_log_stream/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
|
||||
4. PASS이고 task group이 `m-remote-run-lifecycle`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
|
||||
- Task ids:
|
||||
- `run-input`
|
||||
- `step-log`
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] RunRequest To DataBuild Input | [ ] |
|
||||
| [API-2] Remote Job Loop | [ ] |
|
||||
| [API-3] Step Events And Logs Report | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `01_run_request_pipeline`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다.
|
||||
- [ ] `RunRequest`를 inline YAML 또는 workspace-confined file path 중 하나로 해석하고 variables를 pipeline property 입력으로 병합한다.
|
||||
- [ ] runner job loop가 claim -> 실행 -> log append -> execution report 순서를 보장한다.
|
||||
- [ ] `BuildResult.stepEvents`가 `ExecutionReportRequest.step_events`와 `execution_result.stepEvents`에 보존된다.
|
||||
- [ ] agent/core/pipeline 테스트와 server connection smoke를 갱신한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-cloud-G06.md`를 `code_review_cloud_G06_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-local-G07.md`를 `plan_local_G07_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/`를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/02+01_step_log_stream/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-remote-run-lifecycle`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-remote-run-lifecycle/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- predecessor `01`의 완료 산출물 없이는 구현이 진행되지 않았는지 확인한다.
|
||||
- remote variables 병합이 기존 YAML validation과 command registry 경로를 우회하지 않는지 확인한다.
|
||||
- job loop가 실패 결과도 report하고 step events/logs를 누락하지 않는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
|
||||
### API-1 중간 검증
|
||||
```bash
|
||||
$ dart test test/oto_agent_registration_test.dart test/oto_application_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```bash
|
||||
$ dart test test/oto_agent_registration_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-3 중간 검증
|
||||
```bash
|
||||
$ dart test test/oto_server_connection_smoke_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ cd apps/runner && dart analyze
|
||||
(output)
|
||||
$ cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart test/oto_server_connection_smoke_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## Section Ownership
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
|
||||
| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify |
|
||||
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
|
||||
| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
|
||||
| 검증 결과 | Implementing agent | Paste actual command output |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
<!-- task=m-remote-run-lifecycle/02+01_step_log_stream plan=0 tag=API -->
|
||||
|
||||
# Plan - API
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
구현 완료의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 주요 설계 결정을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 구현 중 사용자 전용 결정, 사용자 소유 환경/secret, 범위 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 남기고 중단한다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 만들거나, 로그 아카이브를 수행하지 않는다.
|
||||
|
||||
## 배경
|
||||
|
||||
서버와 Dart client에는 claim/report/log API 조각이 있지만, `DefaultAgentRunner`의 기본 실행 경로는 등록 후 job loop를 실행하지 않는다. `Application.build(BuildType.file, yamlContent: ...)`는 기존 YAML 파이프라인 실행과 step event 결과를 이미 제공한다. 이 계획은 claim된 `RunRequest`를 YAML 실행 입력으로 변환하고 결과와 step events를 OTO Server에 보고하는 runner loop를 만든다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
|
||||
- Task ids:
|
||||
- `run-input`: run request가 pipeline 실행 입력으로 변환되는 기준이 정의되어 있다.
|
||||
- `step-log`: step event와 log stream이 원격 소비자에게 전달되는 흐름이 정의되어 있다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/PLAN-cloud-G07.md`
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/common/rules-roadmap.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `agent-ops/rules/project/domain/agent/rules.md`
|
||||
- `agent-ops/rules/project/domain/core/rules.md`
|
||||
- `agent-ops/rules/project/domain/pipeline/rules.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/agent-smoke.md`
|
||||
- `agent-test/local/core-smoke.md`
|
||||
- `agent-test/local/pipeline-smoke.md`
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/message-based-build-agent/PHASE.md`
|
||||
- `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
|
||||
- `apps/runner/pubspec.yaml`
|
||||
- `apps/runner/lib/oto/agent/agent_runner.dart`
|
||||
- `apps/runner/lib/oto/agent/edge_registration_client.dart`
|
||||
- `apps/runner/lib/oto/agent/oto_server_job_client.dart`
|
||||
- `apps/runner/lib/oto/application.dart`
|
||||
- `apps/runner/lib/oto/core/build_result.dart`
|
||||
- `apps/runner/lib/oto/core/data_composer.dart`
|
||||
- `apps/runner/lib/oto/core/execution_context.dart`
|
||||
- `apps/runner/lib/oto/pipeline/pipeline.dart`
|
||||
- `apps/runner/lib/cli/commands/command_exe.dart`
|
||||
- `apps/runner/test/oto_agent_registration_test.dart`
|
||||
- `apps/runner/test/oto_server_connection_smoke_test.dart`
|
||||
- `apps/runner/test/oto_application_test.dart`
|
||||
- `apps/runner/test/oto_context_test.dart`
|
||||
- `apps/runner/test/oto_core_test.dart`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
- test_env: `local`.
|
||||
- 읽은 프로파일: `agent-smoke`, `core-smoke`, `pipeline-smoke`.
|
||||
- 필수 baseline: `dart analyze`, agent 관련 `dart test`, core 관련 `dart test`, pipeline 영향 시 `dart test` 전체 또는 관련 회귀.
|
||||
- Go service는 이 plan의 직접 변경 대상이 아니며, `01` 완료 결과를 전제로 한다.
|
||||
- 사전 검증으로 `dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart`와 `dart test test/oto_server_connection_smoke_test.dart`가 통과했다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- `AgentRunner`는 주입된 `runJobs` 훅 실행만 테스트하고 기본 remote job loop는 없다.
|
||||
- `OtoServerJobClient`는 claim/report/log/artifact 호출만 테스트하며, claim result의 `RunRequest`를 실행하지 않는다.
|
||||
- `Application.build`의 step events는 테스트되지만 OTO Server report payload로 자동 전송되는 end-to-end 테스트가 없다.
|
||||
- log stream은 현재 명시 `appendLog` 호출만 검증하고, runner 실행 시작/완료 로그와 BuildResult message 보고 흐름은 자동화되어 있지 않다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
- renamed/removed symbol: none.
|
||||
- 변경 후보 call site: `DefaultAgentRunner` 생성은 agent CLI와 테스트에서 간접 사용된다. `OtoServerJobSession.jobs`는 `oto_agent_registration_test.dart`와 `oto_server_connection_smoke_test.dart`에서 확인했다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
- split decision policy를 먼저 평가했다.
|
||||
- 이 subtask는 `02+01_step_log_stream`이므로 predecessor `01`이 필요하다.
|
||||
- 현재 active `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/complete.log`가 없고 archive predecessor도 확인하지 않았다. predecessor `01`: missing.
|
||||
- `01`이 claim response에 `RunRequest`를 보장해야 이 plan을 구현할 수 있다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- 이 계획은 runner가 claim한 run request를 실행하고 logs/step events/result를 보고하는 데 집중한다.
|
||||
- artifact 자동 추출/보고는 `03+01_artifact_event`로 분리한다.
|
||||
- cancel/status/self-update는 `04+01_cancel_status_update`로 분리한다.
|
||||
- server claim 계약 변경은 `01_run_request_pipeline`의 완료 결과를 사용한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
- build: `local-G07`. Dart runner/core/pipeline 경계가 넓지만 기존 `Application.build`와 deterministic tests로 검증 가능하다.
|
||||
- review: `cloud-G06`. 서버 계약 선행 여부와 runner loop 증거를 함께 확인해야 한다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `01_run_request_pipeline`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다.
|
||||
- [ ] `RunRequest`를 inline YAML 또는 workspace-confined file path 중 하나로 해석하고 variables를 pipeline property 입력으로 병합한다.
|
||||
- [ ] runner job loop가 claim -> 실행 -> log append -> execution report 순서를 보장한다.
|
||||
- [ ] `BuildResult.stepEvents`가 `ExecutionReportRequest.step_events`와 `execution_result.stepEvents`에 보존된다. 검증: OTO Server에서 요청한 빌드/배포 작업의 시작, 진행, 완료, 실패, 취소 상태를 추적할 수 있다.
|
||||
- [ ] agent/core/pipeline 테스트와 server connection smoke를 갱신한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
- Runtime dependency: sibling `01_run_request_pipeline` must produce `complete.log`.
|
||||
- 이 plan은 `agent-task/m-remote-run-lifecycle/02+01_step_log_stream` 이름의 `+01` 의존성만 가진다. 추가 의존성을 plan 본문에 숨기지 않는다.
|
||||
|
||||
### [API-1] RunRequest To DataBuild Input
|
||||
|
||||
문제: [apps/runner/lib/oto/application.dart](/config/workspace/oto/apps/runner/lib/oto/application.dart:76)는 `BuildType.file`에서 YAML 문자열 실행은 지원하지만 remote variables 병합과 workspace-confined path 해석 기준은 없다.
|
||||
|
||||
Before:
|
||||
|
||||
```dart
|
||||
// apps/runner/lib/oto/application.dart:76
|
||||
Future<BuildResult> build(BuildType buildType,
|
||||
{String? yamlContent,
|
||||
DataBuild? buildData,
|
||||
bool logEnable = true,
|
||||
String? logPath}) async {
|
||||
```
|
||||
|
||||
해결 방법: 새 helper `RemoteRunInput` 또는 `OtoRemoteRunExecutor`를 만들어 `oto.RunRequest`를 검증한다. `pipelineYaml`이 있으면 inline을 우선하고, `pipelineYamlPath`는 `AgentRuntimeConfig.workspaceRoot` 아래의 정규화된 파일만 허용한다. `variables`는 parsed YAML map의 `property`에 병합한 뒤 기존 `DataBuild.fromJson`/`Application.build` 경로를 재사용한다. 필요하면 `BuildType.remote`를 추가해 `DataBuild`를 log file 없이 실행한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/runner/lib/oto/agent/remote_run_executor.dart` 추가 또는 기존 agent 파일에 작게 포함한다.
|
||||
- [ ] `apps/runner/lib/oto/application.dart`: `BuildType.remote`가 필요하면 최소 분기 추가.
|
||||
- [ ] `apps/runner/test/oto_agent_registration_test.dart`: inline YAML, path confinement, variables merge 테스트 추가.
|
||||
|
||||
테스트 작성: 정상 inline YAML, both inline/path rejection, path traversal rejection, variables property merge 테스트를 추가한다.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
dart test test/oto_agent_registration_test.dart test/oto_application_test.dart
|
||||
```
|
||||
|
||||
### [API-2] Remote Job Loop
|
||||
|
||||
문제: [apps/runner/lib/oto/agent/agent_runner.dart](/config/workspace/oto/apps/runner/lib/oto/agent/agent_runner.dart:63)는 `_runJobs`가 주입된 경우만 실행한다. 기본 runner는 등록 후 remote job을 claim하지 않는다.
|
||||
|
||||
Before:
|
||||
|
||||
```dart
|
||||
// apps/runner/lib/oto/agent/agent_runner.dart:63
|
||||
if (_runJobs != null) {
|
||||
await _runJobs(config, session);
|
||||
}
|
||||
```
|
||||
|
||||
해결 방법: `OtoRemoteRunLoop`를 기본 job loop로 연결한다. loop는 accepted `OtoServerJobSession`에서만 동작하고, `claimNextJob()` 또는 `claimJob()` 결과가 empty/no job이면 interval 후 재시도한다. 테스트 가능한 `runOnce`를 제공해 infinite loop 없이 검증한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/runner/lib/oto/agent/agent_runner.dart`: default run loop 주입 또는 생성.
|
||||
- [ ] `apps/runner/lib/oto/agent/oto_server_job_client.dart`: `claimNextJob()`와 no-job response 파싱 추가.
|
||||
- [ ] `apps/runner/test/oto_agent_registration_test.dart`: default runner가 run loop를 호출하고 session close를 유지하는 테스트 수정.
|
||||
|
||||
테스트 작성: fake client/server로 `claim -> execute -> report` 순서와 no-job no-op을 검증한다.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
dart test test/oto_agent_registration_test.dart
|
||||
```
|
||||
|
||||
### [API-3] Step Events And Logs Report
|
||||
|
||||
문제: [apps/runner/lib/oto/pipeline/pipeline.dart](/config/workspace/oto/apps/runner/lib/oto/pipeline/pipeline.dart:77)는 started/completed/failed step events를 기록하지만, [apps/runner/lib/oto/agent/oto_server_job_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/oto_server_job_client.dart:107)의 report는 호출자가 직접 불러야 한다.
|
||||
|
||||
Before:
|
||||
|
||||
```dart
|
||||
// apps/runner/lib/oto/agent/oto_server_job_client.dart:107
|
||||
Future<ExecutionReportResult> reportExecution({
|
||||
required String jobId,
|
||||
required String executionId,
|
||||
required BuildResult result,
|
||||
}) async {
|
||||
```
|
||||
|
||||
해결 방법: remote loop가 실행 시작 로그를 `appendLog`로 보내고, `Application.build`가 반환한 `BuildResult`를 항상 `reportExecution`으로 보낸다. 실패 결과도 success=false, exitCode=10, stepEvents 포함으로 보고한다. report message는 server가 기존처럼 execution log에 append한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/runner/lib/oto/agent/remote_run_executor.dart`: 시작/완료/실패 로그와 report 순서 구현.
|
||||
- [ ] `apps/runner/test/oto_server_connection_smoke_test.dart`: 실제 Go server와 Dart runner loop가 inline YAML job을 실행하고 logs/stepEvents/result를 확인하는 smoke 추가.
|
||||
|
||||
테스트 작성: smoke에서 `POST /api/v1/jobs`에 inline YAML run_request를 넣고 runner `runOnce`가 job state succeeded, logs, step_events를 남기는지 확인한다.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
dart test test/oto_server_connection_smoke_test.dart
|
||||
```
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `apps/runner/lib/oto/agent/remote_run_executor.dart` | API-1, API-2, API-3 |
|
||||
| `apps/runner/lib/oto/agent/agent_runner.dart` | API-2 |
|
||||
| `apps/runner/lib/oto/agent/oto_server_job_client.dart` | API-2, API-3 |
|
||||
| `apps/runner/lib/oto/application.dart` | API-1 |
|
||||
| `apps/runner/test/oto_agent_registration_test.dart` | API-1, API-2 |
|
||||
| `apps/runner/test/oto_application_test.dart` | API-1 |
|
||||
| `apps/runner/test/oto_server_connection_smoke_test.dart` | API-3 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd apps/runner && dart analyze
|
||||
cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart test/oto_server_connection_smoke_test.dart
|
||||
```
|
||||
|
||||
예상 결과: 모든 명령이 exit code 0으로 종료한다. Dart test cache 개념은 적용하지 않는다.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
<!-- task=m-remote-run-lifecycle/03+01,02_artifact_event plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
|
||||
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-06
|
||||
task=m-remote-run-lifecycle/03+01,02_artifact_event, plan=0, tag=API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G06.md` -> `code_review_local_G06_N.log`, `PLAN-local-G06.md` -> `plan_local_G06_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/03+01,02_artifact_event/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
|
||||
4. PASS이고 task group이 `m-remote-run-lifecycle`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
|
||||
- Task ids:
|
||||
- `artifact-event`
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] BuildResult Artifact Model | [ ] |
|
||||
| [API-2] Artifact Declaration Extraction | [ ] |
|
||||
| [API-3] Remote Artifact Reporting | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `01_run_request_pipeline`과 `02+01_step_log_stream`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다.
|
||||
- [ ] artifact metadata는 name/path 필수, path는 빈 값 금지로 검증한다.
|
||||
- [ ] `BuildResult`가 artifact 목록을 보존하고 `toJson()`에 포함한다.
|
||||
- [ ] remote loop가 build 성공/실패와 별개로 산출된 artifact 목록을 서버에 보고한다.
|
||||
- [ ] artifact report 실패는 execution report와 구분되는 명확한 실패 결과 또는 로그로 남긴다.
|
||||
- [ ] agent/core/pipeline/server smoke 테스트를 갱신한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-local-G06.md`를 `code_review_local_G06_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-local-G06.md`를 `plan_local_G06_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/m-remote-run-lifecycle/03+01,02_artifact_event/`를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/03+01,02_artifact_event/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-remote-run-lifecycle`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-remote-run-lifecycle/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- predecessor `01`, `02`의 완료 산출물 없이는 구현이 진행되지 않았는지 확인한다.
|
||||
- artifact path 검증이 workspace 밖 경로를 조용히 허용하지 않는지 확인한다.
|
||||
- remote loop가 build result artifacts를 server artifact endpoint에 실제로 남기는지 smoke evidence를 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
|
||||
### API-1 중간 검증
|
||||
```bash
|
||||
$ dart test test/oto_core_test.dart test/oto_application_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```bash
|
||||
$ dart test test/oto_agent_registration_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-3 중간 검증
|
||||
```bash
|
||||
$ dart test test/oto_server_connection_smoke_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ cd apps/runner && dart analyze
|
||||
(output)
|
||||
$ cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart test/oto_server_connection_smoke_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## Section Ownership
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
|
||||
| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify |
|
||||
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
|
||||
| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
|
||||
| 검증 결과 | Implementing agent | Paste actual command output |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
<!-- task=m-remote-run-lifecycle/03+01,02_artifact_event plan=0 tag=API -->
|
||||
|
||||
# Plan - API
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
구현 완료의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 주요 설계 결정을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 구현 중 사용자 전용 결정, 사용자 소유 환경/secret, 범위 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 남기고 중단한다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 만들거나, 로그 아카이브를 수행하지 않는다.
|
||||
|
||||
## 배경
|
||||
|
||||
OTO Server와 Dart job client에는 artifact report API가 이미 있다. 하지만 `Application.build` 결과에는 artifact 목록이 없고, remote job loop가 빌드 결과에서 artifact event를 자동 보고하는 기준도 없다. 이 계획은 기존 서버 API를 유지하면서 runner 결과 모델과 remote loop에 artifact event 전달 흐름을 붙인다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
|
||||
- Task ids:
|
||||
- `artifact-event`: artifact event와 최종 artifact metadata가 원격 소비자에게 전달되는 흐름이 정의되어 있다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/PLAN-cloud-G07.md`
|
||||
- `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/PLAN-local-G07.md`
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/common/rules-roadmap.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `agent-ops/rules/project/domain/agent/rules.md`
|
||||
- `agent-ops/rules/project/domain/core/rules.md`
|
||||
- `agent-ops/rules/project/domain/pipeline/rules.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/agent-smoke.md`
|
||||
- `agent-test/local/core-smoke.md`
|
||||
- `agent-test/local/pipeline-smoke.md`
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/message-based-build-agent/PHASE.md`
|
||||
- `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
|
||||
- `services/core/internal/httpserver/server.go`
|
||||
- `apps/runner/lib/oto/agent/agent_runner.dart`
|
||||
- `apps/runner/lib/oto/agent/oto_server_job_client.dart`
|
||||
- `apps/runner/lib/oto/application.dart`
|
||||
- `apps/runner/lib/oto/core/build_result.dart`
|
||||
- `apps/runner/lib/oto/core/data_composer.dart`
|
||||
- `apps/runner/lib/oto/pipeline/pipeline.dart`
|
||||
- `apps/runner/test/oto_agent_registration_test.dart`
|
||||
- `apps/runner/test/oto_server_connection_smoke_test.dart`
|
||||
- `apps/runner/test/oto_application_test.dart`
|
||||
- `apps/runner/test/oto_core_test.dart`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
- test_env: `local`.
|
||||
- 읽은 프로파일: `agent-smoke`, `core-smoke`, `pipeline-smoke`.
|
||||
- 직접 변경 대상은 Dart runner/core/pipeline이다.
|
||||
- 서버 artifact endpoint는 이미 존재하므로 Go 변경은 기본 범위가 아니다. 서버 응답 계약이 부족한 것이 발견되면 계획 대비 변경에 기록하고 `cd services/core && go test ./...`를 최종 검증에 추가한다.
|
||||
- 사전 검증으로 Dart agent/core/pipeline/server smoke 테스트가 통과했다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- `BuildResult`는 success/exitCode/error/stepEvents만 직렬화하고 artifact metadata를 담지 않는다.
|
||||
- `OtoServerJobClient.reportArtifact`는 단건 보고만 제공하며 build result의 artifacts 목록을 일괄 보고하지 않는다.
|
||||
- server smoke는 수동 artifact report를 검증하지만, remote job loop가 build 결과 artifact를 보고하는 end-to-end 테스트가 없다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
- renamed/removed symbol: none.
|
||||
- 변경 후보 call site: `BuildResult.success`, `BuildResult.failure`, `BuildResult.toJson`, `OtoServerJobClient.reportArtifact`, 후속 `02`의 remote loop executor.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
- split decision policy를 먼저 평가했다.
|
||||
- 이 subtask는 `03+01,02_artifact_event`이므로 predecessor `01`, `02`가 필요하다.
|
||||
- 현재 active `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/complete.log`와 `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/complete.log`가 없고 archive predecessor도 확인하지 않았다. predecessor `01`: missing, predecessor `02`: missing.
|
||||
- `01`은 claim/run request 계약을 제공하고, `02`는 remote loop의 claim/execution/report 흐름을 제공한다. artifact event는 그 흐름에 붙어야 하므로 추가 숨은 의존성을 두지 않는다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- 이 계획은 artifact metadata 모델, artifact 선언 파싱, remote loop의 reportArtifact 호출까지 다룬다.
|
||||
- artifact 파일 업로드/보관소 구현은 현재 server API가 path metadata만 받으므로 범위 밖이다.
|
||||
- cancel/status/self-update는 `04+01,02_cancel_status_update`로 분리한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
- build: `local-G06`. Dart 모델과 runner loop 보강이며 기존 server artifact API를 재사용한다.
|
||||
- review: `local-G06`. predecessor 산출물과 server smoke 결과를 확인해야 한다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `01_run_request_pipeline`과 `02+01_step_log_stream`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다.
|
||||
- [ ] artifact metadata는 name/path 필수, path는 빈 값 금지로 검증한다.
|
||||
- [ ] `BuildResult`가 artifact 목록을 보존하고 `toJson()`에 포함한다.
|
||||
- [ ] remote loop가 build 성공/실패와 별개로 산출된 artifact 목록을 서버에 보고한다.
|
||||
- [ ] artifact report 실패는 execution report와 구분되는 명확한 실패 결과 또는 로그로 남긴다.
|
||||
- [ ] agent/core/pipeline/server smoke 테스트를 갱신한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
- Runtime dependency: sibling `01_run_request_pipeline` must produce `complete.log`.
|
||||
- Runtime dependency: sibling `02+01_step_log_stream` must produce `complete.log`.
|
||||
- 이 plan은 `agent-task/m-remote-run-lifecycle/03+01,02_artifact_event` 이름의 `+01,02` 의존성만 가진다. 추가 의존성을 plan 본문에 숨기지 않는다.
|
||||
|
||||
### [API-1] BuildResult Artifact Model
|
||||
|
||||
문제: [apps/runner/lib/oto/core/build_result.dart](/config/workspace/oto/apps/runner/lib/oto/core/build_result.dart:3)의 `BuildResult`는 artifact metadata를 보존하지 않는다.
|
||||
|
||||
Before:
|
||||
|
||||
```dart
|
||||
class BuildResult {
|
||||
final bool success;
|
||||
final int exitCode;
|
||||
final Object? error;
|
||||
final StackTrace? stackTrace;
|
||||
final List<StepEvent> stepEvents;
|
||||
```
|
||||
|
||||
해결 방법: `BuildArtifact` value class를 추가하고 `BuildResult.success`/`failure`에 `artifacts = const []` optional parameter를 둔다. `toJson()`에는 `artifacts` 배열을 포함한다. 기존 생성자 호출은 모두 source compatible하게 유지한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/runner/lib/oto/core/build_result.dart`: `BuildArtifact`와 `BuildResult.artifacts` 추가.
|
||||
- [ ] `apps/runner/test/oto_core_test.dart` 또는 `apps/runner/test/oto_application_test.dart`: `toJson()` artifact serialization 테스트 추가.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
dart test test/oto_core_test.dart test/oto_application_test.dart
|
||||
```
|
||||
|
||||
### [API-2] Artifact Declaration Extraction
|
||||
|
||||
문제: YAML pipeline 실행 결과에서 어떤 파일을 artifact로 보고할지에 대한 runner-side 기준이 없다.
|
||||
|
||||
해결 방법: remote executor가 build YAML의 `property.artifacts`를 artifact 선언으로 인정한다. 형식은 list of map이며 각 entry는 `name`, `path`를 가진다. `path`는 `Application.defaultWorkspace` 기준 상대 경로 또는 workspace-confined 절대 경로만 허용한다. 유효하지 않은 artifact 선언은 build 실패로 처리해 조용히 누락하지 않는다.
|
||||
|
||||
예시:
|
||||
|
||||
```yaml
|
||||
property:
|
||||
artifacts:
|
||||
- name: smoke-report
|
||||
path: dist/report.json
|
||||
```
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/runner/lib/oto/agent/remote_run_executor.dart`: artifact declaration parser 추가.
|
||||
- [ ] `apps/runner/lib/oto/application.dart`: 필요 시 build result 생성 시 artifact 목록을 받을 수 있는 내부 hook 추가.
|
||||
- [ ] `apps/runner/test/oto_agent_registration_test.dart`: artifact parser 정상/오류/path confinement 테스트 추가.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
dart test test/oto_agent_registration_test.dart
|
||||
```
|
||||
|
||||
### [API-3] Remote Artifact Reporting
|
||||
|
||||
문제: [apps/runner/lib/oto/agent/oto_server_job_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/oto_server_job_client.dart:151)는 단건 `reportArtifact`만 제공하고 remote loop가 자동 호출하지 않는다.
|
||||
|
||||
해결 방법: `OtoServerJobClient.reportArtifacts` helper를 추가하고 remote loop가 `BuildResult.artifacts`를 순서대로 보고한다. 보고 순서는 execution report 이후로 고정한다. artifact report 실패는 log append 후 execution failure로 재보고할 수 없으므로, 실패를 throw하고 caller test에서 HTTP 오류가 드러나게 한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/runner/lib/oto/agent/oto_server_job_client.dart`: `reportArtifacts` 추가.
|
||||
- [ ] `apps/runner/lib/oto/agent/remote_run_executor.dart`: build result artifacts 보고 연결.
|
||||
- [ ] `apps/runner/test/oto_server_connection_smoke_test.dart`: inline YAML job이 artifact metadata를 server artifacts endpoint에 남기는 smoke 추가.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
dart test test/oto_server_connection_smoke_test.dart
|
||||
```
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `apps/runner/lib/oto/core/build_result.dart` | API-1 |
|
||||
| `apps/runner/lib/oto/agent/remote_run_executor.dart` | API-2, API-3 |
|
||||
| `apps/runner/lib/oto/application.dart` | API-2 |
|
||||
| `apps/runner/lib/oto/agent/oto_server_job_client.dart` | API-3 |
|
||||
| `apps/runner/test/oto_core_test.dart` | API-1 |
|
||||
| `apps/runner/test/oto_application_test.dart` | API-1, API-2 |
|
||||
| `apps/runner/test/oto_agent_registration_test.dart` | API-2 |
|
||||
| `apps/runner/test/oto_server_connection_smoke_test.dart` | API-3 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd apps/runner && dart analyze
|
||||
cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_application_test.dart test/oto_context_test.dart test/oto_core_test.dart test/oto_server_connection_smoke_test.dart
|
||||
```
|
||||
|
||||
예상 결과: 모든 명령이 exit code 0으로 종료한다.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
<!-- task=m-remote-run-lifecycle/04+01,02_cancel_status_update plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
|
||||
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-06
|
||||
task=m-remote-run-lifecycle/04+01,02_cancel_status_update, plan=0, tag=API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/04+01,02_cancel_status_update/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
|
||||
4. PASS이고 task group이 `m-remote-run-lifecycle`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
|
||||
- Task ids:
|
||||
- `cancel-status-update`
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Cancellable State Transitions | [ ] |
|
||||
| [API-2] HTTP Cancel And Status Endpoints | [ ] |
|
||||
| [API-3] Self-Update Stability Contract | [ ] |
|
||||
| [API-4] Dart Lifecycle Client Helpers | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `01_run_request_pipeline`과 `02+01_step_log_stream`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다.
|
||||
- [ ] running job/execution을 canceled로 전이할 수 있고 terminal state는 변경하지 않는다.
|
||||
- [ ] cancel endpoint는 runner/job/execution id mismatch를 거부하고, 성공 시 job과 execution state를 모두 `canceled`로 맞춘다.
|
||||
- [ ] status endpoint는 runner registry 상태와 현재/마지막 execution 상태를 반환한다.
|
||||
- [ ] self-update endpoint는 active execution 중에는 defer/reject하고, download URL은 HTTPS만 허용한다. 실제 바이너리 교체는 하지 않는다.
|
||||
- [ ] Dart job client가 cancel/status/self-update helper를 제공하고 snake_case/camelCase JSON을 파싱한다.
|
||||
- [ ] Go service와 Dart agent/server smoke 테스트를 갱신한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-cloud-G07.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-cloud-G07.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/m-remote-run-lifecycle/04+01,02_cancel_status_update/`를 `agent-task/archive/YYYY/MM/m-remote-run-lifecycle/04+01,02_cancel_status_update/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-remote-run-lifecycle`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-remote-run-lifecycle/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- predecessor `01`, `02`의 완료 산출물 없이는 구현이 진행되지 않았는지 확인한다.
|
||||
- running -> canceled 전이는 허용하되 terminal state 변경은 막는지 확인한다.
|
||||
- self-update endpoint가 실제 바이너리 교체를 수행하지 않고 최소 안정성 규칙만 정의하는지 확인한다.
|
||||
- Dart smoke가 실제 server endpoint와 같은 JSON 계약을 검증하는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
|
||||
### API-1 중간 검증
|
||||
```bash
|
||||
$ go test ./internal/cicdstate
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```bash
|
||||
$ go test ./internal/httpserver
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-3 중간 검증
|
||||
```bash
|
||||
$ go test ./internal/httpserver
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-4 중간 검증
|
||||
```bash
|
||||
$ dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ cd services/core && go test ./...
|
||||
(output)
|
||||
$ cd apps/runner && dart analyze
|
||||
(output)
|
||||
$ cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## Section Ownership
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
|
||||
| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify |
|
||||
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
|
||||
| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
|
||||
| 검증 결과 | Implementing agent | Paste actual command output |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
<!-- task=m-remote-run-lifecycle/04+01,02_cancel_status_update plan=0 tag=API -->
|
||||
|
||||
# Plan - API
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
구현 완료의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 주요 설계 결정을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 구현 중 사용자 전용 결정, 사용자 소유 환경/secret, 범위 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 남기고 중단한다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 만들거나, 로그 아카이브를 수행하지 않는다.
|
||||
|
||||
## 배경
|
||||
|
||||
`proto/oto/runner.proto`에는 cancel, runner status, self-update 메시지가 이미 정의되어 있다. 현재 서버는 runner claim/report/log/artifact endpoint를 제공하지만 cancel/status/self-update HTTP endpoint와 running -> canceled 상태 전이가 없다. 이 계획은 기존 proto를 재사용해 원격 실행 생명주기의 운영 제어면을 닫는다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
|
||||
- Task ids:
|
||||
- `cancel-status-update`: cancel, status, self-update의 최소 안정성 규칙이 정의되어 있다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/PLAN-cloud-G07.md`
|
||||
- `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/PLAN-local-G07.md`
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/common/rules-roadmap.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `agent-ops/rules/project/domain/agent/rules.md`
|
||||
- `agent-ops/rules/project/domain/core/rules.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/agent-smoke.md`
|
||||
- `agent-test/local/core-smoke.md`
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/message-based-build-agent/PHASE.md`
|
||||
- `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
|
||||
- `proto/oto/runner.proto`
|
||||
- `services/core/internal/cicdstate/store.go`
|
||||
- `services/core/internal/cicdstate/store_test.go`
|
||||
- `services/core/internal/httpserver/server.go`
|
||||
- `services/core/internal/httpserver/server_test.go`
|
||||
- `services/core/internal/runnerregistry/registry.go`
|
||||
- `apps/runner/lib/oto/agent/agent_runner.dart`
|
||||
- `apps/runner/lib/oto/agent/edge_registration_client.dart`
|
||||
- `apps/runner/lib/oto/agent/oto_server_job_client.dart`
|
||||
- `apps/runner/test/oto_agent_registration_test.dart`
|
||||
- `apps/runner/test/oto_server_connection_smoke_test.dart`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
- test_env: `local`.
|
||||
- 읽은 프로파일: `agent-smoke`, `core-smoke`.
|
||||
- Go service 변경은 `go test ./...`로 검증한다.
|
||||
- Dart agent client 변경은 `dart analyze`와 agent/server smoke로 검증한다.
|
||||
- 사전 검증으로 `go test ./...`, Dart agent/core/server smoke 테스트가 통과했다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- store 상태 전이는 queued -> canceled만 허용하고 running -> canceled를 허용하지 않는다.
|
||||
- 서버는 cancel/status/self-update proto 메시지를 HTTP endpoint로 받지 않는다.
|
||||
- Dart job client는 cancel/status/self-update request helper가 없다.
|
||||
- self-update는 실제 바이너리 교체를 수행하기 전에 active execution과 URL scheme을 확인하는 최소 안정성 규칙이 없다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
- renamed/removed symbol: none.
|
||||
- 변경 후보 call site: `validTransitions`, `Store.TransitionJob`, `Store.TransitionExecution`, `Server` route switch, `OtoServerJobClient`.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
- split decision policy를 먼저 평가했다.
|
||||
- 이 subtask는 `04+01,02_cancel_status_update`이므로 predecessor `01`, `02`가 필요하다.
|
||||
- 현재 active `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/complete.log`와 `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/complete.log`가 없고 archive predecessor도 확인하지 않았다. predecessor `01`: missing, predecessor `02`: missing.
|
||||
- `01`은 execution/job identity 계약을 제공하고, `02`는 runner loop의 active execution 개념을 제공한다. cancel/status/self-update는 이 생명주기 위에서 동작하므로 추가 숨은 의존성을 두지 않는다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- 이 계획은 HTTP/proto JSON boundary, store 상태 전이, Dart client helper, smoke 테스트를 다룬다.
|
||||
- 실제 self-update 바이너리 다운로드/교체/재시작은 위험도가 높고 플랫폼별 정책이 필요하므로 범위 밖이다. 이 milestone에서는 최소 안정성 규칙과 accepted/deferred response만 정의한다.
|
||||
- UI, auth, persistent DB migration은 범위 밖이다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
- build: `cloud-G07`. Go server state machine과 Dart client protocol을 함께 바꾸며 remote lifecycle 제어 의미가 크다.
|
||||
- review: `cloud-G07`. 상태 전이, HTTP status, Dart smoke가 서로 맞는지 교차 확인해야 한다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `01_run_request_pipeline`과 `02+01_step_log_stream`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다.
|
||||
- [ ] running job/execution을 canceled로 전이할 수 있고 terminal state는 변경하지 않는다.
|
||||
- [ ] cancel endpoint는 runner/job/execution id mismatch를 거부하고, 성공 시 job과 execution state를 모두 `canceled`로 맞춘다.
|
||||
- [ ] status endpoint는 runner registry 상태와 현재/마지막 execution 상태를 반환한다.
|
||||
- [ ] self-update endpoint는 active execution 중에는 defer/reject하고, download URL은 HTTPS만 허용한다. 실제 바이너리 교체는 하지 않는다.
|
||||
- [ ] Dart job client가 cancel/status/self-update helper를 제공하고 snake_case/camelCase JSON을 파싱한다.
|
||||
- [ ] Go service와 Dart agent/server smoke 테스트를 갱신한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
- Runtime dependency: sibling `01_run_request_pipeline` must produce `complete.log`.
|
||||
- Runtime dependency: sibling `02+01_step_log_stream` must produce `complete.log`.
|
||||
- 이 plan은 `agent-task/m-remote-run-lifecycle/04+01,02_cancel_status_update` 이름의 `+01,02` 의존성만 가진다. 추가 의존성을 plan 본문에 숨기지 않는다.
|
||||
|
||||
### [API-1] Cancellable State Transitions
|
||||
|
||||
문제: [services/core/internal/cicdstate/store.go](/config/workspace/oto/services/core/internal/cicdstate/store.go:17)의 `validTransitions`는 `StateRunning -> StateCanceled`를 허용하지 않는다.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
var validTransitions = map[string][]string{
|
||||
StateQueued: {StateRunning, StateFailed, StateCanceled},
|
||||
StateRunning: {StateSucceeded, StateFailed},
|
||||
StateSucceeded: nil,
|
||||
StateFailed: nil,
|
||||
StateCanceled: nil,
|
||||
}
|
||||
```
|
||||
|
||||
해결 방법: `StateRunning`에서 `StateCanceled`로 전이 가능하게 하고, job/execution을 함께 취소하는 store helper를 추가한다. 이미 succeeded/failed/canceled인 terminal state는 그대로 거부한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `services/core/internal/cicdstate/store.go`: running cancel 전이와 `CancelExecution` 또는 `CancelJobExecution` helper 추가.
|
||||
- [ ] `services/core/internal/cicdstate/store_test.go`: queued/running cancel, terminal cancel rejection, job/execution 동시 갱신 테스트 추가.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
go test ./internal/cicdstate
|
||||
```
|
||||
|
||||
### [API-2] HTTP Cancel And Status Endpoints
|
||||
|
||||
문제: [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:420)의 runner route switch는 claim/report/log/artifact만 처리하고 cancel/status endpoint가 없다.
|
||||
|
||||
해결 방법: 기존 proto JSON 필드를 쓰는 endpoint를 추가한다. 제안 route:
|
||||
|
||||
- `POST /api/v1/runners/{runner_id}/executions/{execution_id}/cancel`
|
||||
- `GET /api/v1/runners/{runner_id}/status`
|
||||
|
||||
cancel은 `CancelRunRequest`의 runner/job/execution id가 path와 일치하는지 확인한다. status는 `RunnerStatusResponse`와 호환되는 JSON을 반환하고, 최소한 `accepted`, `runner_id`, `status`, `current_job_id`, `current_execution_id`, `message`를 포함한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `services/core/internal/httpserver/server.go`: route, request decoder, response writer 추가.
|
||||
- [ ] `services/core/internal/httpserver/server_test.go`: cancel 성공/불일치/terminal rejection/status 조회 테스트 추가.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
go test ./internal/httpserver
|
||||
```
|
||||
|
||||
### [API-3] Self-Update Stability Contract
|
||||
|
||||
문제: proto에는 `SelfUpdateRequest`/`SelfUpdateResponse`가 있으나 서버/runner 경계의 안정성 규칙이 없다.
|
||||
|
||||
해결 방법: `POST /api/v1/runners/{runner_id}/self-update` endpoint를 추가한다. request의 `download_url`은 HTTPS만 허용하고, active running execution이 있으면 `accepted=false`, `deferred=true`, `message`를 반환한다. active execution이 없으면 `accepted=true`, `deferred=false`, `target_version`과 `message`를 반환한다. 실제 다운로드와 교체는 하지 않는다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `services/core/internal/httpserver/server.go`: self-update endpoint와 validation 추가.
|
||||
- [ ] `services/core/internal/httpserver/server_test.go`: invalid URL, active execution defer, idle accepted 테스트 추가.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
go test ./internal/httpserver
|
||||
```
|
||||
|
||||
### [API-4] Dart Lifecycle Client Helpers
|
||||
|
||||
문제: [apps/runner/lib/oto/agent/oto_server_job_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/oto_server_job_client.dart:68)는 claim/report/log/artifact만 제공한다.
|
||||
|
||||
해결 방법: `cancelRun`, `fetchRunnerStatus`, `requestSelfUpdate` helper와 response result classes를 추가한다. 기존 helper의 timeout/error handling 패턴을 재사용한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/runner/lib/oto/agent/oto_server_job_client.dart`: lifecycle helper와 result parser 추가.
|
||||
- [ ] `apps/runner/test/oto_agent_registration_test.dart`: request payload/response parser/error handling 테스트 추가.
|
||||
- [ ] `apps/runner/test/oto_server_connection_smoke_test.dart`: 실제 server cancel/status/self-update smoke 추가.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
|
||||
```
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `services/core/internal/cicdstate/store.go` | API-1 |
|
||||
| `services/core/internal/cicdstate/store_test.go` | API-1 |
|
||||
| `services/core/internal/httpserver/server.go` | API-2, API-3 |
|
||||
| `services/core/internal/httpserver/server_test.go` | API-2, API-3 |
|
||||
| `apps/runner/lib/oto/agent/oto_server_job_client.dart` | API-4 |
|
||||
| `apps/runner/test/oto_agent_registration_test.dart` | API-4 |
|
||||
| `apps/runner/test/oto_server_connection_smoke_test.dart` | API-4 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd services/core && go test ./...
|
||||
cd apps/runner && dart analyze
|
||||
cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
|
||||
```
|
||||
|
||||
예상 결과: 모든 명령이 exit code 0으로 종료한다.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -12,6 +12,7 @@ class JobClaimResult {
|
|||
final String jobId;
|
||||
final String executionId;
|
||||
final String state;
|
||||
final oto.RunRequest? runRequest;
|
||||
|
||||
const JobClaimResult({
|
||||
required this.accepted,
|
||||
|
|
@ -19,6 +20,7 @@ class JobClaimResult {
|
|||
required this.executionId,
|
||||
required this.state,
|
||||
this.errorMessage,
|
||||
this.runRequest,
|
||||
});
|
||||
|
||||
factory JobClaimResult.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -37,6 +39,7 @@ class JobClaimResult {
|
|||
executionId: (json['execution_id'] ?? json['executionId'] ?? '')
|
||||
.toString(),
|
||||
state: (json['state'] ?? '').toString(),
|
||||
runRequest: _parseRunRequest(json['run_request'] ?? json['runRequest']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -288,3 +291,36 @@ oto.StepEventReport _stepEventReport(StepEvent event) {
|
|||
}
|
||||
|
||||
String? _emptyToNull(String value) => value.isEmpty ? null : value;
|
||||
|
||||
/// Parses a claim response `run_request` payload (snake_case or camelCase) into
|
||||
/// an [oto.RunRequest]. Returns null when the server omitted the run request.
|
||||
oto.RunRequest? _parseRunRequest(dynamic raw) {
|
||||
if (raw is! Map) {
|
||||
return null;
|
||||
}
|
||||
final request = oto.RunRequest();
|
||||
final runnerId = (raw['runner_id'] ?? raw['runnerId'] ?? '').toString();
|
||||
if (runnerId.isNotEmpty) request.runnerId = runnerId;
|
||||
final jobId = (raw['job_id'] ?? raw['jobId'] ?? '').toString();
|
||||
if (jobId.isNotEmpty) request.jobId = jobId;
|
||||
final executionId = (raw['execution_id'] ?? raw['executionId'] ?? '')
|
||||
.toString();
|
||||
if (executionId.isNotEmpty) request.executionId = executionId;
|
||||
final pipelineYamlPath =
|
||||
(raw['pipeline_yaml_path'] ?? raw['pipelineYamlPath'] ?? '').toString();
|
||||
if (pipelineYamlPath.isNotEmpty) request.pipelineYamlPath = pipelineYamlPath;
|
||||
final pipelineYaml = (raw['pipeline_yaml'] ?? raw['pipelineYaml'] ?? '')
|
||||
.toString();
|
||||
if (pipelineYaml.isNotEmpty) request.pipelineYaml = pipelineYaml;
|
||||
final variables = raw['variables'];
|
||||
if (variables is Map) {
|
||||
variables.forEach((key, value) {
|
||||
request.variables[key.toString()] = value?.toString() ?? '';
|
||||
});
|
||||
}
|
||||
final commandTypes = raw['command_types'] ?? raw['commandTypes'];
|
||||
if (commandTypes is List) {
|
||||
request.commandTypes.addAll(commandTypes.map((e) => e.toString()));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -414,6 +414,14 @@ runtime:
|
|||
'job_id': 'job-123',
|
||||
'execution_id': 'exec-123',
|
||||
'state': 'running',
|
||||
'run_request': {
|
||||
'runner_id': 'agent-123',
|
||||
'job_id': 'job-123',
|
||||
'execution_id': 'exec-123',
|
||||
'pipeline_yaml': 'commands:\n - type: Shell',
|
||||
'variables': {'FLAVOR': 'release'},
|
||||
'command_types': ['Shell', 'Git'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else if (request.method == 'POST' &&
|
||||
|
|
@ -460,6 +468,13 @@ runtime:
|
|||
);
|
||||
expect(claim.accepted, isTrue);
|
||||
expect(claim.state, 'running');
|
||||
expect(claim.runRequest, isNotNull);
|
||||
expect(claim.runRequest!.jobId, 'job-123');
|
||||
expect(claim.runRequest!.executionId, 'exec-123');
|
||||
expect(claim.runRequest!.runnerId, 'agent-123');
|
||||
expect(claim.runRequest!.pipelineYaml, 'commands:\n - type: Shell');
|
||||
expect(claim.runRequest!.variables['FLAVOR'], 'release');
|
||||
expect(claim.runRequest!.commandTypes, ['Shell', 'Git']);
|
||||
|
||||
final result = BuildResult.success(
|
||||
stepEvents: [
|
||||
|
|
|
|||
|
|
@ -397,10 +397,19 @@ fi
|
|||
|
||||
final httpClient = http.Client();
|
||||
try {
|
||||
const inlineYaml = 'commands:\n - type: Shell';
|
||||
final createJobResponse = await httpClient.post(
|
||||
Uri.parse('http://$serverAddr/api/v1/jobs'),
|
||||
headers: {'content-type': 'application/json'},
|
||||
body: jsonEncode({'id': jobId, 'name': 'smoke build'}),
|
||||
body: jsonEncode({
|
||||
'id': jobId,
|
||||
'name': 'smoke build',
|
||||
'run_request': {
|
||||
'pipeline_yaml': inlineYaml,
|
||||
'variables': {'FLAVOR': 'release'},
|
||||
'command_types': ['Shell', 'Git'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
createJobResponse.statusCode,
|
||||
|
|
@ -414,6 +423,10 @@ fi
|
|||
);
|
||||
expect(claim.accepted, isTrue);
|
||||
expect(claim.state, 'running');
|
||||
expect(claim.runRequest, isNotNull);
|
||||
expect(claim.runRequest!.pipelineYaml, inlineYaml);
|
||||
expect(claim.runRequest!.jobId, jobId);
|
||||
expect(claim.runRequest!.executionId, executionId);
|
||||
|
||||
await jobs.appendLog(
|
||||
executionId: executionId,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,17 @@ var validTransitions = map[string][]string{
|
|||
StateCanceled: nil,
|
||||
}
|
||||
|
||||
// RunInput holds the remote run request payload stored on a job so that a
|
||||
// runner can receive it when it claims the job. It deliberately avoids any
|
||||
// protobuf dependency; conversion to/from otopb.RunRequest happens at the HTTP
|
||||
// boundary.
|
||||
type RunInput struct {
|
||||
PipelineYAMLPath string
|
||||
PipelineYAML string
|
||||
Variables map[string]string
|
||||
CommandTypes []string
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
ID string
|
||||
Name string
|
||||
|
|
@ -29,6 +40,7 @@ type Job struct {
|
|||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ExecutionID string
|
||||
RunInput *RunInput
|
||||
}
|
||||
|
||||
func (j *Job) TransitionTo(newState string) error {
|
||||
|
|
@ -96,7 +108,7 @@ func NewStore() *Store {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Store) CreateJob(id, name string) (*Job, error) {
|
||||
func (s *Store) CreateJob(id, name string, runInput *RunInput) (*Job, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
|
|
@ -111,9 +123,10 @@ func (s *Store) CreateJob(id, name string) (*Job, error) {
|
|||
State: StateQueued,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
RunInput: copyRunInput(runInput),
|
||||
}
|
||||
s.jobs[id] = job
|
||||
return job, nil
|
||||
return copyJob(job), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetJob(id string) (*Job, error) {
|
||||
|
|
@ -307,6 +320,25 @@ func copyJob(j *Job) *Job {
|
|||
return nil
|
||||
}
|
||||
cp := *j
|
||||
cp.RunInput = copyRunInput(j.RunInput)
|
||||
return &cp
|
||||
}
|
||||
|
||||
func copyRunInput(in *RunInput) *RunInput {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
cp := *in
|
||||
if in.Variables != nil {
|
||||
cp.Variables = make(map[string]string, len(in.Variables))
|
||||
for k, v := range in.Variables {
|
||||
cp.Variables[k] = v
|
||||
}
|
||||
}
|
||||
if in.CommandTypes != nil {
|
||||
cp.CommandTypes = make([]string, len(in.CommandTypes))
|
||||
copy(cp.CommandTypes, in.CommandTypes)
|
||||
}
|
||||
return &cp
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ func TestStoreCreatesJobAndExecution(t *testing.T) {
|
|||
|
||||
// 1. Create a job
|
||||
mockTime = time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)
|
||||
job, err := store.CreateJob("job-1", "build")
|
||||
job, err := store.CreateJob("job-1", "build", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateJob failed: %v", err)
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ func TestStoreCreatesJobAndExecution(t *testing.T) {
|
|||
}
|
||||
|
||||
// 2. Try duplicate job creation
|
||||
_, err = store.CreateJob("job-1", "duplicate")
|
||||
_, err = store.CreateJob("job-1", "duplicate", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for duplicate job")
|
||||
}
|
||||
|
|
@ -84,13 +84,66 @@ func TestStoreCreatesJobAndExecution(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStoreCreatesJobWithRunInputCopy(t *testing.T) {
|
||||
store := NewStore()
|
||||
|
||||
variables := map[string]string{"FLAVOR": "release"}
|
||||
commandTypes := []string{"Shell", "Git"}
|
||||
input := &RunInput{
|
||||
PipelineYAML: "commands:\n - type: Shell",
|
||||
Variables: variables,
|
||||
CommandTypes: commandTypes,
|
||||
}
|
||||
|
||||
job, err := store.CreateJob("job-1", "build", input)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateJob failed: %v", err)
|
||||
}
|
||||
if job.RunInput == nil || job.RunInput.PipelineYAML != "commands:\n - type: Shell" {
|
||||
t.Fatalf("job RunInput = %+v, want stored pipeline yaml", job.RunInput)
|
||||
}
|
||||
|
||||
// Mutating the caller-owned input must not leak into the store.
|
||||
variables["FLAVOR"] = "debug"
|
||||
commandTypes[0] = "Mutated"
|
||||
input.PipelineYAML = "mutated"
|
||||
|
||||
stored, err := store.GetJob("job-1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetJob failed: %v", err)
|
||||
}
|
||||
if stored.RunInput == nil {
|
||||
t.Fatal("stored RunInput is nil")
|
||||
}
|
||||
if stored.RunInput.PipelineYAML != "commands:\n - type: Shell" {
|
||||
t.Fatalf("stored PipelineYAML leaked mutation: %q", stored.RunInput.PipelineYAML)
|
||||
}
|
||||
if stored.RunInput.Variables["FLAVOR"] != "release" {
|
||||
t.Fatalf("stored Variables leaked mutation: %q", stored.RunInput.Variables["FLAVOR"])
|
||||
}
|
||||
if stored.RunInput.CommandTypes[0] != "Shell" {
|
||||
t.Fatalf("stored CommandTypes leaked mutation: %q", stored.RunInput.CommandTypes[0])
|
||||
}
|
||||
|
||||
// Mutating the returned copy must not leak back into the store either.
|
||||
stored.RunInput.Variables["FLAVOR"] = "leak"
|
||||
stored.RunInput.CommandTypes[0] = "Leak"
|
||||
reread, err := store.GetJob("job-1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetJob after mutation failed: %v", err)
|
||||
}
|
||||
if reread.RunInput.Variables["FLAVOR"] != "release" || reread.RunInput.CommandTypes[0] != "Shell" {
|
||||
t.Fatalf("GetJob did not return a deep copy of RunInput: %+v", reread.RunInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreAppendsLogsAndArtifacts(t *testing.T) {
|
||||
var mockTime time.Time
|
||||
store := NewStore()
|
||||
store.now = func() time.Time { return mockTime }
|
||||
|
||||
mockTime = time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)
|
||||
store.CreateJob("job-1", "build")
|
||||
store.CreateJob("job-1", "build", nil)
|
||||
|
||||
// Append log to non-existent execution
|
||||
err := store.AppendLog("exec-unknown", "line 1")
|
||||
|
|
@ -223,7 +276,7 @@ func TestStoreJobExecutionNotFound(t *testing.T) {
|
|||
|
||||
func TestStoreStateTransitions(t *testing.T) {
|
||||
store := NewStore()
|
||||
store.CreateJob("job-1", "build")
|
||||
store.CreateJob("job-1", "build", nil)
|
||||
|
||||
job, _ := store.GetJob("job-1")
|
||||
if job.State != StateQueued {
|
||||
|
|
@ -255,7 +308,7 @@ func TestStoreStateTransitions(t *testing.T) {
|
|||
}
|
||||
|
||||
// Reset: queued -> running -> failed
|
||||
store.CreateJob("job-2", "test")
|
||||
store.CreateJob("job-2", "test", nil)
|
||||
job2, _ := store.GetJob("job-2")
|
||||
err = job2.TransitionTo(StateRunning)
|
||||
if err != nil {
|
||||
|
|
@ -270,7 +323,7 @@ func TestStoreStateTransitions(t *testing.T) {
|
|||
}
|
||||
|
||||
// queued -> canceled
|
||||
store.CreateJob("job-3", "lint")
|
||||
store.CreateJob("job-3", "lint", nil)
|
||||
job3, _ := store.GetJob("job-3")
|
||||
err = job3.TransitionTo(StateCanceled)
|
||||
if err != nil {
|
||||
|
|
@ -281,7 +334,7 @@ func TestStoreStateTransitions(t *testing.T) {
|
|||
}
|
||||
|
||||
// queued -> failed (direct from queued)
|
||||
store.CreateJob("job-4", "deploy")
|
||||
store.CreateJob("job-4", "deploy", nil)
|
||||
job4, _ := store.GetJob("job-4")
|
||||
err = job4.TransitionTo(StateFailed)
|
||||
if err != nil {
|
||||
|
|
@ -294,7 +347,7 @@ func TestStoreStateTransitions(t *testing.T) {
|
|||
|
||||
func TestStoreExecutionStateTransitions(t *testing.T) {
|
||||
store := NewStore()
|
||||
store.CreateJob("job-1", "build")
|
||||
store.CreateJob("job-1", "build", nil)
|
||||
store.CreateExecution("job-1", "exec-1")
|
||||
|
||||
exec, _ := store.GetExecution("exec-1")
|
||||
|
|
@ -326,7 +379,7 @@ func TestStorePersistedJobTransition(t *testing.T) {
|
|||
store := NewStore()
|
||||
store.now = func() time.Time { return time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC) }
|
||||
|
||||
store.CreateJob("job-1", "build")
|
||||
store.CreateJob("job-1", "build", nil)
|
||||
|
||||
// queued -> running -> succeeded
|
||||
err := store.TransitionJob("job-1", StateRunning)
|
||||
|
|
@ -385,7 +438,7 @@ func TestStorePersistedExecutionTransition(t *testing.T) {
|
|||
store := NewStore()
|
||||
store.now = func() time.Time { return time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC) }
|
||||
|
||||
store.CreateJob("job-1", "build")
|
||||
store.CreateJob("job-1", "build", nil)
|
||||
store.CreateExecution("job-1", "exec-1")
|
||||
|
||||
// queued -> running -> failed
|
||||
|
|
@ -441,7 +494,7 @@ func TestStorePersistedExecutionTransition(t *testing.T) {
|
|||
}
|
||||
|
||||
// queued -> running -> succeeded
|
||||
store.CreateJob("job-2", "deploy")
|
||||
store.CreateJob("job-2", "deploy", nil)
|
||||
store.CreateExecution("job-2", "exec-2")
|
||||
err = store.TransitionExecution("exec-2", StateRunning)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -485,8 +485,9 @@ func handleRouter(store *cicdstate.Store, registry *runnerregistry.Registry) htt
|
|||
func handleCreateJob(store *cicdstate.Store) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
RunRequest *otopb.RunRequest `json:"run_request"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeResponse(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
|
|
@ -496,7 +497,7 @@ func handleCreateJob(store *cicdstate.Store) http.HandlerFunc {
|
|||
writeResponse(w, http.StatusBadRequest, map[string]string{"error": "id and name are required"})
|
||||
return
|
||||
}
|
||||
job, err := store.CreateJob(req.ID, req.Name)
|
||||
job, err := store.CreateJob(req.ID, req.Name, runRequestFromJSON(req.RunRequest))
|
||||
if err != nil {
|
||||
writeResponse(w, http.StatusConflict, map[string]string{"error": err.Error()})
|
||||
return
|
||||
|
|
@ -745,6 +746,23 @@ func handleRunnerClaimJob(store *cicdstate.Store, registry *runnerregistry.Regis
|
|||
})
|
||||
return
|
||||
}
|
||||
if job.RunInput == nil {
|
||||
writeResponse(w, http.StatusConflict, &otopb.JobClaimResponse{
|
||||
RunnerId: runnerID,
|
||||
JobId: jobID,
|
||||
ErrorMessage: "job has no run request",
|
||||
State: job.State,
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := validateRunInput(job.RunInput); err != nil {
|
||||
writeResponse(w, http.StatusBadRequest, &otopb.JobClaimResponse{
|
||||
RunnerId: runnerID,
|
||||
JobId: jobID,
|
||||
ErrorMessage: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if _, err := store.CreateExecution(jobID, execID); err != nil {
|
||||
writeResponse(w, http.StatusBadRequest, &otopb.JobClaimResponse{
|
||||
RunnerId: runnerID,
|
||||
|
|
@ -779,6 +797,7 @@ func handleRunnerClaimJob(store *cicdstate.Store, registry *runnerregistry.Regis
|
|||
JobId: jobID,
|
||||
ExecutionId: execID,
|
||||
State: cicdstate.StateRunning,
|
||||
RunRequest: runRequestToProto(job.RunInput, runnerID, jobID, execID),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -980,7 +999,7 @@ type jobResponse struct {
|
|||
}
|
||||
|
||||
func jobToJSON(j *cicdstate.Job) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
out := map[string]interface{}{
|
||||
"id": j.ID,
|
||||
"name": j.Name,
|
||||
"state": j.State,
|
||||
|
|
@ -988,6 +1007,74 @@ func jobToJSON(j *cicdstate.Job) map[string]interface{} {
|
|||
"updated_at": j.UpdatedAt.Format(time.RFC3339),
|
||||
"execution_id": j.ExecutionID,
|
||||
}
|
||||
if j.RunInput != nil {
|
||||
out["run_request"] = runInputToJSON(j.RunInput)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// validateRunInput enforces the remote claim contract: exactly one of
|
||||
// pipeline_yaml or pipeline_yaml_path must be provided.
|
||||
func validateRunInput(in *cicdstate.RunInput) error {
|
||||
hasYAML := strings.TrimSpace(in.PipelineYAML) != ""
|
||||
hasPath := strings.TrimSpace(in.PipelineYAMLPath) != ""
|
||||
if hasYAML == hasPath {
|
||||
return fmt.Errorf("run request must set exactly one of pipeline_yaml or pipeline_yaml_path")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runRequestFromJSON converts an incoming otopb.RunRequest decoded from the job
|
||||
// create body into a store-level RunInput. It returns nil when no run request
|
||||
// was supplied, keeping job creation backward compatible.
|
||||
func runRequestFromJSON(rr *otopb.RunRequest) *cicdstate.RunInput {
|
||||
if rr == nil {
|
||||
return nil
|
||||
}
|
||||
in := &cicdstate.RunInput{
|
||||
PipelineYAMLPath: rr.GetPipelineYamlPath(),
|
||||
PipelineYAML: rr.GetPipelineYaml(),
|
||||
CommandTypes: append([]string(nil), rr.GetCommandTypes()...),
|
||||
}
|
||||
if len(rr.GetVariables()) > 0 {
|
||||
in.Variables = make(map[string]string, len(rr.GetVariables()))
|
||||
for k, v := range rr.GetVariables() {
|
||||
in.Variables[k] = v
|
||||
}
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
// runRequestToProto builds the claim response RunRequest from the stored input,
|
||||
// filling in the runner/job/execution identifiers resolved at claim time.
|
||||
func runRequestToProto(in *cicdstate.RunInput, runnerID, jobID, execID string) *otopb.RunRequest {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
rr := &otopb.RunRequest{
|
||||
RunnerId: runnerID,
|
||||
JobId: jobID,
|
||||
ExecutionId: execID,
|
||||
PipelineYamlPath: in.PipelineYAMLPath,
|
||||
PipelineYaml: in.PipelineYAML,
|
||||
CommandTypes: append([]string(nil), in.CommandTypes...),
|
||||
}
|
||||
if len(in.Variables) > 0 {
|
||||
rr.Variables = make(map[string]string, len(in.Variables))
|
||||
for k, v := range in.Variables {
|
||||
rr.Variables[k] = v
|
||||
}
|
||||
}
|
||||
return rr
|
||||
}
|
||||
|
||||
func runInputToJSON(in *cicdstate.RunInput) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"pipeline_yaml_path": in.PipelineYAMLPath,
|
||||
"pipeline_yaml": in.PipelineYAML,
|
||||
"variables": in.Variables,
|
||||
"command_types": in.CommandTypes,
|
||||
}
|
||||
}
|
||||
|
||||
type execResponse struct {
|
||||
|
|
|
|||
|
|
@ -557,9 +557,151 @@ func TestHandleCreateJob(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleCreateJobStoresRunRequest(t *testing.T) {
|
||||
store := cicdstate.NewStore()
|
||||
|
||||
body := bytes.NewBufferString(`{
|
||||
"id":"job-run",
|
||||
"name":"build",
|
||||
"run_request":{
|
||||
"pipeline_yaml":"commands:\n - type: Shell",
|
||||
"variables":{"FLAVOR":"release"},
|
||||
"command_types":["Shell","Git"]
|
||||
}
|
||||
}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/jobs", body)
|
||||
rr := httptest.NewRecorder()
|
||||
handleRouter(store, nil)(rr, req)
|
||||
if rr.Code != http.StatusCreated {
|
||||
t.Fatalf("create status = %v, want %v; body=%s", rr.Code, http.StatusCreated, rr.Body.String())
|
||||
}
|
||||
|
||||
// Stored input must survive on the store.
|
||||
job, err := store.GetJob("job-run")
|
||||
if err != nil {
|
||||
t.Fatalf("GetJob failed: %v", err)
|
||||
}
|
||||
if job.RunInput == nil || job.RunInput.PipelineYAML != "commands:\n - type: Shell" {
|
||||
t.Fatalf("stored RunInput = %+v", job.RunInput)
|
||||
}
|
||||
if job.RunInput.Variables["FLAVOR"] != "release" {
|
||||
t.Fatalf("stored variables = %+v", job.RunInput.Variables)
|
||||
}
|
||||
|
||||
// GET /jobs/{id} must expose the stored input.
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/jobs/job-run", nil)
|
||||
rr = httptest.NewRecorder()
|
||||
handleRouter(store, nil)(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("get status = %v, want %v", rr.Code, http.StatusOK)
|
||||
}
|
||||
var res map[string]interface{}
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &res); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
runReq, ok := res["run_request"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("run_request missing in GET job: %v", res["run_request"])
|
||||
}
|
||||
if runReq["pipeline_yaml"] != "commands:\n - type: Shell" {
|
||||
t.Fatalf("GET run_request pipeline_yaml = %v", runReq["pipeline_yaml"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRunnerCicdClaimReturnsRunRequest(t *testing.T) {
|
||||
store := cicdstate.NewStore()
|
||||
registry := runnerregistry.New()
|
||||
registry.Register(&otopb.RegisterRunnerRequest{
|
||||
EnrollmentToken: "token-123",
|
||||
RunnerId: "runner-123",
|
||||
ProtocolVersion: "oto.runner.v1",
|
||||
Capability: &otopb.RunnerCapability{Name: "oto-runner", Version: "1.0.0"},
|
||||
})
|
||||
if _, err := store.CreateJob("job-1", "build", &cicdstate.RunInput{
|
||||
PipelineYAMLPath: "/pipelines/build.yaml",
|
||||
CommandTypes: []string{"Shell"},
|
||||
}); err != nil {
|
||||
t.Fatalf("CreateJob failed: %v", err)
|
||||
}
|
||||
|
||||
claimBody := bytes.NewBufferString(`{
|
||||
"runner_id":"runner-123",
|
||||
"job_id":"job-1",
|
||||
"execution_id":"exec-1"
|
||||
}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/runners/runner-123/jobs/claim", claimBody)
|
||||
rr := httptest.NewRecorder()
|
||||
handleRouter(store, registry)(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("claim status = %v, want %v; body=%s", rr.Code, http.StatusOK, rr.Body.String())
|
||||
}
|
||||
var claim otopb.JobClaimResponse
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &claim); err != nil {
|
||||
t.Fatalf("decode claim response: %v", err)
|
||||
}
|
||||
if claim.GetRunRequest() == nil {
|
||||
t.Fatal("claim run_request is nil")
|
||||
}
|
||||
if claim.GetRunRequest().GetPipelineYamlPath() != "/pipelines/build.yaml" {
|
||||
t.Fatalf("claim run_request pipeline_yaml_path = %q", claim.GetRunRequest().GetPipelineYamlPath())
|
||||
}
|
||||
if claim.GetRunRequest().GetRunnerId() != "runner-123" ||
|
||||
claim.GetRunRequest().GetJobId() != "job-1" ||
|
||||
claim.GetRunRequest().GetExecutionId() != "exec-1" {
|
||||
t.Fatalf("claim run_request ids = %+v", claim.GetRunRequest())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRunnerCicdRejectsInvalidRunInput(t *testing.T) {
|
||||
store := cicdstate.NewStore()
|
||||
registry := runnerregistry.New()
|
||||
registry.Register(&otopb.RegisterRunnerRequest{
|
||||
EnrollmentToken: "token-123",
|
||||
RunnerId: "runner-123",
|
||||
ProtocolVersion: "oto.runner.v1",
|
||||
Capability: &otopb.RunnerCapability{Name: "oto-runner", Version: "1.0.0"},
|
||||
})
|
||||
|
||||
// Job with no run request → claim is rejected with 409.
|
||||
if _, err := store.CreateJob("job-missing", "build", nil); err != nil {
|
||||
t.Fatalf("CreateJob failed: %v", err)
|
||||
}
|
||||
missingBody := bytes.NewBufferString(`{"runner_id":"runner-123","job_id":"job-missing","execution_id":"exec-1"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/runners/runner-123/jobs/claim", missingBody)
|
||||
rr := httptest.NewRecorder()
|
||||
handleRouter(store, registry)(rr, req)
|
||||
if rr.Code != http.StatusConflict {
|
||||
t.Fatalf("missing run input claim status = %v, want %v; body=%s", rr.Code, http.StatusConflict, rr.Body.String())
|
||||
}
|
||||
|
||||
// Job with both pipeline_yaml and pipeline_yaml_path → claim is rejected with 400.
|
||||
if _, err := store.CreateJob("job-both", "build", &cicdstate.RunInput{
|
||||
PipelineYAML: "commands: []",
|
||||
PipelineYAMLPath: "/pipelines/build.yaml",
|
||||
}); err != nil {
|
||||
t.Fatalf("CreateJob failed: %v", err)
|
||||
}
|
||||
bothBody := bytes.NewBufferString(`{"runner_id":"runner-123","job_id":"job-both","execution_id":"exec-2"}`)
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/runners/runner-123/jobs/claim", bothBody)
|
||||
rr = httptest.NewRecorder()
|
||||
handleRouter(store, registry)(rr, req)
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Fatalf("invalid run input claim status = %v, want %v; body=%s", rr.Code, http.StatusBadRequest, rr.Body.String())
|
||||
}
|
||||
|
||||
// The rejected job must remain queued (no state mutation / execution created).
|
||||
job, err := store.GetJob("job-both")
|
||||
if err != nil {
|
||||
t.Fatalf("GetJob failed: %v", err)
|
||||
}
|
||||
if job.State != cicdstate.StateQueued || job.ExecutionID != "" {
|
||||
t.Fatalf("rejected job mutated state = %+v", job)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCreateExecution(t *testing.T) {
|
||||
store := cicdstate.NewStore()
|
||||
store.CreateJob("job-1", "build")
|
||||
store.CreateJob("job-1", "build", nil)
|
||||
|
||||
body := bytes.NewBufferString(`{"id":"exec-1"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/jobs/job-1/executions", body)
|
||||
|
|
@ -611,7 +753,7 @@ func TestHandleCreateExecution(t *testing.T) {
|
|||
|
||||
func TestHandleExecutionLogsAndArtifacts(t *testing.T) {
|
||||
store := cicdstate.NewStore()
|
||||
store.CreateJob("job-1", "build")
|
||||
store.CreateJob("job-1", "build", nil)
|
||||
store.CreateExecution("job-1", "exec-1")
|
||||
|
||||
// Append log
|
||||
|
|
@ -762,7 +904,11 @@ func TestHandleRunnerCicdClaimReportLogsAndArtifacts(t *testing.T) {
|
|||
Version: "1.0.0",
|
||||
},
|
||||
})
|
||||
if _, err := store.CreateJob("job-1", "build"); err != nil {
|
||||
if _, err := store.CreateJob("job-1", "build", &cicdstate.RunInput{
|
||||
PipelineYAML: "commands:\n - type: Shell",
|
||||
Variables: map[string]string{"FLAVOR": "release"},
|
||||
CommandTypes: []string{"Shell", "Git"},
|
||||
}); err != nil {
|
||||
t.Fatalf("CreateJob failed: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -784,6 +930,12 @@ func TestHandleRunnerCicdClaimReportLogsAndArtifacts(t *testing.T) {
|
|||
if !claim.GetAccepted() || claim.GetState() != cicdstate.StateRunning {
|
||||
t.Fatalf("claim response = %+v, want accepted running", claim)
|
||||
}
|
||||
if claim.GetRunRequest().GetPipelineYaml() != "commands:\n - type: Shell" {
|
||||
t.Fatalf("claim run_request pipeline yaml = %q", claim.GetRunRequest().GetPipelineYaml())
|
||||
}
|
||||
if claim.GetRunRequest().GetJobId() != "job-1" || claim.GetRunRequest().GetExecutionId() != "exec-1" || claim.GetRunRequest().GetRunnerId() != "runner-123" {
|
||||
t.Fatalf("claim run_request ids = %+v", claim.GetRunRequest())
|
||||
}
|
||||
|
||||
job, err := store.GetJob("job-1")
|
||||
if err != nil {
|
||||
|
|
@ -866,7 +1018,7 @@ func TestHandleRunnerCicdClaimReportLogsAndArtifacts(t *testing.T) {
|
|||
|
||||
func TestHandleRunnerCicdRejectsUnknownRunner(t *testing.T) {
|
||||
store := cicdstate.NewStore()
|
||||
if _, err := store.CreateJob("job-1", "build"); err != nil {
|
||||
if _, err := store.CreateJob("job-1", "build", nil); err != nil {
|
||||
t.Fatalf("CreateJob failed: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue