운영 릴리즈 버전관리 자동화#611
Conversation
Co-authored-by: OmX <omx@oh-my-codex.dev>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (12)
📝 WalkthroughWalkthrough프로덕션 배포 자동화를 위한 완전한 버전 관리 및 릴리즈 운영 체계를 구축합니다. 릴리즈 의도 해석, 레코드 생성/검증, 배포 워크플로, 백엔드 기록, 롤백 절차를 포함하는 자동화 파이프라인과 함께 운영 규칙, 에이전트 스킬, 체크리스트를 정의합니다. Changes프로덕션 릴리즈 자동화 워크플로
운영 규칙 및 설명서
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-authored-by: OmX <omx@oh-my-codex.dev>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
ops/version-management.md (1)
121-130: ⚡ Quick win라벨 전용 규칙과 PR body fallback 규칙을 이 문서에 함께 명시해 계약을 단일화해 주세요.
현재 문구만 보면 운영 의도가 “라벨만 허용”으로 해석될 수 있는데,
ops/release-intent.md는 라벨 미가용 시 body fallback을 허용합니다. 두 문서를 동시에 보는 사람에게 계약이 갈릴 수 있어요.제안 diff
## Main-branch release intent Production versioning is derived from PR intent, not from per-release environment variables. Allowed release intents are exactly: @@ - `release:major` `hotfix` labels and `-hotfix.N` image tags are not used. + +If labels are unavailable in a specific workflow context, the PR body `release: patch|minor|major` +fallback defined in `ops/release-intent.md` may be used. If both are present and disagree, fail.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ops/version-management.md` around lines 121 - 130, 문서 'Main-branch release intent' 섹션(현재 ops/version-management.md)에 라벨 우선 규칙과 라벨 미사용 시 PR body fallback 규칙을 명확히 통합해 단일 계약으로 서술하세요: 허용된 intent 목록(`release:patch`, `release:minor`, `release:major`)을 유지하되, 우선순위 규칙(라벨가 있으면 라벨을 사용하고, 라벨이 없을 경우 ops/release-intent.md에 정의된 PR body 값을 fallback으로 사용)을 명시하고 예외/예시 한두 줄을 추가해 모순이 없도록 정리합니다.ops/deploy-checklist.md (1)
8-8: ⚡ Quick win릴리즈 인텐트 표기 값을 명시적으로 고정해 주세요.
Line 8의
release: ...표기는 허용 값이 열려 있어 운영자가patch|minor|major외 값을 넣을 여지를 남깁니다. 체크리스트에도 허용 문자열을 정확히 고정하는 편이 안전합니다.✍️ 제안 수정안
-- Confirm the PR has exactly one release intent: `release:patch`, `release:minor`, `release:major`, or a `release: ...` line in the PR body. +- Confirm the PR has exactly one release intent: `release:patch`, `release:minor`, `release:major`, or exactly one PR body line of `release:patch|minor|major`.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ops/deploy-checklist.md` at line 8, Change the ambiguous "release: ..." checklist item to enumerate the exact allowed values and require exactly one of them; replace the text containing `release: ...` with a fixed list like "`release:patch`, `release:minor`, or `release:major`" and update the sentence that currently reads "Confirm the PR has exactly one release intent: `release:patch`, `release:minor`, `release:major`, or a `release: ...` line in the PR body." so it only accepts the three explicit tokens (`release:patch`, `release:minor`, `release:major`) and enforces exactly one occurrence.scripts/release/validate-release-record.mjs (4)
61-67: 💤 Low value파일명 검증에서
basename사용을 권장합니다.현재
file.endsWith(expectedName)검증은 작동하지만,path.basename(file) === expectedName방식이 의도를 더 명확하게 표현하며, 다른 경로에 동일 이름 파일이 있을 때 발생할 수 있는 오탐을 방지합니다.현재 Line 145의 필터링(
/prod-\d{8}-\d{4}\.yaml$/)이 이미 파일명을 제한하므로 큰 문제는 아니지만, 더 명시적인 검증이 가능합니다.♻️ basename 사용 개선안
+import { basename, join } from 'node:path'; + const validateFile = (file) => { const content = readFileSync(file, 'utf8'); const releaseId = parseScalar(content, 'release_id'); const expectedName = `${releaseId}.yaml`; if (!RELEASE_ID.test(releaseId)) fail(file, `invalid release_id: ${releaseId}`); - if (!file.endsWith(expectedName)) { + if (basename(file) !== expectedName) { fail(file, `filename must match release_id (${expectedName})`); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/release/validate-release-record.mjs` around lines 61 - 67, The filename check using file.endsWith(expectedName) can produce false positives for paths; change it to compare the basename explicitly by using path.basename(file) === expectedName. Locate the block that computes expectedName and validates RELEASE_ID.test(releaseId) and replace the endsWith check with a basename equality check (use the existing expectedName variable and the file variable), keeping the same fail(file, ...) invocation when the basename does not match.
143-160: 💤 Low value각 파일을 두 번 읽는 것을 개선할 수 있습니다.
Line 148의
validateFile과 Line 149의readFileSync가 동일한 파일을 중복으로 읽습니다. 파일 수가 적다면 성능상 큰 문제는 아니지만,validateFile이 검증 결과와 함께 파싱된 데이터(또는 content)를 반환하도록 리팩토링하면 파일 I/O를 줄일 수 있습니다.현재 구조는 명확하고 작동하므로 선택적 개선 사항입니다.
♻️ 이중 읽기 제거 개선안
-const validateFile = (file) => { +const validateFile = (file, content) => { - const content = readFileSync(file, 'utf8'); const releaseId = parseScalar(content, 'release_id'); // ... rest of validation + return content; }; const files = targets .flatMap(listYamlFiles) .filter((file) => /prod-\d{8}-\d{4}\.yaml$/.test(file)); const backendDeployIds = new Map(); for (const file of files) { - validateFile(file); - const content = readFileSync(file, 'utf8'); + const content = readFileSync(file, 'utf8'); + validateFile(file, content); const backendDeployId = parseScalar(content, 'metadata.backend_deploy_id');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/release/validate-release-record.mjs` around lines 143 - 160, The code currently reads each file twice via validateFile(file) and then readFileSync(file) before parseScalar; refactor validateFile to return the file content or parsed data so you can reuse it instead of calling readFileSync again: modify validateFile to return the validated content (or the parsed YAML/metadata), then replace the readFileSync + parseScalar call in the loop to use the returned content (or parsed object) when extracting metadata.backend_deploy_id; keep existing logic around backendDeployIds, parseScalar usage (or adapt to the returned parsed object), and preserve the fail(...) behavior when duplicates are found.
17-23: ⚡ Quick win
listYamlFiles가 재귀 탐색을 지원하지 않습니다.현재 함수는 디렉터리의 직접 자식 파일만 수집하며, 중첩된 하위 디렉터리는 처리하지 않습니다. 반면 워크플로우(
.github/workflows/release-record-check.yml:6)는releases/**/*.yaml패턴으로 중첩 디렉터리 변경도 감지합니다.릴리즈 레코드가
releases/직속에만 저장된다면 현재 구현으로 충분하지만, 워크플로우 경로 패턴과 스크립트 처리 능력 간 불일치가 있습니다.♻️ 재귀 탐색을 추가하는 개선안
const listYamlFiles = (target) => { if (!existsSync(target)) return []; if (statSync(target).isFile()) return [target]; - return readdirSync(target) - .filter((file) => file.endsWith('.yaml')) - .map((file) => join(target, file)); + const entries = readdirSync(target, { withFileTypes: true }); + return entries.flatMap((entry) => { + const fullPath = join(target, entry.name); + if (entry.isDirectory()) return listYamlFiles(fullPath); + return entry.name.endsWith('.yaml') ? [fullPath] : []; + }); };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/release/validate-release-record.mjs` around lines 17 - 23, The listYamlFiles function currently only collects .yaml files at the top-level of the target directory; update listYamlFiles to perform a recursive directory walk so it collects .yaml files from nested subdirectories as well (while preserving the existing behavior when target is a file or missing). Implement this by iterating directory entries (e.g., readdirSync with Dirent or using statSync on each entry) and when an entry is a directory recursively call listYamlFiles (or push results from a helper walk) and when an entry is a file with .yaml extension add its joined path; keep using existsSync/statSync to short-circuit non-existent or file targets and ensure returned paths are full/joined using join().
25-42: ⚡ Quick win정규식 기반 YAML 파서의 한계를 고려하세요.
현재
parseScalar는 단순한key: value구조만 파싱하며, 멀티라인 값, 배열, 주석, YAML 특수 문자 등을 처리하지 못합니다. 릴리즈 레코드가 단순한 스칼라 값만 사용한다면 작동하지만, 향후 확장성과 견고성을 위해js-yaml같은 검증된 라이브러리 사용을 권장합니다.현재 구조가 단순하고 추가 의존성을 피하려는 의도라면 현재 구현도 충분합니다.
♻️ js-yaml 사용 예시
+import yaml from 'js-yaml'; -const parseScalar = (content, path) => { - const lines = content.split(/\r?\n/); - const stack = []; - for (const raw of lines) { - const match = raw.match(/^(\s*)([A-Za-z0-9_]+):\s*(.*)$/); - if (!match) continue; - const indent = match[1].length; - const key = match[2]; - const value = match[3].trim(); - while (stack.length && stack[stack.length - 1].indent >= indent) - stack.pop(); - stack.push({ indent, key }); - if (stack.map((item) => item.key).join('.') === path) { - return value.replace(/^['"]|['"]$/g, ''); - } - } - return ''; +const parseScalar = (content, path) => { + const doc = yaml.load(content); + const keys = path.split('.'); + let value = doc; + for (const key of keys) { + value = value?.[key]; + } + return value ? String(value) : ''; };그리고
package.json에 의존성 추가:"dependencies": { "js-yaml": "^4.1.0" }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/release/validate-release-record.mjs` around lines 25 - 42, The current parseScalar function uses a fragile regex-based YAML parser that fails for multiline values, arrays, comments and other YAML features; replace its body to parse the content with a proper YAML library (e.g., install and import js-yaml and use YAML.load or safeLoad), then traverse the resulting object by splitting the dot-path and returning the scalar (or empty string if missing). Update parseScalar to call js-yaml's loader, handle parse errors by returning '' or rethrowing with context, and ensure the symbol parseScalar remains the entry point for callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/record-backend-prod-release.yml:
- Around line 24-28: 현재 Checkout step ("Checkout code" using
actions/checkout@v4) can pull the default branch for the event; explicitly pin
the branch to main by adding with: ref: main (keeping fetch-depth: 0) so the
workflow always checks out the main branch for releases; update the checkout
step in the workflow to include ref: main to enforce main-branch deployments per
the ZERO-ONE rule.
In `@ops/backend-release-dispatch.md`:
- Around line 7-13: The example trigger payload for event_type
"backend-prod-deployed" currently shows an empty client_payload which violates
the document's Required payload/fields rules; update the example JSON so
client_payload includes the actual required fields (e.g., service, version,
environment, deploy_id or whatever the "Required payload/fields" section
mandates) and show sample values, ensuring consistency with the document's
Required payload/fields schema; apply the same correction to the other example
occurrences referenced in the document.
In `@ops/rollback.md`:
- Line 27: 현재 문서의 "follow-up release/incident note" 요구만으로는 롤백 기록이 releases 디렉터리에
남지 않을 수 있으므로, ops/rollback.md의 해당 문구(“follow-up release/incident note”)를 수정해 롤백
결과를 반드시 releases/prod-*.yaml 패턴으로 기록하도록 명시하세요; 즉 문장에 최소 요구사항으로 "record rollback
outcome as a releases/prod-*.yaml release note"를 추가하고 예시 파일명 형식과 필수 필드(예:
timestamp, operator, reason, reverted_commit)도 간단히 기술하여 자동화가 SSOT로 참조할 수 있게 하세요.
---
Nitpick comments:
In `@ops/deploy-checklist.md`:
- Line 8: Change the ambiguous "release: ..." checklist item to enumerate the
exact allowed values and require exactly one of them; replace the text
containing `release: ...` with a fixed list like "`release:patch`,
`release:minor`, or `release:major`" and update the sentence that currently
reads "Confirm the PR has exactly one release intent: `release:patch`,
`release:minor`, `release:major`, or a `release: ...` line in the PR body." so
it only accepts the three explicit tokens (`release:patch`, `release:minor`,
`release:major`) and enforces exactly one occurrence.
In `@ops/version-management.md`:
- Around line 121-130: 문서 'Main-branch release intent' 섹션(현재
ops/version-management.md)에 라벨 우선 규칙과 라벨 미사용 시 PR body fallback 규칙을 명확히 통합해 단일
계약으로 서술하세요: 허용된 intent 목록(`release:patch`, `release:minor`, `release:major`)을
유지하되, 우선순위 규칙(라벨가 있으면 라벨을 사용하고, 라벨이 없을 경우 ops/release-intent.md에 정의된 PR body 값을
fallback으로 사용)을 명시하고 예외/예시 한두 줄을 추가해 모순이 없도록 정리합니다.
In `@scripts/release/validate-release-record.mjs`:
- Around line 61-67: The filename check using file.endsWith(expectedName) can
produce false positives for paths; change it to compare the basename explicitly
by using path.basename(file) === expectedName. Locate the block that computes
expectedName and validates RELEASE_ID.test(releaseId) and replace the endsWith
check with a basename equality check (use the existing expectedName variable and
the file variable), keeping the same fail(file, ...) invocation when the
basename does not match.
- Around line 143-160: The code currently reads each file twice via
validateFile(file) and then readFileSync(file) before parseScalar; refactor
validateFile to return the file content or parsed data so you can reuse it
instead of calling readFileSync again: modify validateFile to return the
validated content (or the parsed YAML/metadata), then replace the readFileSync +
parseScalar call in the loop to use the returned content (or parsed object) when
extracting metadata.backend_deploy_id; keep existing logic around
backendDeployIds, parseScalar usage (or adapt to the returned parsed object),
and preserve the fail(...) behavior when duplicates are found.
- Around line 17-23: The listYamlFiles function currently only collects .yaml
files at the top-level of the target directory; update listYamlFiles to perform
a recursive directory walk so it collects .yaml files from nested subdirectories
as well (while preserving the existing behavior when target is a file or
missing). Implement this by iterating directory entries (e.g., readdirSync with
Dirent or using statSync on each entry) and when an entry is a directory
recursively call listYamlFiles (or push results from a helper walk) and when an
entry is a file with .yaml extension add its joined path; keep using
existsSync/statSync to short-circuit non-existent or file targets and ensure
returned paths are full/joined using join().
- Around line 25-42: The current parseScalar function uses a fragile regex-based
YAML parser that fails for multiline values, arrays, comments and other YAML
features; replace its body to parse the content with a proper YAML library
(e.g., install and import js-yaml and use YAML.load or safeLoad), then traverse
the resulting object by splitting the dot-path and returning the scalar (or
empty string if missing). Update parseScalar to call js-yaml's loader, handle
parse errors by returning '' or rethrowing with context, and ensure the symbol
parseScalar remains the entry point for callers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ac4e7ff4-f619-42b8-a45e-b6bd0026c098
📒 Files selected for processing (20)
.claude/rules/version-management-frontend.md.claude/skills/zeroone-version-management/SKILL.md.codex/skills/zeroone-version-management/SKILL.md.github/workflows/deploy-prod.yml.github/workflows/record-backend-prod-release.yml.github/workflows/release-record-check.ymlAGENTS.mdCLAUDE.mdops/agent-skills/zeroone-version-management.mdops/backend-release-dispatch.mdops/deploy-checklist.mdops/release-intent.mdops/rollback.mdops/version-management.mdpackage.jsonreleases/.gitkeepscripts/release/generate-backend-prod-release-record.mjsscripts/release/generate-prod-release-record.mjsscripts/release/resolve-prod-release-intent.mjsscripts/release/validate-release-record.mjs
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
There was a problem hiding this comment.
체크아웃 브랜치를 main으로 명시 고정하세요.
현재는 repository_dispatch 실행 시점의 기본 브랜치 기준으로 체크아웃될 수 있어, 커밋/푸시 대상(main)과 작업 베이스가 어긋날 위험이 있습니다.
수정 제안
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
+ ref: mainBased on learnings: Main-branch production deployments must follow ops/version-management.md (ZERO-ONE Version Management Rule - Frontend Repository).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/record-backend-prod-release.yml around lines 24 - 28, 현재
Checkout step ("Checkout code" using actions/checkout@v4) can pull the default
branch for the event; explicitly pin the branch to main by adding with: ref:
main (keeping fetch-depth: 0) so the workflow always checks out the main branch
for releases; update the checkout step in the workflow to include ref: main to
enforce main-branch deployments per the ZERO-ONE rule.
| Backend production deploy success must call the frontend repository with `repository_dispatch` or an equivalent API trigger. | ||
|
|
||
| ```json | ||
| { | ||
| "event_type": "backend-prod-deployed", | ||
| "client_payload": {} | ||
| } |
There was a problem hiding this comment.
트리거 예시의 client_payload가 필수 스키마와 충돌합니다.
Line 9-13 예시를 그대로 따르면 빈 payload가 전송되어, 같은 문서의 Required payload/fields 규칙을 즉시 위반합니다. 트리거 예시는 최소한 “필수 필드를 포함한 payload” 형태로 맞춰야 계약 오해를 막을 수 있습니다.
제안 diff
{
"event_type": "backend-prod-deployed",
- "client_payload": {}
+ "client_payload": {
+ "release_id": "prod-YYYYMMDD-HHmm",
+ "env": "prod",
+ "backend": {
+ "image": "zeroone-backend:vMAJOR.MINOR.PATCH-shortCommit",
+ "commit": "shortCommit",
+ "version": "vMAJOR.MINOR.PATCH",
+ "changed": true
+ },
+ "database": {
+ "changed": false,
+ "migration_version": "none",
+ "migration_files": []
+ },
+ "rollback": {
+ "backend": "zeroone-backend:vMAJOR.MINOR.PATCH-shortCommit"
+ },
+ "metadata": {
+ "release_intent": "patch",
+ "bootstrap_mode": false,
+ "backend_deploy_id": "backend-prod-unique-id"
+ }
+ }
}Also applies to: 22-55
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ops/backend-release-dispatch.md` around lines 7 - 13, The example trigger
payload for event_type "backend-prod-deployed" currently shows an empty
client_payload which violates the document's Required payload/fields rules;
update the example JSON so client_payload includes the actual required fields
(e.g., service, version, environment, deploy_id or whatever the "Required
payload/fields" section mandates) and show sample values, ensuring consistency
with the document's Required payload/fields schema; apply the same correction to
the other example occurrences referenced in the document.
| 3. Deploy the fixed frontend/backend images. | ||
| 4. Run backend health check. | ||
| 5. Run frontend smoke/E2E check. | ||
| 6. Record the rollback outcome in a follow-up release/incident note. |
There was a problem hiding this comment.
롤백 결과 기록 위치를 releases/로 강제해 주세요.
Line 27이 “follow-up release/incident note”만 요구하면, 실제 롤백 이력이 releases/에 남지 않아 다음 배포/롤백 자동화가 참조하는 SSOT가 깨질 수 있습니다. 최소 요구사항에 releases/prod-*.yaml 기록을 명시적으로 포함해야 합니다.
🧭 제안 수정안
-6. Record the rollback outcome in a follow-up release/incident note.
+6. Record the rollback as a new `releases/prod-YYYYMMDD-HHmm.yaml` entry (with rollback context), and optionally mirror details in an incident note.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 6. Record the rollback outcome in a follow-up release/incident note. | |
| 6. Record the rollback as a new `releases/prod-YYYYMMDD-HHmm.yaml` entry (with rollback context), and optionally mirror details in an incident note. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ops/rollback.md` at line 27, 현재 문서의 "follow-up release/incident note" 요구만으로는
롤백 기록이 releases 디렉터리에 남지 않을 수 있으므로, ops/rollback.md의 해당 문구(“follow-up
release/incident note”)를 수정해 롤백 결과를 반드시 releases/prod-*.yaml 패턴으로 기록하도록 명시하세요; 즉
문장에 최소 요구사항으로 "record rollback outcome as a releases/prod-*.yaml release note"를
추가하고 예시 파일명 형식과 필수 필드(예: timestamp, operator, reason, reverted_commit)도 간단히 기술하여
자동화가 SSOT로 참조할 수 있게 하세요.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Summary
main운영 배포 전용 version-management rule/docs/agent skill SSOT를 추가했습니다.release:patch|minor|major)로 frontend version/image를 계산하고, 최신releases/backend/DB 상태와 합쳐 최종 release record를 남기게 했습니다.repository_dispatch(backend-prod-deployed) payload를 보내면, frontend repo가FE(current)+BE(new)atomic 조합으로releases/prod-*.yaml을 기록하게 했습니다.Release contract
release:patch,release:minor,release:major만입니다.hotfixtag/label은 사용하지 않습니다.zeroone-frontend:vMAJOR.MINOR.PATCH-shortCommit,zeroone-backend:vMAJOR.MINOR.PATCH-shortCommit형식만 허용합니다.prod/latest-prod는 배포 포인터일 뿐 rollback 기준으로 쓰면 validation 실패합니다.metadata.backend_deploy_id중복을 거부하고, payload/current state가 불명확하면 release record를 쓰지 않고 실패합니다.Verification
actionlint,node --check, release-record validation, backend dispatch smoke, duplicate deploy-id smoke, empty previous-release fail smoke,git diff --checkyarn typecheckyarn lintpassed with existing warnings onlynext buildpassed (sitemap local backend127.0.0.1:8080warning only)