diff --git a/ci/gitlab/runtime-intelligence-artifacts.yml b/ci/gitlab/runtime-intelligence-artifacts.yml index 9e4c904..1b15cca 100644 --- a/ci/gitlab/runtime-intelligence-artifacts.yml +++ b/ci/gitlab/runtime-intelligence-artifacts.yml @@ -105,13 +105,18 @@ inferedge:deployment-risk-gate: - job: inferedge:portfolio-report artifacts: true script: - - mkdir -p "$INFEREDGE_REPORT_DIR" - - poetry run inferedgelab portfolio-demo-check --format json > "$INFEREDGE_REPORT_DIR/deployment_risk_summary.json" - - poetry run python scripts/check_runtime_intelligence_ci_artifacts.py --report-dir "$INFEREDGE_REPORT_DIR" --summary-out "$INFEREDGE_REPORT_DIR/runtime_intelligence_ci_artifact_gate_summary.md" - - python -c "import json, pathlib, sys; data=json.loads(pathlib.Path('$INFEREDGE_REPORT_DIR/deployment_risk_summary.json').read_text()); sys.exit(0 if data.get('status') == 'pass' else 2)" + - bash scripts/smoke_runtime_intelligence_chain.sh --output-dir "$INFEREDGE_REPORT_DIR" artifacts: when: always expire_in: 14 days paths: + - "$INFEREDGE_REPORT_DIR/edgeenv_runtime_regression.md" + - "$INFEREDGE_REPORT_DIR/edgeenv_runtime_regression.html" + - "$INFEREDGE_REPORT_DIR/runtime_anomaly_summary.md" + - "$INFEREDGE_REPORT_DIR/runtime_anomaly_summary.html" + - "$INFEREDGE_REPORT_DIR/runtime_anomaly_gate_summary.md" + - "$INFEREDGE_REPORT_DIR/runtime_intelligence_bundle_manifest_gate_summary.md" + - "$INFEREDGE_REPORT_DIR/portfolio_demo_check.json" + - "$INFEREDGE_REPORT_DIR/portfolio_demo_check.md" - "$INFEREDGE_REPORT_DIR/deployment_risk_summary.json" - "$INFEREDGE_REPORT_DIR/runtime_intelligence_ci_artifact_gate_summary.md" diff --git a/docs/ci/runtime_intelligence_gitlab_artifacts.md b/docs/ci/runtime_intelligence_gitlab_artifacts.md index 1844564..42871ea 100644 --- a/docs/ci/runtime_intelligence_gitlab_artifacts.md +++ b/docs/ci/runtime_intelligence_gitlab_artifacts.md @@ -26,6 +26,10 @@ bash scripts/smoke_runtime_intelligence_chain.sh \ --output-dir reports/runtime_intelligence_chain ``` +The optional template uses the same smoke script in the final +`deployment-risk` stage so the GitLab artifact gate and the local reproduction +path validate the same file bundle. + This maps to the ecosystem ownership model: - Runtime evidence stays additive and Lab-compatible. @@ -95,6 +99,7 @@ The initial gate is conservative: - producer schema markers for EdgeEnv history, Orchestrator feed, and AIGuard diagnosis evidence must stay aligned with the committed smoke artifacts - portfolio demo check status must be `pass` +- deployment risk summary status must be `pass` - the final deployment-risk job must re-check the collected manifest/report gate summaries and Runtime Intelligence Risk Summary markers before passing - the bundle manifest gate summary must include validated contract markers for diff --git a/scripts/check_runtime_intelligence_ci_artifacts.py b/scripts/check_runtime_intelligence_ci_artifacts.py index 6a592c8..6a0568d 100644 --- a/scripts/check_runtime_intelligence_ci_artifacts.py +++ b/scripts/check_runtime_intelligence_ci_artifacts.py @@ -18,6 +18,10 @@ "runtime_intelligence_bundle_manifest_gate_summary.md", "runtime_anomaly_gate_summary.md", } +REQUIRED_JSON_ARTIFACTS = { + "portfolio_demo_check.json", + "deployment_risk_summary.json", +} REQUIRED_BUNDLE_MANIFEST_SUMMARY_MARKERS = ( "## Validated Contract Markers", "source_repositories: Runtime, EdgeEnv, Orchestrator, AIGuard, Lab", @@ -65,6 +69,7 @@ def _validate_required_files(report_dir: Path, errors: list[str]) -> None: REQUIRED_MARKDOWN_ARTIFACTS | REQUIRED_HTML_ARTIFACTS | REQUIRED_SUMMARY_ARTIFACTS + | REQUIRED_JSON_ARTIFACTS ): _record((report_dir / name).is_file(), errors, f"missing artifact: {name}") @@ -117,6 +122,16 @@ def _validate_portfolio_status(path: Path, errors: list[str]) -> None: ) +def _validate_deployment_risk_status(path: Path, errors: list[str]) -> None: + payload = _load_json(path, errors, "Deployment risk summary JSON") + if payload: + _record( + payload.get("status") == "pass", + errors, + "deployment_risk_summary.json status must be pass", + ) + + def _write_summary(path: Path, report_dir: Path, errors: list[str]) -> None: lines = [ "# Runtime Intelligence CI Artifact Gate", @@ -152,6 +167,10 @@ def main(report_dir: str, summary_out: str = "") -> int: ) _validate_runtime_report(report_path / "runtime_anomaly_summary.md", errors) _validate_portfolio_status(report_path / "portfolio_demo_check.json", errors) + _validate_deployment_risk_status( + report_path / "deployment_risk_summary.json", + errors, + ) if summary_out: _write_summary(Path(summary_out), report_path, errors) diff --git a/tests/test_runtime_intelligence_ci_template.py b/tests/test_runtime_intelligence_ci_template.py index 59cc7ef..1f47cf1 100644 --- a/tests/test_runtime_intelligence_ci_template.py +++ b/tests/test_runtime_intelligence_ci_template.py @@ -43,8 +43,9 @@ def test_runtime_intelligence_gitlab_template_keeps_local_first_artifact_contrac assert "--guard-analysis" in text assert "check_runtime_intelligence_artifact_bundle.py" in text assert "runtime_anomaly_gate_summary.md" in text - assert "check_runtime_intelligence_ci_artifacts.py" in text + assert "smoke_runtime_intelligence_chain.sh --output-dir" in text assert "runtime_intelligence_ci_artifact_gate_summary.md" in text + assert "deployment_risk_summary.json" in text assert "needs:" in text assert "inferedge:deterministic-anomaly-summary" in text assert "inferedge:portfolio-report" in text @@ -126,6 +127,10 @@ def test_runtime_intelligence_ci_artifact_gate_passes_for_expected_outputs(tmp_p '{"status": "pass"}', encoding="utf-8", ) + (report_dir / "deployment_risk_summary.json").write_text( + '{"status": "pass"}', + encoding="utf-8", + ) summary_path = tmp_path / "ci_artifact_gate_summary.md" result = ci_artifact_gate( @@ -163,6 +168,10 @@ def test_runtime_intelligence_ci_artifact_gate_fails_for_missing_risk_summary( '{"status": "pass"}', encoding="utf-8", ) + (report_dir / "deployment_risk_summary.json").write_text( + '{"status": "pass"}', + encoding="utf-8", + ) summary_path = tmp_path / "ci_artifact_gate_summary.md" result = ci_artifact_gate( @@ -221,6 +230,10 @@ def test_runtime_intelligence_ci_artifact_gate_fails_for_missing_contract_marker '{"status": "pass"}', encoding="utf-8", ) + (report_dir / "deployment_risk_summary.json").write_text( + '{"status": "pass"}', + encoding="utf-8", + ) summary_path = tmp_path / "ci_artifact_gate_summary.md" result = ci_artifact_gate( @@ -272,6 +285,10 @@ def test_runtime_intelligence_ci_artifact_gate_fails_for_missing_coverage_gap_ma '{"status": "pass"}', encoding="utf-8", ) + (report_dir / "deployment_risk_summary.json").write_text( + '{"status": "pass"}', + encoding="utf-8", + ) summary_path = tmp_path / "ci_artifact_gate_summary.md" result = ci_artifact_gate( @@ -282,3 +299,73 @@ def test_runtime_intelligence_ci_artifact_gate_fails_for_missing_coverage_gap_ma assert result == 2 summary = summary_path.read_text(encoding="utf-8") assert "runtime report missing marker: runtime_telemetry_field_gap" in summary + + +def test_runtime_intelligence_ci_artifact_gate_fails_for_failed_deployment_risk( + tmp_path, +): + report_dir = tmp_path / "runtime_intelligence_ci" + report_dir.mkdir() + for name in ( + "edgeenv_runtime_regression.md", + "edgeenv_runtime_regression.html", + "runtime_anomaly_summary.html", + "portfolio_demo_check.md", + ): + (report_dir / name).write_text("placeholder\n", encoding="utf-8") + (report_dir / "runtime_anomaly_summary.md").write_text( + "\n".join( + [ + "## Runtime Intelligence Risk Summary", + "Lab remains the final deployment decision owner.", + "AIGuard runtime operation anomalies", + "runtime_queue_overload, runtime_thermal_instability", + "Runtime telemetry coverage gaps", + "runtime_telemetry_field_gap", + "Inspect telemetry coverage missing fields", + "guard_warning_review", + "edgeenv_runtime_regression_review", + ] + ), + encoding="utf-8", + ) + (report_dir / "runtime_intelligence_bundle_manifest_gate_summary.md").write_text( + "\n".join( + [ + "- Status: passed", + "## Validated Contract Markers", + "- source_repositories: Runtime, EdgeEnv, Orchestrator, AIGuard, Lab", + "- producer_contracts: EdgeEnv history, Orchestrator feed, AIGuard diagnosis", + "- ownership: regression_owner=edgeenv, deployment_decision_owner=lab", + "- orchestrator_mapping_hint: coverage_summary_owner=edgeenv", + "- orchestrator_mapping_hint: operation_context_role=supplemental", + "- orchestrator_mapping_hint: aiguard_evidence_candidates=runtime_queue_overload,runtime_thermal_instability", + "- aiguard_raw_context: telemetry_coverage_source=history_telemetry_coverage", + "- aiguard_raw_context: orchestrator_mapping_hint preserved", + "- edgeenv_handoff: lab_bundle_alignment validated", + ] + ), + encoding="utf-8", + ) + (report_dir / "runtime_anomaly_gate_summary.md").write_text( + "- Status: passed\n", + encoding="utf-8", + ) + (report_dir / "portfolio_demo_check.json").write_text( + '{"status": "pass"}', + encoding="utf-8", + ) + (report_dir / "deployment_risk_summary.json").write_text( + '{"status": "fail"}', + encoding="utf-8", + ) + summary_path = tmp_path / "ci_artifact_gate_summary.md" + + result = ci_artifact_gate( + report_dir=str(report_dir), + summary_out=str(summary_path), + ) + + assert result == 2 + summary = summary_path.read_text(encoding="utf-8") + assert "deployment_risk_summary.json status must be pass" in summary