🔧 Split SEO metadata write-back from gates - #24
Conversation
sim-so
left a comment
There was a problem hiding this comment.
리뷰 요약
전체적으로 머지 가능한 상태입니다. gate와 metadata write-back을 분리한 핵심 의도가 코드로 잘 구현되어 있고, 테스트와 계약 문서(docs/seo-metadata-module-io.md)도 꼼꼼합니다. 다만 1번 항목은 머지 전에 한 번 확인해 주세요.
✅ 의도대로 동작 확인된 부분
metadata-suggestion.json은 블로그 repo에 커밋되지 않음 — push 단계가git add -- "${{ inputs.file_path }}"로_posts/*.md만 stage하고, suggestion JSON은target/체크아웃 밖(results/)에 생성되어 애초에 커밋 대상이 될 수 없습니다.- 실제 SEO 적용은 frontmatter 변경으로만 남음 —
metadata.apply()가 frontmatter/body를 분리한 뒤 body는 그대로 두고 title/description/categories/image/canonical/hreflang만 반영합니다. 번역 본문 fidelity를 해치지 않습니다. - advisory가 gate로 번지지 않음 — metadata suggestion의
ERROR/PARTIAL/SKIPPED가 SEOconclusion: fail로 이어지던 로직이 제거되어, "advisory는 report-only" 정책과 일치합니다.
🔴 머지 전 확인 필요 (1번)
자동 apply 후보의 비결정성 → 재발화(re-trigger) 루프 / Discord ready 미발화 위험
notify_ready가needs.metadata_apply.outputs.changed != 'true'조건으로 바뀌어, metadata를 커밋한 run에서는 Discord ready를 보내지 않고 후속 run에서 보내는 설계입니다.- 이 설계가 성립하려면 후속 run의 apply가 반드시
changed == false(idempotent) 여야 합니다. - 그런데
metadata_apply잡은OPENAI_API_KEY를 넘기고,metadata_suggestion.py는--openai-required와 무관하게OPENAI_API_KEY만 있으면 LLM으로 title/description 후보를 생성합니다. → 재발화 run에서 후보가 미세하게 달라지면changed=true가 반복되어 커밋 루프 + Discord ready 영구 미발화 가능성이 있습니다. - 참고: 자동 잡은
--manifest를 넘기지 않아 대개PARTIAL로 SKIP될 가능성이 높지만(실제로는 거의 안 터질 수 있음), target repo가 정책 매니페스트를 실어READY에 도달하면 위험이 살아납니다. - 권장 확인/보강:
- 실제
pull_request트리거에서 자동 재발화 경로가 idempotent한지 확인 (E2E는workflow_dispatch라 이 경로가 검증되지 않았을 수 있음) - apply/verify 경로에서 후보를 결정적으로 만들기 (예: 이 잡에서
OPENAI_API_KEY미주입 → deterministic fallback), 또는 - "현재 frontmatter가 후보와 이미 동일하면 skip" short-circuit, 또는 재발화 횟수 상한
- 실제
🟡 개선 제안 (머지 차단 아님)
metadata_apply잡 실패 시 전체 lifecycle이 red — suggestion 파일이 없으면FileNotFoundError로 잡이 실패해, gate를 다 통과해도 워크플로가 실패로 표시될 수 있습니다. 파일 부재를 graceful하게 SKIP(changed=false) 처리하면 안전합니다.- 후속 스키마 — 앞으로 논의한
advisory: { frontmatter: [...], content: [...] }+auto_apply_candidate형태는 이 PR 범위가 아니며, 추가 시metadata_suggestion.pyoutput shape +publish_pr_comment._metadata_summary+apply_metadata_suggestion.py를 함께 바꾸면 됩니다. (후속 이슈 권장)
👍 좋았던 점
SEO_OPENAI_REQUIRED→SEO_RUBRIC_OPENAI_REQUIRED/SEO_METADATA_OPENAI_REQUIRED분리가 깔끔하고 하위호환까지 챙김- failed-safe(검증 실패 시 push 안 함)가 스텝 순서상 올바르게 보장됨
- 테스트 커버리지(intent 분기, safe-field 제한, policy override, PARTIAL/READY)가 촘촘함
| name: HF Agent / Request Human Merge | ||
| needs: finalize | ||
| needs: [finalize, metadata_apply] | ||
| if: ${{ always() && needs.finalize.result == 'success' && needs.metadata_apply.outputs.changed != 'true' }} |
There was a problem hiding this comment.
[머지 전 확인 필요] 이 조건은 "metadata 커밋이 발생한 run에서는 Discord ready를 보내지 않고, bot push로 재발화된 후속 run에서 보낸다"는 설계에 의존합니다.
- 성립 전제: 후속 run의 apply가 반드시
changed == false(idempotent). - 위험: 후보가 LLM으로 생성되어 재발화 run마다 title/description이 달라지면
changed=true가 반복 → 커밋 루프 + Discord ready 영구 미발화 가능성. - 확인 요청: 실제
pull_request트리거에서 재발화 경로가 idempotent한지 (E2E는workflow_dispatch라 이 경로 미검증 가능성). 필요하면 "frontmatter가 후보와 동일하면 skip" short-circuit 또는 재발화 상한 추가 권장.
| changed: ${{ steps.apply.outputs.changed }} | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.KREW_BOT_TOKEN }} | ||
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} |
There was a problem hiding this comment.
위 재발화 이슈의 근본 원인 지점입니다. 이 잡이 OPENAI_API_KEY를 주입하면 metadata_suggestion.py는 --openai-required 없이도 LLM으로 후보(title/description)를 생성합니다.
자동 apply의 idempotency를 보장하려면 이 경로에서 후보를 결정적으로 만드는 방안을 검토해 주세요 (예: 이 잡에서 OPENAI_API_KEY를 넘기지 않고 deterministic fallback 사용).
| - name: Apply approved metadata suggestion | ||
| id: apply | ||
| run: | | ||
| python -m hf_agent.apply_metadata_suggestion \ |
There was a problem hiding this comment.
[개선 제안] results/metadata-suggestion.json이 없으면 apply_metadata_suggestion이 FileNotFoundError로 실패하고, 잡 실패 → gate를 다 통과해도 전체 워크플로 conclusion이 red가 될 수 있습니다.
review==success 게이팅이 대부분 막아주지만, suggestion 파일 부재를 graceful하게 SKIP(changed=false) 처리하면 더 견고합니다.
|
소현님 리뷰 반영했습니다.
테스트: /Users/mjjwa/.local/bin/uv run --python 3.11 --with pytest --with pyyaml --with markdown --with beautifulsoup4 pytest tests/test_apply_metadata_suggestion.py tests/test_pr_review_workflow.py -q
# 20 passed
/Users/mjjwa/.local/bin/uv run --python 3.11 --with pytest --with pyyaml --with markdown --with beautifulsoup4 pytest tests skills/seo/tests -q
# 185 passed |
a704a74 to
e475a14
Compare
요약
SEO 평가 gate와 metadata 생성/적용 흐름을 분리합니다.
seo.json은 기존처럼 SEO 평가/루브릭 gate 결과만 담도록 유지했습니다.metadata-suggestion.json의ERROR,PARTIAL,SKIPPED가 SEO gate failure로 번지지 않게 했습니다.PARTIAL상태에서도 safe frontmatter 필드(title,description,categories,image)는 자동 적용합니다.canonical,hreflang,json_ld처럼 정책 판단이 필요한 필드는 정책값이 명확할 때만 적용합니다.OPENAI_API_KEY를 주입하지 않아 deterministic/idempotent하게 동작하도록 했습니다.metadata-suggestion.json이 없으면 workflow를 실패시키지 않고SKIPPED,changed=false로 처리합니다.metadata apply댓글을 남기면 partial suggestion의 safe fields를 frontmatter에 적용할 수 있게 했습니다.canonical/hreflang도 적용할 수 있게 했습니다.주요 변경
PR agent
SEO_OPENAI_REQUIRED를 평가/metadata 양쪽에 동시에 쓰던 흐름을 분리했습니다.SEO_RUBRIC_OPENAI_REQUIRED: SEO 루브릭 gate용SEO_METADATA_OPENAI_REQUIRED: metadata suggestion 생성용scripts/hf_agent/apply_metadata_suggestion.py추가metadata-suggestion.json을 읽습니다.SKIPPED,changed=false로 종료합니다.PARTIAL상태여도 safe frontmatter fields를 적용합니다.NO_CHANGE,changed=false로 종료합니다.metadata apply를 요청하면PARTIAL상태에서도 safe fields를 적용할 수 있습니다.canonical/hreflang도 적용할 수 있습니다.HF Agent / Apply Metadata Suggestionjob 추가OPENAI_API_KEY를 주입하지 않습니다. 자동 재발화 경로에서 LLM 후보가 매번 달라져 commit loop가 생기는 것을 막기 위함입니다.🔧 Update SEO metadata커밋을 push합니다.metadata apply/SEO metadata apply/메타데이터 적용을 포함하면 metadata apply intent로 처리합니다.PARTIALsuggestion에서는title,description,categories,image만 safe fields로 적용합니다.canonical/hreflang도 적용할 수 있습니다.SEO metadata output
results/seo.md,results/seo.json은 SEO gate/report용으로 유지합니다.results/metadata-suggestion.json은 metadata 생성 모듈 output으로 분리합니다.metadata-suggestion.json의 status, candidate, policy decision, warnings 요약만 표시합니다.metadata-suggestion.json에는 계속skill,conclusion을 넣지 않습니다.metadata-suggestion.json파일 자체는 블로그 repo에 commit하지 않습니다. 실제 SEO 적용은_posts/*.mdfrontmatter 변경으로 남습니다.SEO 모듈 담당자 리뷰 포인트
SEO 모듈 쪽에서 새로 큰 작업을 해야 하는 상태는 아닙니다. 이미
metadata-suggestion.json을 생성하고 있으므로, 이 PR에서는 PR agent가 그 산출물을 어떻게 소비할지 정리했습니다.리뷰할 때는 아래를 봐주시면 됩니다.
docs/seo-metadata-module-io.mdmetadata-suggestion.json필드 의미가 SEO 모듈 의도와 맞는지PARTIAL상태에서도 safe frontmatter field 후보를 채워주는 기준이 맞는지needs_policy_decision으로 남기는 기준이 맞는지skills/seo/tools/metadata_suggestion.pycandidate.title,candidate.description,candidate.categories,candidate.image가 deterministic fallback에서도 안정적으로 나오는지target_files,commit_message가 정책 field까지 자동 적용 가능한 케이스에만 들어가는지scripts/hf_agent/apply_metadata_suggestion.py_posts/*.mdfrontmatter만 수정하도록 제한한 것이 적절한지scripts/hf_agent/handle_pr_feedback.pymetadata applyintent 처리와 safe field 범위가 적절한지E2E
기존 번역 PR에서 검증했습니다.
1. Review/report E2E
2. Trusted comment metadata apply E2E
metadata applytarget_urlsource_urlcanonical_policytranslation_indexingtarget_localesource_locale82ddd20d779083121bc79567a9db2f89f49d661a—🔧 Apply SEO metadata suggestiontitledescriptioncategoriesimagecanonicalhreflang3. Idempotency E2E after reviewer feedback
Verify metadata update: skippedPush metadata update: skippedcef3a2f43eb95cfc4a4561b9f6fe3e8077f88de24. Failed-safe E2E
metadata apply참고: #24 최신 상태에서 #168에도 workflow를 돌려봤지만, independent verifier가 실패하여 metadata apply job까지 진행되지 않았습니다. 이는 “verifier가 green일 때만 metadata apply”라는 현재 조건과 일치합니다.
테스트