Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/hourly-commercial-maintenance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Hourly Commercial Maintenance
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

on:
schedule:
- cron: "41 * * * *"
workflow_dispatch:
inputs:
dry_run:
description: Inspect the queue without dispatching a repair
required: false
default: false
type: boolean

concurrency:
group: hourly-commercial-maintenance-${{ github.repository }}
cancel-in-progress: true

permissions: {}

jobs:
review-repair:
permissions:
actions: read
contents: read
id-token: write
issues: read
pull-requests: read
statuses: read
uses: ContextualWisdomLab/.github/.github/workflows/nvidia-nim-pr-maintenance.yml@2ebda8063d3c541e2552dc474c3c8601ddc986f8
with:
target_repository: ContextualWisdomLab/newsdom-api
base_branch: develop
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}
max_prs: "50"
max_dispatches: "1"
retry_hours: "1"
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- OpenAPI 제목/설명, README, `ArticleNode.headline` 문서를 일반 문서용 (section heading) 표현으로 재구성하여 특정 언어/신문 가정을 소비자에게 노출하지 않도록 함. 응답 스키마 필드는 하위 호환을 위해 변경하지 않음.

### Added
- 매시간 조직 중앙의 immutable NVIDIA NIM OpenCode 유지보수 workflow를 호출하여 열린 PR의 file-scoped 리뷰 수정 작업을 한 번에 하나씩 재시도하는 상용 유지보수 루프를 추가했습니다. Leaf workflow는 `COPILOT_GITHUB_TOKEN`이나 모델 키를 보유하지 않으며, 모든 수정 head는 기존 독립 리뷰와 필수 Checks를 다시 통과해야 합니다.
- [UX/DX] OpenAPI Swagger 문서 이해도 향상을 위해 `ParseQuality` 및 `HealthResponse` 모델에 구체적인 `json_schema_extra` 예제 추가. (Palette)
- [CLI] 단일 NewsDOM JSON 파일을 페이지 단위로 분리하는 `tools/split_dom.py` 도구를 추가했습니다.
- [CLI] NewsDOM JSON 파일의 모든 텍스트 내용을 마스킹하여 익명화하는 `tools/anonymize_dom.py` 도구를 추가했습니다.
Expand Down
56 changes: 56 additions & 0 deletions docs/operations/hourly-commercial-maintenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Hourly commercial maintenance

## Purpose

NewsDOM invokes the organization-owned pull-request maintenance control plane once per hour. The leaf repository supplies only its repository identity, `develop` integration branch, cadence, and bounded queue limits. Review parsing, exact-head selection, repair dispatch, credential separation, and OpenCode model configuration remain owned by `ContextualWisdomLab/.github`.

## Schedule and single-flight behavior

- Schedule: minute 41 of every hour.
- Maximum inspected pull requests: 50.
- Maximum repair dispatches per run: 1.
- Same-head repair retry floor: 1 hour.
- Concurrency: one active run per repository; a newer scheduler invocation cancels an older scheduler invocation before another repair can be dispatched.
- Manual mode: `workflow_dispatch` exposes `dry_run`, which reads the queue and records decisions without dispatching a worker.

The central worker uses OpenCode with the organization secret `NVIDIA_NIM_API_KEY`. The leaf workflow never receives that secret and never uses `COPILOT_GITHUB_TOKEN`. GitHub transport authorization is obtained centrally through the existing OpenCode App OIDC exchange or an explicitly declared maintenance token fallback. The read-only OpenCode and Noema review workflows are unchanged.

## Merge and safety boundaries

The hourly workflow does not merge a pull request. It only dispatches a conservative repair when the current head has actionable, file-scoped review evidence or meets the central conflict-resolution contract. A repair:

1. revalidates the live base and head SHAs;
2. accepts only same-repository heads;
3. gives OpenCode access only to the approved paths;
4. denies model shell, task, network, LSP, and external-directory access;
5. refuses a push if the head moves;
6. produces a new head that must pass all repository checks and independent review; and
7. cannot approve, publish, release, or weaken branch protection.

## MSA boundary

This workflow is a leaf adapter, not a second implementation. It pins the reusable central workflow by full commit SHA. Other CWL components can adopt the same central component while supplying their own target repository, default branch, and schedule. NewsDOM continues to operate independently as a FastAPI PDF-to-DOM service and as a naruon-compatible sidecar.

## Operational prerequisites

- The organization OpenCode GitHub App must be able to read and write the target pull-request head.
- The central `.github` repository must expose the pinned reusable workflow and worker commit.
- `NVIDIA_NIM_API_KEY` must be available to the central worker repository.
- Required checks and independent-review rules remain active.

If the central app-token exchange is unavailable and no optional maintenance token is configured, cross-repository dispatch fails closed. It does not fall back to Copilot or another inference provider.

## Verification

Before enabling the schedule on `develop`, verify:

- the caller pins exactly one 40-character central commit SHA;
- no model or Copilot credential appears in the leaf workflow;
- the workflow grants no `contents: write` or `pull-requests: write` permission;
- static contracts pass under the repository's 100% coverage gate;
- the central workflow passes its own unit, security, and independent-review gates; and
- one manual dry run records a bounded no-write queue decision.

## Rollback

Delete or disable `.github/workflows/hourly-commercial-maintenance.yml`. This does not affect application runtime, existing review workflows, branch protection, or manually initiated maintenance. Revert only the leaf adapter; the central component can remain available to other repositories.
99 changes: 99 additions & 0 deletions tests/test_hourly_commercial_maintenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Contracts for NewsDOM's hourly commercial-maintenance caller."""

from __future__ import annotations

from pathlib import Path
import re


REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
WORKFLOW_PATH = REPOSITORY_ROOT / ".github/workflows/hourly-commercial-maintenance.yml"


def _workflow_text() -> str:
"""Return the tracked hourly workflow text."""

return WORKFLOW_PATH.read_text(encoding="utf-8")


def test_hourly_cadence_is_single_flight() -> None:
"""The repository should request one non-overlapping maintenance run each hour."""

workflow = _workflow_text()

assert 'cron: "41 * * * *"' in workflow
assert "hourly-commercial-maintenance-${{ github.repository }}" in workflow
assert "cancel-in-progress: true" in workflow
assert 'max_dispatches: "1"' in workflow
assert 'retry_hours: "1"' in workflow


def test_caller_targets_newsdom_develop_through_immutable_central_source() -> None:
"""The leaf workflow must retain only local routing and pin central behavior by SHA."""

workflow = _workflow_text()
match = re.search(
r"uses: ContextualWisdomLab/\.github/\.github/workflows/"
r"nvidia-nim-pr-maintenance\.yml@([0-9a-f]{40})",
workflow,
)

assert match is not None
assert match.group(1) == "2ebda8063d3c541e2552dc474c3c8601ddc986f8"
assert "target_repository: ContextualWisdomLab/newsdom-api" in workflow
assert "base_branch: develop" in workflow
assert "uses: ./.github/workflows/" not in workflow
assert "@main" not in workflow


def test_caller_grants_only_read_and_oidc_permissions() -> None:
"""The leaf token must be read-only except for the OIDC identity exchange."""

workflow = _workflow_text()

assert "permissions: {}" in workflow
required_permissions = (
"actions: read",
"contents: read",
"id-token: write",
"issues: read",
"pull-requests: read",
"statuses: read",
)
for permission in required_permissions:
assert permission in workflow

forbidden_permissions = (
"actions: write",
"contents: write",
"issues: write",
"pull-requests: write",
"statuses: write",
)
for permission in forbidden_permissions:
assert permission not in workflow

assert "secrets: inherit" not in workflow


def test_caller_never_uses_copilot_or_model_credentials() -> None:
"""Inference credentials belong to the central NIM worker, not the leaf scheduler."""

workflow = _workflow_text()

assert "COPILOT_GITHUB_TOKEN" not in workflow
assert "NVIDIA_NIM_API_KEY" not in workflow
assert "NVIDIA_API_KEY" not in workflow
assert "STRIX_GITHUB_MODELS_TOKEN" not in workflow
assert "models.github.ai" not in workflow


def test_manual_dry_run_is_available_without_changing_schedule_behavior() -> None:
"""Operators should be able to inspect the queue without dispatching repairs."""

workflow = _workflow_text()

assert "workflow_dispatch:" in workflow
assert "dry_run:" in workflow
assert "type: boolean" in workflow
assert "github.event_name == 'workflow_dispatch'" in workflow
5 changes: 4 additions & 1 deletion tests/test_workflow_runtime_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ def _workflow_paths() -> list[Path]:
)


def test_each_workflow_job_forces_javascript_actions_to_node24():
def test_each_direct_workflow_job_forces_javascript_actions_to_node24():
for workflow_path in _workflow_paths():
data = yaml.safe_load(workflow_path.read_text(encoding="utf-8"))
for job_name, job_data in data["jobs"].items():
if workflow_path.name in {"scorecards.yml", "gh-pages.yml"}:
continue
if "uses" in job_data:
assert "env" not in job_data, (workflow_path, job_name)
continue
assert job_data["env"]["FORCE_JAVASCRIPT_ACTIONS_TO_NODE24"] is True, (
workflow_path,
job_name,
Expand Down
Loading