Skip to content

[caliper] Extend the notification and include artifacts censoring - #145

Open
kpouget wants to merge 17 commits into
openshift-psap:mainfrom
kpouget:censoring
Open

[caliper] Extend the notification and include artifacts censoring#145
kpouget wants to merge 17 commits into
openshift-psap:mainfrom
kpouget:censoring

Conversation

@kpouget

@kpouget kpouget commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added artifact censoring with in-place sanitization/redaction, dry-run, verbose output, optional output-directory mode, and optional JSON reporting; also added filename/content sensitivity detection.
    • Added a new artifacts censor CLI command.
    • Added orchestration censoring with generated YAML reports and automatic CI notifications, plus export behavior controls (disable censoring / disable file export / enhanced dry-run).
    • Added richer export completion notifications with enhanced Markdown, artifact links, and per-step/post-process summaries.
  • Bug Fixes
    • Improved export configuration overrides, fail behavior, and exit/status reporting by including censoring/failure reasons.
    • Refined CI notification formatting and improved CI entrypoint exit-status handling.

@openshift-ci

openshift-ci Bot commented Jul 29, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign mml-coder for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kpouget, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8613ded4-5b7e-44c5-b5d6-a39bc4fe5aff

📥 Commits

Reviewing files that changed from the base of the PR and between 7c131b9 and 4fc116f.

📒 Files selected for processing (12)
  • projects/caliper/cli/commands.py
  • projects/caliper/cli/main.py
  • projects/caliper/engine/file_export/censoring.py
  • projects/caliper/engine/file_export/censoring_rules.py
  • projects/caliper/orchestration/censoring.py
  • projects/caliper/orchestration/export.py
  • projects/core/library/export.py
  • projects/core/library/export_notifications.py
  • projects/core/library/step_status.py
  • projects/core/library/vault.py
  • projects/skeleton/orchestration/test_skeleton.py
  • vaults/psap-forge-notifications.yaml
📝 Walkthrough

Walkthrough

Adds artifact censoring rules, vault-secret discovery, CLI and orchestration integration, export controls, enhanced notifications, MLflow configuration wiring, and richer CI status metadata.

Changes

Caliper artifact censoring

Layer / File(s) Summary
Censoring rules and vault discovery
projects/caliper/engine/file_export/censoring_rules.py, projects/core/library/vault.py, projects/caliper/orchestration/censoring.py, vaults/psap-forge-mlflow-export.yaml
Defines sensitive filename/content patterns, adds vault sensitivity metadata, and discovers eligible vault secrets.
Censoring engine and CLI command
projects/caliper/engine/file_export/censoring.py, projects/caliper/cli/commands.py, projects/caliper/cli/main.py
Sanitizes or excludes artifact files and adds the artifacts censor command with dry-run, copy, deletion, reporting, and verbose modes.
Orchestration reports and export control flow
projects/caliper/orchestration/censoring.py, projects/caliper/orchestration/export.py
Applies censoring to single-run and multi-run exports, writes reports and notifications, and adds mock export and exception-based outcomes.
Export CLI and notification wiring
projects/core/library/export.py, projects/caliper/cli/commands.py
Updates export options, artifact-directory resolution, orchestration wiring, MLflow links, step details, final artifact handling, and MLflow configuration passing.
Notification output generation
projects/core/library/export_notifications.py, projects/caliper/orchestration/notification.py
Adds shared notification generation with artifact links, step logs, test status, postprocess status, dry-run support, and updated formatting.
CI status and metadata persistence
projects/core/library/ci.py, projects/core/library/config.py, projects/core/ci_entrypoint/prepare_ci.py
Persists exit reasons, batches preset metadata writes, and adjusts CI and notification-file logging.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant OrchestrationExport
  participant Censoring
  participant MLflow
  participant Notifications
  CLI->>OrchestrationExport: invoke export with censoring and file-export flags
  OrchestrationExport->>Censoring: scan and sanitize artifact files
  Censoring-->>OrchestrationExport: return censoring status and report data
  OrchestrationExport->>MLflow: export processed artifacts
  MLflow-->>OrchestrationExport: return export status
  OrchestrationExport->>Notifications: provide status and artifact details
  Notifications-->>CLI: write notification files and return outcome
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: notification enhancements and artifact censoring.
Docstring Coverage ✅ Passed Docstring coverage is 84.44% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
projects/core/library/export.py (1)

432-454: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use logger, and don't emit placeholder link targets. Line 436 uses the root logging logger instead of the module logger. More importantly, _create_mlflow_url now returns f"BASE_URL_MISSING/{step_dir_name}" (line 229) and this code falls back to "NO_URL", so the notification renders markdown links pointing at non-URLs. Prefer emitting plain text for the step title when no MLflow URL is available.

🤖 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 `@projects/core/library/export.py` around lines 432 - 454, The
_process_step_status function uses the root logging API and creates placeholder
link targets when _create_mlflow_url cannot produce a valid URL. Replace the
warning call with the module-level logger, and when mlflow_log_url is
unavailable, emit the step title as plain text rather than constructing a
markdown link with "NO_URL" or another placeholder target.
projects/caliper/orchestration/export.py (1)

237-308: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Two control-flow problems in the new fall-through to status.yaml.

  1. Lines 298-299 raise CensoringOccurredException before status.yaml is read, so the caller never receives the real export status. In projects/core/library/export.py the handler substitutes {"backends": {}}, which drops the MLflow run/experiment URLs from the notification — exactly the case where censoring info matters most. Prefer reading/returning the status first and signalling censoring via the returned censoring_occurred field (the caller already checks status.get("censoring_occurred") at line 1124), or attach the status to the exception.
  2. The dry-run paths now fall through to open(status_yaml) at line 301: the multi-run dry-run branch (lines 238-241) only logs and skips, and run_artifacts_export(dry_run=True) is unlikely to write the file either, so this raises FileNotFoundError (and status may be None for multi-run before line 305). Return early for dry_run.
🤖 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 `@projects/caliper/orchestration/export.py` around lines 237 - 308, Fix the
export control flow around the single-run and multi-run branches: return the
real status from status_yaml before signaling censoring, preserving the
censoring_occurred field for callers instead of raising
CensoringOccurredException first. Add an early dry_run return before the final
status file read, covering both the multi-run skip path and
run_artifacts_export(dry_run=True), and ensure no missing or None status reaches
the final status handling.
🧹 Nitpick comments (17)
projects/caliper/engine/file_export/censoring_rules.py (2)

85-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

pattern.match only works because every filename pattern is .*-prefixed.

Any future entry added to SENSITIVE_FILE_PATTERNS without a leading .* will silently never match a full path. pattern.search makes the intent explicit and removes the hidden coupling.

♻️ Proposed change
-    return any(pattern.match(filename) for pattern in COMPILED_FILE_PATTERNS)
+    return any(pattern.search(filename) for pattern in COMPILED_FILE_PATTERNS)

(with the leading .* then removable from the pattern list)

🤖 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 `@projects/caliper/engine/file_export/censoring_rules.py` at line 85, Update
the filename matching in the censoring-rule function to use pattern.search
instead of pattern.match, and remove the now-unnecessary leading .* prefixes
from SENSITIVE_FILE_PATTERNS while preserving matching against any path segment.

28-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant case variant under re.IGNORECASE.

COMPILED_KEYWORD_PATTERNS compiles every pattern with re.IGNORECASE (Line 50), so the lowercase bearer entry is dead weight and only inflates the scan loop.

♻️ Proposed cleanup
     # Bearer tokens
     r"Bearer\s+[A-Za-z0-9+/=]+",
-    r"bearer\s+[A-Za-z0-9+/=]+",
🤖 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 `@projects/caliper/engine/file_export/censoring_rules.py` around lines 28 - 29,
Remove the redundant lowercase bearer pattern from COMPILED_KEYWORD_PATTERNS’
source pattern list, retaining the case-insensitive Bearer pattern because
compilation already uses re.IGNORECASE.
projects/caliper/cli/commands.py (2)

1040-1046: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant existence checks; let click enforce the directory contract.

The option already declares exists=True, so Lines 1075-1077 are unreachable. Adding file_okay=False also makes Lines 1079-1081 unnecessary.

♻️ Proposed cleanup
-    type=click.Path(path_type=Path, exists=True),
+    type=click.Path(path_type=Path, exists=True, file_okay=False, dir_okay=True),

Also applies to: 1075-1081

🤖 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 `@projects/caliper/cli/commands.py` around lines 1040 - 1046, Update the
from_path click option to enforce the directory contract directly by adding
file_okay=False alongside exists=True, then remove the redundant existence and
directory validation branches in the related command logic around from_path.

1115-1144: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Report format diverges from the orchestration report, and the full body is echoed.

projects/caliper/orchestration/censoring.py writes YAML (censoring_report.yaml) and projects/core/library/export_notifications.py Line 455 keys its links on that exact name; this command emits JSON with a different schema (censored_details vs censored_by_reason). Also, Line 1141 dumps the whole report to stdout, which is noisy for large artifact trees — the path alone is enough.

🤖 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 `@projects/caliper/cli/commands.py` around lines 1115 - 1144, Update the
report-writing block in the command to emit YAML to the canonical filename
censoring_report.yaml, matching the schema produced by
orchestration/censoring.py, including censored_by_reason instead of
censored_details. Replace the full report body echoed after writing with a
concise message containing only the report path, while preserving dry-run
behavior and error handling.
projects/caliper/engine/file_export/censoring.py (1)

99-107: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

read_bytes()[:1024] loads the entire file to inspect 1 KB.

For extension-less artifacts of arbitrary size (logs, dumps, archives) this is a needless full read on the hot path. Read only the sample.

♻️ Proposed fix
-            sample = file_path.read_bytes()[:1024]
+            with file_path.open("rb") as f:
+                sample = f.read(1024)
🤖 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 `@projects/caliper/engine/file_export/censoring.py` around lines 99 - 107,
Update the extension-less file inspection logic around the sample calculation to
read only the first 1024 bytes from the file stream instead of calling
file_path.read_bytes() and loading the entire file. Preserve the existing
empty-file handling and printable_ratio classification behavior.
projects/core/library/vault.py (1)

42-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

is_sensible is a pass-through over a public field.

The property adds no encapsulation since sensible is already public, and consumers now have two names for one concept. Also consider sensitive — "sensible" reads as a false friend in English and this name is now baked into the vault YAML schema, so renaming later is a breaking change.

♻️ Optional simplification
-    `@property`
-    def is_sensible(self) -> bool:
-        """Whether this content should be considered sensitive for censoring purposes"""
-        return self.sensible
-

with content_def.is_sensiblecontent_def.sensible in projects/caliper/orchestration/censoring.py Line 71.

🤖 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 `@projects/core/library/vault.py` around lines 42 - 45, Remove the redundant
is_sensible property from the content definition class and use the existing
sensible field directly in consumers such as the censoring logic. Rename the
public schema field to sensitive instead of sensible, updating its YAML
serialization/deserialization and all references consistently.
projects/caliper/orchestration/censoring.py (2)

24-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

VAULT_CONTENT_TYPES is never used.

discover_vault_secrets iterates all vault content and filters only on is_sensible, so this list is dead code that implies a name-based filter that does not exist. Remove it, or wire it in if name-based selection was intended.

🤖 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 `@projects/caliper/orchestration/censoring.py` around lines 24 - 38, Remove the
unused VAULT_CONTENT_TYPES constant, since discover_vault_secrets currently
filters vault content only through is_sensible and does not perform name-based
selection. Do not add name-based filtering unless the surrounding implementation
explicitly requires that behavior.

178-244: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

apply_multi_run_censoring duplicates apply_orchestration_censoring verbatim.

The two bodies differ only in log message prefixes. Any fix (including the exclusion gap above) has to be applied twice, which is exactly how the two paths will drift.

♻️ Proposed consolidation
+def _apply_censoring(
+    from_path: Path,
+    export_cfg: CaliperOrchestrationExportConfig,
+    disable_censoring: bool = False,
+    label: str = "",
+) -> bool:
+    ...  # single implementation, `label` used in log prefixes
+
+
+def apply_multi_run_censoring(from_path, export_cfg, disable_censoring=False) -> bool:
+    return _apply_censoring(from_path, export_cfg, disable_censoring, label="multi-run ")
🤖 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 `@projects/caliper/orchestration/censoring.py` around lines 178 - 244,
Consolidate apply_multi_run_censoring with apply_orchestration_censoring by
extracting their shared censoring workflow into one helper or making one
function delegate to the other, parameterizing only the differing log prefix or
context. Preserve each function’s public behavior, including disable-censoring
handling, reporting, notifications, return values, and exclusion processing, so
future fixes apply through a single implementation.
projects/caliper/orchestration/export.py (4)

371-378: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Drop the redundant first scan. all_artifact_paths at line 371 is overwritten at line 377 before any use; censoring runs in-place between them, so only the second scan matters.

♻️ Suggested fix
-    all_artifact_paths = [p for p in from_path.rglob("*") if p.is_file()]
-
     # Apply censoring for multi-run export if enabled
     censoring_occurred = apply_multi_run_censoring(from_path, export_cfg, disable_censoring)
 
     # Update artifact paths - use original paths since we modified in-place
     all_artifact_paths = [p for p in from_path.rglob("*") if p.is_file()]
🤖 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 `@projects/caliper/orchestration/export.py` around lines 371 - 378, Remove the
initial all_artifact_paths scan before apply_multi_run_censoring in the export
flow. Keep the post-censoring scan because it is the only assignment used after
in-place modifications.

472-493: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

raise ... from None discards the original traceback. Use from e to keep the cause chain for debugging.

♻️ Suggested fix
-            raise ExportFailedException(f"Failed to write status YAML: {e}") from None
+            raise ExportFailedException(f"Failed to write status YAML: {e}") from e
🤖 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 `@projects/caliper/orchestration/export.py` around lines 472 - 493, Update the
OSError handler in the status YAML export block to raise ExportFailedException
from the caught exception e instead of suppressing its cause with from None.
Preserve the existing error message and handling in the surrounding status_yaml
flow.

420-466: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Mock branch inside try masks nothing, but hardcoded localhost URLs leak into real notifications. When --disable-file-export is used in CI, run_url/experiment_url point at http://localhost:5000/... and are rendered as clickable "MLflow Run" links in the notification. Consider marking them clearly (e.g. DISABLED_EXPORT sentinel) so readers aren't given dead links.

🤖 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 `@projects/caliper/orchestration/export.py` around lines 420 - 466, Update the
disable_file_export mock metadata in the multi-run export flow so run_url and
experiment_url use an unmistakable DISABLED_EXPORT sentinel instead of localhost
URLs. Keep the mock success result and other metadata unchanged, ensuring
notifications cannot render the values as misleading clickable MLflow links.

169-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Return annotation is stale. The function now returns the parsed status dict (line 308), not an int.

♻️ Suggested fix
-) -> int:
+) -> dict[str, Any]:
🤖 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 `@projects/caliper/orchestration/export.py` around lines 169 - 171, Update the
return annotation of the affected function to reflect that it returns the parsed
status dictionary rather than an int. Locate the function by its parameters
disable_censoring and disable_file_export, and keep its existing return behavior
unchanged.
projects/core/library/export.py (4)

406-410: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Return annotation contradicts the body. The function returns list[str] (return [] / return notifications_from_files) but is annotated -> None.

♻️ Suggested fix
-def _process_notification_files(step_dir: Path) -> None:
+def _process_notification_files(step_dir: Path) -> list[str]:
🤖 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 `@projects/core/library/export.py` around lines 406 - 410, Update the return
annotation of _process_notification_files to list[str] so it matches the
empty-list and notifications_from_files return values; leave the existing
processing behavior unchanged.

890-892: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

artifact_dir is accepted but unused. The directory is communicated to the orchestration layer via config.project.set_config("caliper.export.from", ...) in the entrypoint, so this parameter is dead. Either use it (e.g. set the config here) or drop it.

🤖 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 `@projects/core/library/export.py` around lines 890 - 892, Remove the unused
artifact_dir parameter from run_caliper_orchestration_export, and update all
call sites to stop passing it; retain the existing configuration flow through
config.project.set_config("caliper.export.from", ...) in the entrypoint.

612-618: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Signature says -> str, body returns (str, bool). send_notification unpacks two values, so fix the annotation.

♻️ Suggested fix
-) -> str:
+) -> tuple[str, bool]:

Also applies to: 685-685

🤖 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 `@projects/core/library/export.py` around lines 612 - 618, Update the return
annotation of _build_enhanced_notification to describe the two-value tuple it
actually returns, matching the unpacking performed by send_notification. Apply
the same correction to the related annotation at the additional location.

688-701: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the shadowing local imports. Path and yaml are already imported at module scope; re-importing inside these helpers adds noise.

Also applies to: 758-767

🤖 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 `@projects/core/library/export.py` around lines 688 - 701, Remove the redundant
local imports of Path and yaml from _extract_test_labels_info and the
additionally referenced helper around the indicated second location, relying on
the existing module-level imports while leaving the helper behavior unchanged.
projects/core/library/export_notifications.py (1)

102-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rebinding the artifact_dir parameter to env.ARTIFACT_DIR is surprising. Line 107 overwrites the caller-supplied directory just to build the Slack context; use a distinct local name to avoid confusion for future readers.

🤖 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 `@projects/core/library/export_notifications.py` around lines 102 - 115, Rename
the local value derived from env.ARTIFACT_DIR in the notification export flow to
a distinct context-specific name, then pass that renamed variable to
NotificationContext while preserving the caller-supplied artifact_dir parameter
unchanged.
🤖 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 `@projects/caliper/cli/commands.py`:
- Around line 1098-1099: Update the `censored_files` and `clean_files`
comprehensions to distinguish sanitized results from files that were only
censored: include a result in `censored_files` only when `censored` is true and
`sanitized` is false, and include sanitized results in `clean_files` so both
in-place and `--output` modes retain them.
- Around line 1092-1095: Propagate the command’s dry_run state into
apply_censoring_to_artifacts and the underlying censoring flow, adding a dry_run
parameter where needed; when enabled, skip the write_text operation while
preserving censoring analysis and results. Update the call site near the
existing apply_censoring_to_artifacts invocation so dry-run mode does not modify
files.

In `@projects/caliper/engine/file_export/censoring.py`:
- Around line 166-168: Update the exception path in the file sanitization flow
to fail closed: when the operation around the censoring/write-back logic raises,
return a CensoringResult with censored=True so the file is excluded from export.
Preserve the existing warning and failure message details.
- Around line 134-147: Update the keyword-handling logic around
COMPILED_KEYWORD_PATTERNS so each matched span is redacted in place rather than
replacing the entire content with “Content censored by caliper”. Preserve the
existing reason reporting using KEYWORD_PATTERNS, mark sanitized when any match
is found, and retain all non-matching content.

In `@projects/caliper/orchestration/censoring.py`:
- Around line 140-151: Ensure both orchestration export paths remove or
quarantine every result marked censored and not sanitized before exporting, or
pass the processed_paths allow-list to the exporter instead of exporting the
entire from_path tree. Apply this handling in the single-run flow around the
clean_files/sanitized_files/excluded_files classification at
projects/caliper/orchestration/censoring.py lines 140-151 and in the multi-run
flow at lines 209-220; both sites require the same change.

In `@projects/core/library/ci.py`:
- Line 249: Remove the logger.info call that logs the caller-provided message in
the notification flow; retain the existing created-path log and do not log the
full payload or its sensitive contents.
- Around line 172-184: Sanitize the reason assigned in the command execution
flow before it reaches persistence or logging. In the result-handling branch,
accept only an explicit allowlist of safe reason values; for exceptions handled
by handle_ci_exception, set reason to the generic command_failed value instead
of str(e). Ensure the downstream artifact and log handling uses only this
sanitized reason.

In `@projects/core/library/config.py`:
- Around line 215-219: Update the configuration-processing flow around the
deferred file write so messages for keys successfully applied before a later
exception are still appended to presets_applied.txt. Flush accumulated
file_messages in a finally path, or keep one append handle open during
processing, while preserving the existing message format and partial-application
behavior.
- Around line 167-173: Update the preset/override message construction and
persistence flow around dest_txt, file_messages, and the related logging to
never interpolate raw value or resolved actual_value; record only the key or an
explicit safe redaction. Avoid resolving secret references solely for message
formatting, including the handled_secretly=False path, while preserving
non-sensitive override reporting and writing only redacted content under
ARTIFACT_DIR.
- Around line 193-213: Update the config override handling around set_config and
the new-key branch so each applied override appends its sanitized metadata
message to file_messages regardless of log. Restrict log to guarding logger.info
calls only, preserving the existing message content and save behavior; ensure
ignored overrides remain unchanged unless they are also considered applied.

In `@projects/core/library/export_notifications.py`:
- Around line 354-359: Update the notification URL construction around
mlflow_log_url and step_title to reuse _create_mlflow_file_url_for_step instead
of concatenating paths onto mlflow_run_url, preserving query parameters and
fragments. Ensure step_status always receives a step heading: keep the linked
title when mlflow_run_url exists and append an equivalent plain-text title when
it does not.
- Around line 182-256: The notification-building logic is duplicated between
this module and projects/core/library/export.py; choose one canonical
implementation and remove the duplicate definitions, updating callers to import
and reuse the canonical symbols including _build_enhanced_notification,
_extract_artifact_links, _process_notification_files, _process_step_details,
_create_mlflow_file_url_for_step, and CI_METADATA_DIRNAME. Ensure
CI_METADATA_DIRNAME is imported from projects.core.ci_entrypoint.prepare_ci and
preserve the existing public behavior through the shared implementation.
- Around line 49-67: Update the dry_run flow in the notification export function
so the notification file-writing block executes before returning
notification_success. Preserve the existing dry-run logging and ensure the later
dry_run-specific file log remains reachable; remove the early return before the
file write.
- Around line 517-537: Update get_file_link to return a valid fallback or omit
the link when mlflow_run_url is absent, preventing None from being interpolated
into notifications. Update _create_mlflow_file_url_for_step to accept the string
paths supplied by notification callers by normalizing file_path to a Path before
using name or is_file, while preserving URL generation for both Path and str
inputs.

In `@projects/core/library/export.py`:
- Around line 705-750: Update _process_step_details and the __test_labels__.yaml
processing so MLflow links retain the step-directory prefix when artifact_dir is
the step directory. Pass the step name explicitly or compute the path relative
to the artifact root, and use that prefixed path in
_create_mlflow_file_url_for_step; ensure the displayed directory name is the
step name rather than "root" for files directly inside a step directory.
- Around line 1001-1029: Normalize the resolved artifact_dir to a pathlib.Path
before it is stored in config or passed to notification generation. Apply this
after the precedence resolution and before the dry-run/logging and set_config
flow, while preserving the existing FOURNOS_CI parent-directory handling and
error return for missing values.

In `@vaults/psap-forge-mlflow-export.yaml`:
- Around line 20-22: Update the `description` for the `mlflow username` vault
entry to explicitly state why it is intentionally marked `sensible: false` and
exempt from CI log censoring, replacing the conflicting claim that it is stored
for censoring.

---

Outside diff comments:
In `@projects/caliper/orchestration/export.py`:
- Around line 237-308: Fix the export control flow around the single-run and
multi-run branches: return the real status from status_yaml before signaling
censoring, preserving the censoring_occurred field for callers instead of
raising CensoringOccurredException first. Add an early dry_run return before the
final status file read, covering both the multi-run skip path and
run_artifacts_export(dry_run=True), and ensure no missing or None status reaches
the final status handling.

In `@projects/core/library/export.py`:
- Around line 432-454: The _process_step_status function uses the root logging
API and creates placeholder link targets when _create_mlflow_url cannot produce
a valid URL. Replace the warning call with the module-level logger, and when
mlflow_log_url is unavailable, emit the step title as plain text rather than
constructing a markdown link with "NO_URL" or another placeholder target.

---

Nitpick comments:
In `@projects/caliper/cli/commands.py`:
- Around line 1040-1046: Update the from_path click option to enforce the
directory contract directly by adding file_okay=False alongside exists=True,
then remove the redundant existence and directory validation branches in the
related command logic around from_path.
- Around line 1115-1144: Update the report-writing block in the command to emit
YAML to the canonical filename censoring_report.yaml, matching the schema
produced by orchestration/censoring.py, including censored_by_reason instead of
censored_details. Replace the full report body echoed after writing with a
concise message containing only the report path, while preserving dry-run
behavior and error handling.

In `@projects/caliper/engine/file_export/censoring_rules.py`:
- Line 85: Update the filename matching in the censoring-rule function to use
pattern.search instead of pattern.match, and remove the now-unnecessary leading
.* prefixes from SENSITIVE_FILE_PATTERNS while preserving matching against any
path segment.
- Around line 28-29: Remove the redundant lowercase bearer pattern from
COMPILED_KEYWORD_PATTERNS’ source pattern list, retaining the case-insensitive
Bearer pattern because compilation already uses re.IGNORECASE.

In `@projects/caliper/engine/file_export/censoring.py`:
- Around line 99-107: Update the extension-less file inspection logic around the
sample calculation to read only the first 1024 bytes from the file stream
instead of calling file_path.read_bytes() and loading the entire file. Preserve
the existing empty-file handling and printable_ratio classification behavior.

In `@projects/caliper/orchestration/censoring.py`:
- Around line 24-38: Remove the unused VAULT_CONTENT_TYPES constant, since
discover_vault_secrets currently filters vault content only through is_sensible
and does not perform name-based selection. Do not add name-based filtering
unless the surrounding implementation explicitly requires that behavior.
- Around line 178-244: Consolidate apply_multi_run_censoring with
apply_orchestration_censoring by extracting their shared censoring workflow into
one helper or making one function delegate to the other, parameterizing only the
differing log prefix or context. Preserve each function’s public behavior,
including disable-censoring handling, reporting, notifications, return values,
and exclusion processing, so future fixes apply through a single implementation.

In `@projects/caliper/orchestration/export.py`:
- Around line 371-378: Remove the initial all_artifact_paths scan before
apply_multi_run_censoring in the export flow. Keep the post-censoring scan
because it is the only assignment used after in-place modifications.
- Around line 472-493: Update the OSError handler in the status YAML export
block to raise ExportFailedException from the caught exception e instead of
suppressing its cause with from None. Preserve the existing error message and
handling in the surrounding status_yaml flow.
- Around line 420-466: Update the disable_file_export mock metadata in the
multi-run export flow so run_url and experiment_url use an unmistakable
DISABLED_EXPORT sentinel instead of localhost URLs. Keep the mock success result
and other metadata unchanged, ensuring notifications cannot render the values as
misleading clickable MLflow links.
- Around line 169-171: Update the return annotation of the affected function to
reflect that it returns the parsed status dictionary rather than an int. Locate
the function by its parameters disable_censoring and disable_file_export, and
keep its existing return behavior unchanged.

In `@projects/core/library/export_notifications.py`:
- Around line 102-115: Rename the local value derived from env.ARTIFACT_DIR in
the notification export flow to a distinct context-specific name, then pass that
renamed variable to NotificationContext while preserving the caller-supplied
artifact_dir parameter unchanged.

In `@projects/core/library/export.py`:
- Around line 406-410: Update the return annotation of
_process_notification_files to list[str] so it matches the empty-list and
notifications_from_files return values; leave the existing processing behavior
unchanged.
- Around line 890-892: Remove the unused artifact_dir parameter from
run_caliper_orchestration_export, and update all call sites to stop passing it;
retain the existing configuration flow through
config.project.set_config("caliper.export.from", ...) in the entrypoint.
- Around line 612-618: Update the return annotation of
_build_enhanced_notification to describe the two-value tuple it actually
returns, matching the unpacking performed by send_notification. Apply the same
correction to the related annotation at the additional location.
- Around line 688-701: Remove the redundant local imports of Path and yaml from
_extract_test_labels_info and the additionally referenced helper around the
indicated second location, relying on the existing module-level imports while
leaving the helper behavior unchanged.

In `@projects/core/library/vault.py`:
- Around line 42-45: Remove the redundant is_sensible property from the content
definition class and use the existing sensible field directly in consumers such
as the censoring logic. Rename the public schema field to sensitive instead of
sensible, updating its YAML serialization/deserialization and all references
consistently.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 54eefc44-f3cf-4e10-aa24-7d1ce6abacd2

📥 Commits

Reviewing files that changed from the base of the PR and between ba1b681 and 553125e.

📒 Files selected for processing (14)
  • projects/caliper/cli/commands.py
  • projects/caliper/cli/main.py
  • projects/caliper/engine/file_export/censoring.py
  • projects/caliper/engine/file_export/censoring_rules.py
  • projects/caliper/orchestration/censoring.py
  • projects/caliper/orchestration/export.py
  • projects/caliper/orchestration/notification.py
  • projects/core/ci_entrypoint/prepare_ci.py
  • projects/core/library/ci.py
  • projects/core/library/config.py
  • projects/core/library/export.py
  • projects/core/library/export_notifications.py
  • projects/core/library/vault.py
  • vaults/psap-forge-mlflow-export.yaml

Comment thread projects/caliper/cli/commands.py
Comment on lines +1098 to +1099
censored_files = [r.file_path for r in censoring_results if r.censored]
clean_files = [r.file_path for r in censoring_results if not r.censored]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Sanitized files are misclassified, causing data loss in both output modes.

CensoringResult.censored is True for sanitized files too (censoring.py Line 162), so:

  • censored_files includes them → the in-place branch at Lines 1175-1176 unlink()s files that were already safely sanitized.
  • clean_files excludes them → --output mode silently omits them from the exported copy.

Only filename-excluded results (censored and not sanitized) should be removed/omitted.

🐛 Proposed fix
-    # Separate censored and clean files
-    censored_files = [r.file_path for r in censoring_results if r.censored]
-    clean_files = [r.file_path for r in censoring_results if not r.censored]
+    # Files excluded outright (sensitive filename) vs files safe to export
+    censored_files = [r.file_path for r in censoring_results if r.censored and not r.sanitized]
+    clean_files = [r.file_path for r in censoring_results if not r.censored or r.sanitized]
📝 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.

Suggested change
censored_files = [r.file_path for r in censoring_results if r.censored]
clean_files = [r.file_path for r in censoring_results if not r.censored]
# Files excluded outright (sensitive filename) vs files safe to export
censored_files = [r.file_path for r in censoring_results if r.censored and not r.sanitized]
clean_files = [r.file_path for r in censoring_results if not r.censored or r.sanitized]
🤖 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 `@projects/caliper/cli/commands.py` around lines 1098 - 1099, Update the
`censored_files` and `clean_files` comprehensions to distinguish sanitized
results from files that were only censored: include a result in `censored_files`
only when `censored` is true and `sanitized` is false, and include sanitized
results in `clean_files` so both in-place and `--output` modes retain them.

Comment thread projects/caliper/engine/file_export/censoring.py Outdated
Comment thread projects/caliper/engine/file_export/censoring.py Outdated
Comment thread projects/caliper/orchestration/censoring.py
Comment thread projects/core/library/export_notifications.py
Comment thread projects/core/library/export_notifications.py Outdated
Comment thread projects/core/library/export.py Outdated
Comment thread projects/core/library/export.py
Comment thread vaults/psap-forge-mlflow-export.yaml Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (3)
projects/core/library/ci.py (2)

249-249: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Don't log the full notification payload.

message is caller-supplied content; Line 248 already records the created path. Drop this line or log a redacted summary.

As per coding guidelines, "Never print, log, or include secret values in error messages."

🤖 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 `@projects/core/library/ci.py` at line 249, Remove the logger.info call that
logs the caller-supplied message payload, preserving the existing path logging
on the preceding line; if logging is required, replace it with a non-sensitive
redacted summary that excludes secret values.

Source: Coding guidelines


172-184: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

reason from exception text is persisted to exit_status.yaml and logged.

Line 184 captures arbitrary exception text (which can embed credentials or URLs with userinfo) and Lines 193-204 write it under the CI metadata dir and log it. Persist only allowlisted reason values and use a generic command_failed for exceptions.

As per coding guidelines, "Never persist secrets or sensitive data ... to ARTIFACT_DIR, logs, or files."

🤖 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 `@projects/core/library/ci.py` around lines 172 - 184, Update the exception
path in the command execution wrapper to stop assigning exception text to
reason; use the generic allowlisted reason "command_failed" instead. Preserve
handle_ci_exception(e) and exit_code = 1, and ensure only allowlisted reason
values reach the existing exit_status.yaml persistence and logging flow.

Source: Coding guidelines

projects/caliper/cli/commands.py (1)

1098-1099: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Sanitized files are still classified as censored.

apply_censoring_to_artifacts sets censored=True for sanitized files too, so the in-place branch (Lines 1175-1176) deletes files that were safely sanitized and --output mode omits them. Only censored and not sanitized (filename-excluded) results should be treated as censored.

🐛 Proposed fix
-    censored_files = [r.file_path for r in censoring_results if r.censored]
-    clean_files = [r.file_path for r in censoring_results if not r.censored]
+    censored_files = [r.file_path for r in censoring_results if r.censored and not r.sanitized]
+    clean_files = [r.file_path for r in censoring_results if not r.censored or r.sanitized]
🤖 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 `@projects/caliper/cli/commands.py` around lines 1098 - 1099, Update the
comprehensions building censored_files and clean_files to distinguish sanitized
results from filename-excluded results: classify a result as censored only when
r.censored is true and r.sanitized is false, while retaining sanitized files in
the clean_files collection so in-place and --output modes preserve them.
🧹 Nitpick comments (7)
projects/core/library/export_notifications.py (1)

107-114: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Don't rebind the artifact_dir parameter. The Slack provider gets env.ARTIFACT_DIR while _build_enhanced_notification used the caller-supplied artifact_dir, so the two channels can describe different directories. Use a distinct local name and prefer the passed value.

-                artifact_dir = Path(env.ARTIFACT_DIR) if env.ARTIFACT_DIR else None
+                context_artifact_dir = artifact_dir or (
+                    Path(env.ARTIFACT_DIR) if env.ARTIFACT_DIR else None
+                )
                 context = NotificationContext(
                     ...
-                    artifact_dir=artifact_dir,
+                    artifact_dir=context_artifact_dir,
                 )
🤖 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 `@projects/core/library/export_notifications.py` around lines 107 - 114, Update
the notification-building flow around _build_enhanced_notification to avoid
rebinding the artifact_dir parameter: store env.ARTIFACT_DIR in a distinct local
variable, then set NotificationContext.artifact_dir to the caller-supplied
artifact_dir when present, falling back to the environment-derived value only
when needed.
projects/caliper/orchestration/export.py (1)

371-377: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Dead first rglob scan.

Line 371's list is discarded and recomputed at Line 377; drop it and collect paths only after censoring.

♻️ Proposed cleanup
-    all_artifact_paths = [p for p in from_path.rglob("*") if p.is_file()]
-
     # Apply censoring for multi-run export if enabled
     censoring_occurred = apply_multi_run_censoring(from_path, export_cfg, disable_censoring)
 
     # Update artifact paths - use original paths since we modified in-place
     all_artifact_paths = [p for p in from_path.rglob("*") if p.is_file()]
🤖 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 `@projects/caliper/orchestration/export.py` around lines 371 - 377, Remove the
initial unused all_artifact_paths rglob scan before apply_multi_run_censoring.
Keep the single artifact-path collection after censoring so paths reflect the
in-place modifications.
projects/caliper/cli/commands.py (1)

1139-1142: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid echoing the whole report back.

report_path.read_text() duplicates the entire report to stdout on every run; the path log is enough, and verbose mode already lists censored files.

♻️ Proposed tweak
-                click.echo(
-                    f"\nCensoring report written to {report_path}\n{report_path.read_text()}"
-                )
+                click.echo(f"\nCensoring report written to {report_path}")
🤖 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 `@projects/caliper/cli/commands.py` around lines 1139 - 1142, Remove the
report_path.read_text() interpolation from the click.echo call in the
report-writing flow, leaving only a concise message containing report_path. Keep
the existing report generation and verbose censored-file output unchanged.
projects/caliper/engine/file_export/censoring_rules.py (1)

53-72: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Remove the duplicate case-insensitive bearer pattern.

KEYWORD_PATTERNS is compiled with re.IGNORECASE, so r"Bearer\s+[A-Za-z0-9+/=]+" already matches lowercase bearer tokens; keep only one case-normalized bearer pattern.

🤖 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 `@projects/caliper/engine/file_export/censoring_rules.py` around lines 53 - 72,
Remove the duplicate case-insensitive bearer-token regex from KEYWORD_PATTERNS,
retaining a single bearer pattern that works with the existing re.IGNORECASE
compilation. Leave the other sensitive-file patterns and compilation logic
unchanged.
projects/caliper/orchestration/censoring.py (2)

293-296: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

export_blocked=True path is now unreachable.

Both call sites pass export_blocked=False, and given the engine's current behavior (see companion comment in projects/caliper/engine/file_export/censoring.py), excluded_files is always empty, so this branch and the censoring_blocked notification can never fire. Root cause and full context noted in the consolidated comment.

Also applies to: 317-334

🤖 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 `@projects/caliper/orchestration/censoring.py` around lines 293 - 296, Remove
the unused export_blocked parameter and unreachable censoring_blocked
notification branch from _create_censoring_notification, then update both call
sites to use the simplified signature. Preserve the remaining censoring
notification behavior for actual censoring results.

108-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Near-duplicate implementations of the single-run and multi-run censoring flows.

apply_orchestration_censoring and apply_multi_run_censoring are copy-pasted except for log wording. Extracting a shared _apply_censoring(from_path, export_cfg, disable_censoring, label) helper would cut the duplication and avoid the two copies drifting (e.g. a future bugfix landing in only one of them).

♻️ Sketch
-def apply_orchestration_censoring(from_path, export_cfg, disable_censoring=False) -> bool:
-    ...
-def apply_multi_run_censoring(from_path, export_cfg, disable_censoring=False) -> bool:
-    ...
+def _apply_censoring(from_path, export_cfg, disable_censoring, label="") -> bool:
+    if disable_censoring:
+        ...
+    vault_secrets, secret_mapping = discover_vault_secrets(verbose=export_cfg.verbose)
+    all_artifact_paths = [p for p in from_path.rglob("*") if p.is_file()]
+    ...
+
+def apply_orchestration_censoring(from_path, export_cfg, disable_censoring=False) -> bool:
+    return _apply_censoring(from_path, export_cfg, disable_censoring, label="")
+
+def apply_multi_run_censoring(from_path, export_cfg, disable_censoring=False) -> bool:
+    return _apply_censoring(from_path, export_cfg, disable_censoring, label="multi-run ")

Also applies to: 178-244

🤖 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 `@projects/caliper/orchestration/censoring.py` around lines 108 - 176, Extract
the shared censoring workflow from apply_orchestration_censoring and
apply_multi_run_censoring into a helper named _apply_censoring accepting
from_path, export_cfg, disable_censoring, and a label for flow-specific log
wording. Move the common discovery, processing, result classification,
reporting, notification, and return-value logic into that helper, then reduce
both public functions to delegate to it with their appropriate labels while
preserving existing behavior.
projects/caliper/engine/file_export/censoring.py (1)

115-131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

"Excluded" file semantics are now unreachable — everything gets sanitized-in-place instead.

Every branch that sets censored=True here also sets sanitized=True (this filename branch, and the keyword/vault branch below). No code path ever returns censored=True, sanitized=False. That makes CensoringResult.__str__'s EXCLUDED case, the module docstring's "Excluding files with sensitive filename patterns" (line 251), and downstream orchestration logic in projects/caliper/orchestration/censoring.py that branches on excluded_files/export_blocked all dead/unreachable. Either update the docs/messaging to reflect "always sanitize in place," or reintroduce a genuine exclusion path if that's still the intended UX for unsanitizable sensitive files.

🤖 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 `@projects/caliper/engine/file_export/censoring.py` around lines 115 - 131,
Restore a genuine exclusion path in _sanitize_file_content for sensitive files
that cannot be safely sanitized, returning censored=True with sanitized=False so
CensoringResult.__str__ and orchestration excluded_files/export_blocked handling
remain meaningful. Preserve in-place sanitization for files that can be safely
rewritten, and update the module documentation only if the intended behavior is
instead always-sanitize.
🤖 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 `@projects/caliper/engine/file_export/censoring.py`:
- Around line 144-181: Run vault-secret scanning independently of the
keyword-pattern branch in the censoring flow. Keep keyword matches redacted and
their reasons recorded, then always iterate over self.vault_secrets to replace
detected values, set sanitized, and append the vault identifier reason; remove
the conditional that skips this scan when keyword_detected is true.
- Around line 144-172: Update the keyword-redaction logic around
COMPILED_KEYWORD_PATTERNS to collect all matches in a single scan, track matched
pattern indices, and merge overlapping or adjacent spans before redacting them
in reverse order. Remove the redundant keyword_detected pass and rename the
unused pattern_idx in the reasons loop to an underscore. Preserve recording
reasons for every pattern that matched and set sanitized when merged matches are
redacted.

In `@projects/caliper/orchestration/export.py`:
- Around line 420-436: Update the disable_file_export branch that builds ml_meta
for FileExportBackendResult so it does not populate fake MLflow experiment_url,
run_url, or tracking_uri values. Omit those URL keys or mark them disabled,
while preserving the mock run_id and successful result behavior for
notifications.
- Around line 301-308: Update run_from_orchestration_config to return dict[str,
Any] and guard the status.yaml read for multi-run dry-run execution, returning
an appropriate status without opening the file when no export writes it;
preserve the existing status loading and single-run censoring behavior for
normal executions.

In `@projects/core/library/export_notifications.py`:
- Around line 259-275: Update _get_execution_engine_config to use an explicit
allowlist of approved non-sensitive cluster configuration keys instead of
iterating over every cluster_config entry. Include only allowlisted display
values in the notification, ignore all other keys—including future
credential-like fields—and preserve the existing formatting and None behavior
when no approved values are present.
- Around line 8-13: Add the missing re import alongside the existing
standard-library imports so _process_notification_files can call re.sub without
raising NameError or failing lint.

---

Duplicate comments:
In `@projects/caliper/cli/commands.py`:
- Around line 1098-1099: Update the comprehensions building censored_files and
clean_files to distinguish sanitized results from filename-excluded results:
classify a result as censored only when r.censored is true and r.sanitized is
false, while retaining sanitized files in the clean_files collection so in-place
and --output modes preserve them.

In `@projects/core/library/ci.py`:
- Line 249: Remove the logger.info call that logs the caller-supplied message
payload, preserving the existing path logging on the preceding line; if logging
is required, replace it with a non-sensitive redacted summary that excludes
secret values.
- Around line 172-184: Update the exception path in the command execution
wrapper to stop assigning exception text to reason; use the generic allowlisted
reason "command_failed" instead. Preserve handle_ci_exception(e) and exit_code =
1, and ensure only allowlisted reason values reach the existing exit_status.yaml
persistence and logging flow.

---

Nitpick comments:
In `@projects/caliper/cli/commands.py`:
- Around line 1139-1142: Remove the report_path.read_text() interpolation from
the click.echo call in the report-writing flow, leaving only a concise message
containing report_path. Keep the existing report generation and verbose
censored-file output unchanged.

In `@projects/caliper/engine/file_export/censoring_rules.py`:
- Around line 53-72: Remove the duplicate case-insensitive bearer-token regex
from KEYWORD_PATTERNS, retaining a single bearer pattern that works with the
existing re.IGNORECASE compilation. Leave the other sensitive-file patterns and
compilation logic unchanged.

In `@projects/caliper/engine/file_export/censoring.py`:
- Around line 115-131: Restore a genuine exclusion path in
_sanitize_file_content for sensitive files that cannot be safely sanitized,
returning censored=True with sanitized=False so CensoringResult.__str__ and
orchestration excluded_files/export_blocked handling remain meaningful. Preserve
in-place sanitization for files that can be safely rewritten, and update the
module documentation only if the intended behavior is instead always-sanitize.

In `@projects/caliper/orchestration/censoring.py`:
- Around line 293-296: Remove the unused export_blocked parameter and
unreachable censoring_blocked notification branch from
_create_censoring_notification, then update both call sites to use the
simplified signature. Preserve the remaining censoring notification behavior for
actual censoring results.
- Around line 108-176: Extract the shared censoring workflow from
apply_orchestration_censoring and apply_multi_run_censoring into a helper named
_apply_censoring accepting from_path, export_cfg, disable_censoring, and a label
for flow-specific log wording. Move the common discovery, processing, result
classification, reporting, notification, and return-value logic into that
helper, then reduce both public functions to delegate to it with their
appropriate labels while preserving existing behavior.

In `@projects/caliper/orchestration/export.py`:
- Around line 371-377: Remove the initial unused all_artifact_paths rglob scan
before apply_multi_run_censoring. Keep the single artifact-path collection after
censoring so paths reflect the in-place modifications.

In `@projects/core/library/export_notifications.py`:
- Around line 107-114: Update the notification-building flow around
_build_enhanced_notification to avoid rebinding the artifact_dir parameter:
store env.ARTIFACT_DIR in a distinct local variable, then set
NotificationContext.artifact_dir to the caller-supplied artifact_dir when
present, falling back to the environment-derived value only when needed.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 83754375-4267-400d-ac40-38a25484f8ae

📥 Commits

Reviewing files that changed from the base of the PR and between 553125e and ab1ce91.

📒 Files selected for processing (12)
  • projects/caliper/cli/commands.py
  • projects/caliper/cli/main.py
  • projects/caliper/engine/file_export/censoring.py
  • projects/caliper/engine/file_export/censoring_rules.py
  • projects/caliper/orchestration/censoring.py
  • projects/caliper/orchestration/export.py
  • projects/core/ci_entrypoint/prepare_ci.py
  • projects/core/library/ci.py
  • projects/core/library/export.py
  • projects/core/library/export_notifications.py
  • projects/core/library/vault.py
  • vaults/psap-forge-mlflow-export.yaml
🚧 Files skipped from review as they are similar to previous changes (5)
  • projects/core/ci_entrypoint/prepare_ci.py
  • vaults/psap-forge-mlflow-export.yaml
  • projects/core/library/vault.py
  • projects/caliper/cli/main.py
  • projects/core/library/export.py

Comment thread projects/caliper/engine/file_export/censoring.py
Comment thread projects/caliper/engine/file_export/censoring.py
Comment thread projects/caliper/orchestration/export.py
Comment thread projects/caliper/orchestration/export.py
Comment thread projects/core/library/export_notifications.py
Comment thread projects/core/library/export_notifications.py
@kpouget

kpouget commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@psap-forge-bot

Copy link
Copy Markdown

❌ Execution of skeleton


Artifact Links


Step details

00__preflight (🟡 1W)

01__test (🟡 3W)

  • smokeNone
  • loadNone
    002__postprocessing: ✅ parse:✅ visualize:✅ artifacts_to_kpis:✅ kpis_to_csv:⚪ artifacts_to_ai_data:✅ s3_import:✅ analyse_kpis:✅ s3_export:✅

02__export-artifacts (🟡 1W)

lost+found


@psap-forge-bot

Copy link
Copy Markdown

@kpouget

kpouget commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@psap-forge-bot

Copy link
Copy Markdown
🔴 Submission of skeleton failed after 17 seconds 🔴

Error: FournosJobFailureError: FOURNOS Job 'forge-skeleton-20260730-082750' failed: Resolution failed: Job has reached the specified backoff limit

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@kpouget

kpouget commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@psap-forge-bot

Copy link
Copy Markdown
🔴 Submission of skeleton failed after 1 minute, 23 seconds 🔴

Error: FournosJobFailureError: FOURNOS Job 'forge-skeleton-20260730-091406' failed: Resolution failed: Job has reached the specified backoff limit

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

♻️ Duplicate comments (7)
projects/core/library/ci.py (2)

249-249: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the raw notification-payload log.

message is caller-provided notification content and may contain sensitive data. Keep the created-path log, but do not log the payload.

🤖 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 `@projects/core/library/ci.py` at line 249, Remove the logger.info call that
interpolates the caller-provided message in the notification flow, while
preserving the existing log indicating the notification was created.

Source: Coding guidelines


172-204: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not persist or log arbitrary exit reasons.

reason can be caller-controlled or str(e), which may contain credentials. It is written under ARTIFACT_DIR and logged. Allowlist safe reason codes; use command_failed for exceptions and avoid logging raw reasons.

🤖 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 `@projects/core/library/ci.py` around lines 172 - 204, Restrict exit reasons in
the command execution and status-persistence flow to safe allowlisted codes:
assign “command_failed” in the exception handler instead of str(e), and only
persist or log recognized safe reason values. Update the logic around
handle_ci_exception, exit_status_data, and the reason-based logger calls so
arbitrary caller-controlled or exception text is never written or logged.

Source: Coding guidelines

projects/caliper/engine/file_export/censoring.py (1)

144-172: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Overlapping matches from different patterns still corrupt the redacted output, and the scan runs twice.

Spans are collected against the original content (Lines 156-159) then applied by slicing in reverse start order; overlapping spans from two patterns (e.g. token\s*[:=]\s*\S+ and access[_-]?token\s*[:=]\s*\S+) leave stale end offsets that garble text or leak part of the match. Merge spans before redacting, and drop the redundant detection pass at Lines 145-151 (which also triggers Ruff B007 on the unused pattern_idx at Line 165).

🐛 Single-pass merge-then-redact
-            keyword_detected = False
-            matched_patterns = set()
-            for i, pattern in enumerate(COMPILED_KEYWORD_PATTERNS):
-                matches = list(pattern.finditer(content))
-                if matches:
-                    keyword_detected = True
-                    matched_patterns.add(i)
-
-            if keyword_detected:
-                # Replace all matched spans with redacted text, preserving other content
-                # Process patterns in reverse order by position to maintain string indices
-                all_matches = []
-                for i, pattern in enumerate(COMPILED_KEYWORD_PATTERNS):
-                    for match in pattern.finditer(content):
-                        all_matches.append((match.start(), match.end(), i))
-
-                # Sort by start position in reverse order
-                all_matches.sort(reverse=True)
-
-                # Replace each match with [REDACTED]
-                for start, end, pattern_idx in all_matches:
-                    content = content[:start] + "[REDACTED]" + content[end:]
-
-                # Add reasons for all matched patterns
+            spans = []
+            matched_patterns = set()
+            for i, pattern in enumerate(COMPILED_KEYWORD_PATTERNS):
+                for match in pattern.finditer(content):
+                    spans.append((match.start(), match.end()))
+                    matched_patterns.add(i)
+
+            if spans:
+                spans.sort()
+                merged = []
+                for start, end in spans:
+                    if merged and start <= merged[-1][1]:
+                        merged[-1] = (merged[-1][0], max(merged[-1][1], end))
+                    else:
+                        merged.append((start, end))
+
+                for start, end in reversed(merged):
+                    content = content[:start] + "[REDACTED]" + content[end:]
+
                 for pattern_idx in sorted(matched_patterns):
                     reasons.append(f"contains keyword pattern: {KEYWORD_PATTERNS[pattern_idx]}")
 
                 sanitized = True
🤖 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 `@projects/caliper/engine/file_export/censoring.py` around lines 144 - 172,
Update the keyword-processing block around COMPILED_KEYWORD_PATTERNS to collect
matches and matched pattern indices in one pass, then merge overlapping or
adjacent spans before redacting. Apply each merged span against the original
content in reverse order, preserve reasons for every matched pattern, and remove
the redundant detection loop and unused pattern_idx handling.

Source: Pipeline failures

projects/caliper/cli/commands.py (1)

1097-1099: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Sanitized files are still classified as censored, so they get deleted or dropped.

_sanitize_file_content returns censored=True, sanitized=True for files it repaired in place (projects/caliper/engine/file_export/censoring.py Lines 129-131, 198). Consequently the in-place branch unlink()s them (Line 1176) even though they are now safe, and --output mode omits them from the copy. Only censored and not sanitized results should be excluded — or simply use the filtered_paths allow-list the engine already returns (currently unused).

🐛 Proposed fix
-    # Separate censored and clean files
-    censored_files = [r.file_path for r in censoring_results if r.censored]
-    clean_files = [r.file_path for r in censoring_results if not r.censored]
+    # Files excluded outright vs files safe to export (clean or sanitized in place)
+    censored_files = [r.file_path for r in censoring_results if r.censored and not r.sanitized]
+    clean_files = [r.file_path for r in censoring_results if not r.censored or r.sanitized]
🤖 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 `@projects/caliper/cli/commands.py` around lines 1097 - 1099, Update the file
classification around censoring_results so repaired files with censored=True and
sanitized=True remain eligible for output. Exclude only results that are
censored and not sanitized, or reuse the engine’s filtered_paths allow-list
consistently in both the in-place unlink flow and the --output copy flow.
projects/caliper/orchestration/export.py (2)

419-435: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fake MLflow URLs leak into notifications. With --disable-file-export, http://DRY_RUN_MLFLOW_FAKE_URL/... values are written into status.yaml and rendered as clickable artifact links. Omit the URL keys (or mark them disabled) so consumers don't surface bogus links.

🤖 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 `@projects/caliper/orchestration/export.py` around lines 419 - 435, The mock
results created in the disable_file_export branch must not expose fake MLflow
links. Update the result metadata in the disable_file_export handling to omit
the experiment_url, run_url, and tracking_uri keys (or mark them disabled) while
preserving the mock run identifier and successful status.

300-307: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Multi-run dry-run still falls through to reading a non-existent status.yaml. When len(run_dirs) > 1 and export_cfg.dry_run is set, nothing writes status_yaml, yet Line 300 opens it unconditionally → FileNotFoundError.

🐛 Suggested guard
+    if not status_yaml.exists():
+        logger.info("No status YAML produced (dry-run); returning empty status")
+        return {}
+
     with open(status_yaml) as f:
-        status = yaml.safe_load(f.read())
+        status = yaml.safe_load(f.read()) or {}
🤖 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 `@projects/caliper/orchestration/export.py` around lines 300 - 307, Guard the
status_yaml read in the export flow so multi-run dry runs do not open a file
that was never created. Use the existing run_dirs length and export_cfg.dry_run
conditions to return an appropriate status before the unconditional open, while
preserving status loading and censoring information for normal and single-run
executions.
projects/core/library/export_notifications.py (1)

320-331: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Broken artifact URL and missing heading without MLflow. mlflow_run_url already ends in /artifacts (possibly with ?workspace=... and a # fragment), so appending /artifacts/{step}/run.log yields e.g. .../artifacts?workspace=x/artifacts/<step>/run.log. Reuse _create_mlflow_url (Line 832) instead. Also, when mlflow_run_url is None no heading is emitted while the step body lines still are, producing orphaned content.

🐛 Suggested fix
-        mlflow_log_url = (
-            f"{mlflow_run_url}/artifacts/{step_name}/run.log" if mlflow_run_url else "#"
-        )
-        if mlflow_run_url:
-            step_title = f"#### {exit_status_emoji} [{step_name}]({mlflow_log_url}){log_summary}"
-            step_status.append(step_title)
+        mlflow_log_url = _create_mlflow_url(mlflow_run_url, step_name) if mlflow_run_url else None
+        if mlflow_log_url:
+            step_status.append(
+                f"#### {exit_status_emoji} [{step_name}]({mlflow_log_url}){log_summary}"
+            )
+        else:
+            step_status.append(f"#### {exit_status_emoji} {step_name}{log_summary}")
🤖 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 `@projects/core/library/export_notifications.py` around lines 320 - 331, Update
the notification step formatting around mlflow_log_url and step_title: build the
log link with the existing _create_mlflow_url helper so artifact URLs preserve
query parameters and fragments, and always append a step heading even when
mlflow_run_url is None. Keep the existing step body processing via
_process_notification_files and _process_step_details unchanged.
🧹 Nitpick comments (8)
projects/caliper/orchestration/censoring.py (1)

23-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

VAULT_CONTENT_TYPES is dead code.

discover_vault_secrets now gates on content_def.is_sensible and never consults this list; leaving it invites the wrong assumption that content-name filtering is still applied.

🤖 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 `@projects/caliper/orchestration/censoring.py` around lines 23 - 38, Remove the
unused VAULT_CONTENT_TYPES constant from the censoring module, and ensure
discover_vault_secrets continues to rely solely on content_def.is_sensible for
discovery without introducing another content-name filter.
projects/caliper/engine/file_export/censoring.py (1)

175-180: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Vault-secret substring scan is O(files × secrets × size) with repeated .strip().

Pre-normalize the secret set once in __init__ (and drop empty entries there) so this hot loop does a single in/replace per secret. Also consider that secret_mapping keys are already stripped upstream in discover_vault_secrets.

🤖 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 `@projects/caliper/engine/file_export/censoring.py` around lines 175 - 180,
Pre-normalize non-empty, stripped vault secrets once in __init__ and retain the
normalized collection for censoring. Update the loop in the secret-scanning
method to iterate over those normalized values without repeated strip calls,
while using the already-stripped keys in secret_mapping for vault identifiers.
projects/caliper/cli/commands.py (1)

1011-1016: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

--mlflow-insecure-tls can only be turned on, never off.

mlflow_insecure_tls or config.get("insecure_tls", False) means a config file with insecure_tls: true wins even when the flag is absent, contradicting the "CLI args override config file values" comment. Same shape as the other final_* lines, but here the insecure default is the risky direction.

🤖 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 `@projects/caliper/cli/commands.py` around lines 1011 - 1016, Update the
final_insecure_tls resolution in the CLI configuration flow so an explicitly
provided --mlflow-insecure-tls value overrides config.get("insecure_tls",
False), including an explicit false value, while retaining the config value only
when the CLI option is absent. Keep the existing final_* resolution behavior
unchanged.
projects/caliper/engine/file_export/censoring_rules.py (1)

88-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused find_sensitive_content_in_text helper.

No code consumes it, and censoring.py already implements keyword detection directly, so keeping it only adds an unused public API surface.

🤖 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 `@projects/caliper/engine/file_export/censoring_rules.py` around lines 88 -
104, Remove the unused find_sensitive_content_in_text helper and its associated
implementation, leaving keyword detection in censoring.py as the sole mechanism.
projects/core/library/export.py (2)

26-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Importing underscore-private helpers across modules. _check_job_shutdown_status and _create_mlflow_file_url_for_step are consumed outside their defining module; promote them to public names in export_notifications to make the boundary explicit.

🤖 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 `@projects/core/library/export.py` around lines 26 - 30, Rename the
cross-module helpers _check_job_shutdown_status and
_create_mlflow_file_url_for_step to public names in export_notifications, then
update their definitions and all imports and call sites, including export.py, to
use the new names while leaving send_notification unchanged.

183-185: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

artifact_dir is required but unused. The body relies entirely on caliper.export.from being set by the caller. Either set the config from this parameter here (making the function self-contained) or drop the parameter.

♻️ Suggested option
 def run_caliper_orchestration_export(
     *, artifact_dir: Path, disable_censoring: bool = False, disable_file_export: bool = False
 ):
+    config.project.set_config("caliper.export.from", str(artifact_dir), print=False)
🤖 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 `@projects/core/library/export.py` around lines 183 - 185, Update
run_caliper_orchestration_export so artifact_dir is not an unused required
parameter: either apply it to the caliper.export.from configuration within the
function, making the export self-contained, or remove the parameter and update
its callers; preserve the existing export behavior.
projects/caliper/orchestration/export.py (2)

372-377: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Duplicate full-tree rglob. Line 370 already walked the whole artifact tree and its result is discarded here; only the post-censoring listing is used. Drop the earlier computation (or move it below censoring) to avoid the redundant recursive scan.

♻️ Suggested cleanup
-    all_artifact_paths = [p for p in from_path.rglob("*") if p.is_file()]
-
     # Apply censoring for multi-run export if enabled
     censoring_occurred = apply_orchestration_censoring(from_path, export_cfg, disable_censoring)
 
-    # Update artifact paths - use original paths since we modified in-place
+    # Collect artifact paths after in-place censoring
     all_artifact_paths = [p for p in from_path.rglob("*") if p.is_file()]
🤖 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 `@projects/caliper/orchestration/export.py` around lines 372 - 377, Remove the
earlier full-tree artifact path computation in the export flow and retain a
single recursive listing after apply_orchestration_censoring. Ensure
all_artifact_paths is populated from the post-censoring tree so the updated
in-place artifacts are used without a redundant scan.

471-486: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Read-modify-rewrite of status.yaml is fragile. yaml.safe_load can return None (or raise yaml.YAMLError) — neither is covered by the except OSError, so status_data["censoring_occurred"] = ... would raise TypeError and escape as an unclassified failure. Also the rewrite drops the sort_keys=False / block-style formatting used by write_artifacts_status_yaml.

♻️ Suggested hardening
         try:
             write_artifacts_status_yaml(status_yaml, results)
 
             # Add censoring information to the status file
             with open(status_yaml) as f:
-                status_data = yaml.safe_load(f)
+                status_data = yaml.safe_load(f) or {}
             status_data["censoring_occurred"] = censoring_occurred
             with open(status_yaml, "w") as f:
-                yaml.dump(status_data, f, indent=4)
+                yaml.safe_dump(status_data, f, default_flow_style=False, sort_keys=False)
 
             if not disable_file_export:
                 click.echo(f"Wrote status YAML to {status_yaml}")
-        except OSError as e:
+        except (OSError, yaml.YAMLError) as e:
🤖 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 `@projects/caliper/orchestration/export.py` around lines 471 - 486, Harden the
status YAML update in the status_yaml export flow around
write_artifacts_status_yaml: handle empty or non-mapping safe_load results and
yaml.YAMLError alongside OSError so failures become ExportFailedException with
the existing error reporting. Preserve the formatting established by
write_artifacts_status_yaml by rewriting with sort_keys=False and block-style
output, or reuse that writer while retaining the censoring_occurred field.
🤖 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 `@projects/caliper/engine/file_export/censoring_rules.py`:
- Around line 53-69: The SENSITIVE_FILE_PATTERNS substring rules for “secret”,
“credential”, and “password” currently match ancestor directory names and can
overwrite unrelated files. Update matches_sensitive_filename to apply these
filename-based rules to Path(filename).name, while preserving full-path matching
only for path-dependent .ssh patterns and existing exact-extension/key-name
rules.

In `@projects/caliper/engine/file_export/censoring.py`:
- Around line 100-110: Update the file-content sampling logic in the shown try
block to open file_path and read at most the first 1024 bytes, rather than
calling read_bytes() on the entire file. Preserve the existing empty-file
handling, printable_ratio calculation, and return threshold while ensuring
multi-GB files are never fully loaded into memory.
- Around line 187-195: Import the stat module used by the PermissionError
fallback in the file export logic, so the chmod expression in the read-only
handling block can resolve stat.S_IWUSR and retry writing successfully.

In `@projects/caliper/orchestration/censoring.py`:
- Around line 102-105: Update discover_vault_secrets so failures during expected
vault discovery are not converted into empty results: propagate the exception,
or return an explicit discovery_failed result that orchestration_apply_censoring
treats as fatal before exporting files. Preserve normal empty-secret behavior
only when vault discovery was not expected or completed successfully.
- Around line 108-112: Align the censoring function name between the definition
in orchestration_apply_censoring and the importer/call in
run_from_orchestration_config; rename the exported function or update the import
and invocation so both files consistently use apply_orchestration_censoring with
the existing arguments and return behavior.

In `@projects/caliper/orchestration/export.py`:
- Around line 166-170: Update the return annotation of
run_from_orchestration_config to match its actual parsed status-mapping return
value, using the appropriate dictionary type rather than int. Leave the
function’s runtime behavior and downstream export handling unchanged.
- Around line 271-282: Update the disable_file_export mock status written by the
export flow to match the schema produced by write_artifacts_status_yaml: wrap
export metadata under caliper_artifacts_export, include its version, and
represent the mlflow backend with the expected status/detail and run information
fields. Preserve the existing disabled-export success semantics and
censoring/duration values so _extract_artifact_links and _update_final_artifacts
can consume the mock status.

In `@projects/core/library/config.py`:
- Around line 194-214: The configuration override reporting in
projects/core/library/config.py lines 194-214 must stop interpolating raw values
into logger output and file_messages; report the key with the established safe
redaction and remove the actual_value lookup used only for formatting. Apply the
same key-or-preset-name plus safe-redaction reporting to
projects/core/library/config.py lines 240-250, ensuring both affected paths
protect secrets in logs and ARTIFACT_DIR/presets_applied.txt.
- Around line 242-250: Update the preset-processing flow around set_config() so
each message is appended to preset_messages only after its corresponding
configuration save succeeds. Move the batched presets_applied.txt write into a
finally block that always flushes accumulated successful messages, including
when a later set_config() call raises.

In `@projects/core/library/export_notifications.py`:
- Around line 436-445: Update the notification setup around get_file_link so
format_postprocess_status_notification receives None when mlflow_run_url is
unset, rather than a callback that returns None. Only create and pass the
file-link closure when mlflow_run_url is available, while preserving the
existing _create_mlflow_file_url_for_step behavior.
- Around line 250-266: Update _get_execution_engine_config to use an explicit
allowlist of approved non-sensitive cluster configuration keys, and only include
values whose keys are on that allowlist. Stop iterating and publishing every
entry from cluster_config, while preserving the existing formatting and None
behavior when no approved values are available.
- Around line 8-18: Add a typing-only import for StepStatus under TYPE_CHECKING
in export_notifications.py, ensuring the annotation references at the affected
functions resolve for Ruff without introducing a runtime dependency. Update the
typing import alongside the existing imports and preserve the function-body
imports.
- Around line 940-952: Remove the unused _process_step_status function,
including its call to _process_step_details and the invalid
test_labels_info/postprocess_info mapping access. Do not alter other
step-processing paths.
- Around line 107-114: Stop rebinding the caller-resolved artifact_dir in the
NotificationContext construction; use a separate local for env.ARTIFACT_DIR if
that value is needed, while keeping artifact_dir unchanged so Slack reports the
same directory used to build the GitHub body.

In `@projects/core/library/export.py`:
- Around line 368-377: The censoring handler in run_from_orchestration_config
discards the previously written export status and its artifact links. Preserve
or reload the existing status payload when handling CensoringOccurredException,
then merge success_with_censoring, censoring_occurred, and the exception message
without removing fields such as caliper_artifacts_export, run_url, or
experiment_url.

---

Duplicate comments:
In `@projects/caliper/cli/commands.py`:
- Around line 1097-1099: Update the file classification around censoring_results
so repaired files with censored=True and sanitized=True remain eligible for
output. Exclude only results that are censored and not sanitized, or reuse the
engine’s filtered_paths allow-list consistently in both the in-place unlink flow
and the --output copy flow.

In `@projects/caliper/engine/file_export/censoring.py`:
- Around line 144-172: Update the keyword-processing block around
COMPILED_KEYWORD_PATTERNS to collect matches and matched pattern indices in one
pass, then merge overlapping or adjacent spans before redacting. Apply each
merged span against the original content in reverse order, preserve reasons for
every matched pattern, and remove the redundant detection loop and unused
pattern_idx handling.

In `@projects/caliper/orchestration/export.py`:
- Around line 419-435: The mock results created in the disable_file_export
branch must not expose fake MLflow links. Update the result metadata in the
disable_file_export handling to omit the experiment_url, run_url, and
tracking_uri keys (or mark them disabled) while preserving the mock run
identifier and successful status.
- Around line 300-307: Guard the status_yaml read in the export flow so
multi-run dry runs do not open a file that was never created. Use the existing
run_dirs length and export_cfg.dry_run conditions to return an appropriate
status before the unconditional open, while preserving status loading and
censoring information for normal and single-run executions.

In `@projects/core/library/ci.py`:
- Line 249: Remove the logger.info call that interpolates the caller-provided
message in the notification flow, while preserving the existing log indicating
the notification was created.
- Around line 172-204: Restrict exit reasons in the command execution and
status-persistence flow to safe allowlisted codes: assign “command_failed” in
the exception handler instead of str(e), and only persist or log recognized safe
reason values. Update the logic around handle_ci_exception, exit_status_data,
and the reason-based logger calls so arbitrary caller-controlled or exception
text is never written or logged.

In `@projects/core/library/export_notifications.py`:
- Around line 320-331: Update the notification step formatting around
mlflow_log_url and step_title: build the log link with the existing
_create_mlflow_url helper so artifact URLs preserve query parameters and
fragments, and always append a step heading even when mlflow_run_url is None.
Keep the existing step body processing via _process_notification_files and
_process_step_details unchanged.

---

Nitpick comments:
In `@projects/caliper/cli/commands.py`:
- Around line 1011-1016: Update the final_insecure_tls resolution in the CLI
configuration flow so an explicitly provided --mlflow-insecure-tls value
overrides config.get("insecure_tls", False), including an explicit false value,
while retaining the config value only when the CLI option is absent. Keep the
existing final_* resolution behavior unchanged.

In `@projects/caliper/engine/file_export/censoring_rules.py`:
- Around line 88-104: Remove the unused find_sensitive_content_in_text helper
and its associated implementation, leaving keyword detection in censoring.py as
the sole mechanism.

In `@projects/caliper/engine/file_export/censoring.py`:
- Around line 175-180: Pre-normalize non-empty, stripped vault secrets once in
__init__ and retain the normalized collection for censoring. Update the loop in
the secret-scanning method to iterate over those normalized values without
repeated strip calls, while using the already-stripped keys in secret_mapping
for vault identifiers.

In `@projects/caliper/orchestration/censoring.py`:
- Around line 23-38: Remove the unused VAULT_CONTENT_TYPES constant from the
censoring module, and ensure discover_vault_secrets continues to rely solely on
content_def.is_sensible for discovery without introducing another content-name
filter.

In `@projects/caliper/orchestration/export.py`:
- Around line 372-377: Remove the earlier full-tree artifact path computation in
the export flow and retain a single recursive listing after
apply_orchestration_censoring. Ensure all_artifact_paths is populated from the
post-censoring tree so the updated in-place artifacts are used without a
redundant scan.
- Around line 471-486: Harden the status YAML update in the status_yaml export
flow around write_artifacts_status_yaml: handle empty or non-mapping safe_load
results and yaml.YAMLError alongside OSError so failures become
ExportFailedException with the existing error reporting. Preserve the formatting
established by write_artifacts_status_yaml by rewriting with sort_keys=False and
block-style output, or reuse that writer while retaining the censoring_occurred
field.

In `@projects/core/library/export.py`:
- Around line 26-30: Rename the cross-module helpers _check_job_shutdown_status
and _create_mlflow_file_url_for_step to public names in export_notifications,
then update their definitions and all imports and call sites, including
export.py, to use the new names while leaving send_notification unchanged.
- Around line 183-185: Update run_caliper_orchestration_export so artifact_dir
is not an unused required parameter: either apply it to the caliper.export.from
configuration within the function, making the export self-contained, or remove
the parameter and update its callers; preserve the existing export behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 967c4184-a676-4a7a-a9d2-6f0acefd97e2

📥 Commits

Reviewing files that changed from the base of the PR and between ab1ce91 and 7c131b9.

📒 Files selected for processing (14)
  • projects/caliper/cli/commands.py
  • projects/caliper/cli/main.py
  • projects/caliper/engine/file_export/censoring.py
  • projects/caliper/engine/file_export/censoring_rules.py
  • projects/caliper/orchestration/censoring.py
  • projects/caliper/orchestration/export.py
  • projects/caliper/orchestration/notification.py
  • projects/core/ci_entrypoint/prepare_ci.py
  • projects/core/library/ci.py
  • projects/core/library/config.py
  • projects/core/library/export.py
  • projects/core/library/export_notifications.py
  • projects/core/library/vault.py
  • vaults/psap-forge-mlflow-export.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • projects/core/ci_entrypoint/prepare_ci.py

Comment on lines +53 to +69
SENSITIVE_FILE_PATTERNS = [
r".*\.pem$", # PEM certificate files
r".*\.key$", # Private key files
r".*\.p12$", # PKCS#12 certificate files
r".*\.pfx$", # PKCS#12 certificate files (Windows)
r".*secret.*", # Any file with "secret" in the name
r".*credential.*", # Any file with "credential" in the name
r".*password.*", # Any file with "password" in the name
r".*\.ssh/.*", # SSH directory contents
r".*/\.ssh/.*", # SSH directory contents (with path)
r".*id_rsa.*", # SSH private keys
r".*id_dsa.*", # DSA private keys
r".*id_ecdsa.*", # ECDSA private keys
r".*id_ed25519.*", # Ed25519 private keys
r".*\.env$", # Environment files
r".*\.env\..*", # Environment files with suffixes
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Path-wide substring patterns will wipe every file under a directory whose name contains secret/credential/password.

matches_sensitive_filename is called with str(file_path) (projects/caliper/engine/file_export/censoring.py Line 119), and the whole matched file is then overwritten in place with a one-line placeholder. Patterns such as .*secret.*, .*credential.* and .*password.* therefore match on any ancestor directory component — e.g. artifacts/mlflow-secret-config/run.log or a step dir named create_password_secret — and irrecoverably destroy unrelated artifacts. Consider matching these substring rules against Path(filename).name only, or keeping the full-path form solely for the .ssh/ rules.

🤖 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 `@projects/caliper/engine/file_export/censoring_rules.py` around lines 53 - 69,
The SENSITIVE_FILE_PATTERNS substring rules for “secret”, “credential”, and
“password” currently match ancestor directory names and can overwrite unrelated
files. Update matches_sensitive_filename to apply these filename-based rules to
Path(filename).name, while preserving full-path matching only for path-dependent
.ssh patterns and existing exact-extension/key-name rules.

Comment on lines +100 to +110
try:
# Read first 1KB and check if it's mostly printable
sample = file_path.read_bytes()[:1024]
if not sample:
return True # Empty file is text

# Check if most bytes are printable ASCII or common UTF-8
printable_ratio = sum(1 for b in sample if 32 <= b <= 126 or b in (9, 10, 13)) / len(
sample
)
return printable_ratio > 0.7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

read_bytes()[:1024] loads the entire file into memory.

Every extension-less artifact (including multi-GB tarballs or core dumps in a CI artifact tree) is fully read just to sample 1 KB. Open and read a bounded chunk instead.

♻️ Proposed fix
-            sample = file_path.read_bytes()[:1024]
+            with file_path.open("rb") as fh:
+                sample = fh.read(1024)
📝 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.

Suggested change
try:
# Read first 1KB and check if it's mostly printable
sample = file_path.read_bytes()[:1024]
if not sample:
return True # Empty file is text
# Check if most bytes are printable ASCII or common UTF-8
printable_ratio = sum(1 for b in sample if 32 <= b <= 126 or b in (9, 10, 13)) / len(
sample
)
return printable_ratio > 0.7
try:
# Read first 1KB and check if it's mostly printable
with file_path.open("rb") as fh:
sample = fh.read(1024)
if not sample:
return True # Empty file is text
# Check if most bytes are printable ASCII or common UTF-8
printable_ratio = sum(1 for b in sample if 32 <= b <= 126 or b in (9, 10, 13)) / len(
sample
)
return printable_ratio > 0.7
🤖 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 `@projects/caliper/engine/file_export/censoring.py` around lines 100 - 110,
Update the file-content sampling logic in the shown try block to open file_path
and read at most the first 1024 bytes, rather than calling read_bytes() on the
entire file. Preserve the existing empty-file handling, printable_ratio
calculation, and return threshold while ensuring multi-GB files are never fully
loaded into memory.

Comment thread projects/caliper/engine/file_export/censoring.py
Comment on lines +102 to +105
except Exception as e:
logger.warning(f"Failed to discover vault secrets: {e}")

return vault_secrets, secret_mapping

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Vault discovery fails open: censoring proceeds with zero secrets.

Any failure inside the block (vault manager not initialized, secret dir missing) is swallowed as a warning and discover_vault_secrets returns empty sets, so files containing real vault values are exported unredacted while the report claims everything is clean. For a censoring gate, propagate the failure (or return a discovery_failed flag that orchestration_apply_censoring treats as fatal) when vaults were expected to be available.

🤖 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 `@projects/caliper/orchestration/censoring.py` around lines 102 - 105, Update
discover_vault_secrets so failures during expected vault discovery are not
converted into empty results: propagate the exception, or return an explicit
discovery_failed result that orchestration_apply_censoring treats as fatal
before exporting files. Preserve normal empty-secret behavior only when vault
discovery was not expected or completed successfully.

Comment thread projects/caliper/orchestration/censoring.py
Comment on lines +107 to +114
artifact_dir = Path(env.ARTIFACT_DIR) if env.ARTIFACT_DIR else None
context = NotificationContext(
status=status,
finish_reason=str(finish_reason),
project_name=project or "unknown",
pr_number=os.environ.get("PULL_NUMBER"),
job_type=os.environ.get("JOB_TYPE"),
artifact_dir=artifact_dir,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Parameter rebinding changes which directory Slack reports. artifact_dir (the caller-resolved root used to build the GitHub body) is overwritten with env.ARTIFACT_DIR here, so the Slack context sees a different directory. Use a separate local if that is intentional.

🔧 Suggested fix
-                artifact_dir = Path(env.ARTIFACT_DIR) if env.ARTIFACT_DIR else None
+                slack_artifact_dir = artifact_dir or (
+                    Path(env.ARTIFACT_DIR) if env.ARTIFACT_DIR else None
+                )
                 context = NotificationContext(
@@
-                    artifact_dir=artifact_dir,
+                    artifact_dir=slack_artifact_dir,
                 )
📝 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.

Suggested change
artifact_dir = Path(env.ARTIFACT_DIR) if env.ARTIFACT_DIR else None
context = NotificationContext(
status=status,
finish_reason=str(finish_reason),
project_name=project or "unknown",
pr_number=os.environ.get("PULL_NUMBER"),
job_type=os.environ.get("JOB_TYPE"),
artifact_dir=artifact_dir,
slack_artifact_dir = artifact_dir or (
Path(env.ARTIFACT_DIR) if env.ARTIFACT_DIR else None
)
context = NotificationContext(
status=status,
finish_reason=str(finish_reason),
project_name=project or "unknown",
pr_number=os.environ.get("PULL_NUMBER"),
job_type=os.environ.get("JOB_TYPE"),
artifact_dir=slack_artifact_dir,
🤖 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 `@projects/core/library/export_notifications.py` around lines 107 - 114, Stop
rebinding the caller-resolved artifact_dir in the NotificationContext
construction; use a separate local for env.ARTIFACT_DIR if that value is needed,
while keeping artifact_dir unchanged so Slack reports the same directory used to
build the GitHub body.

Comment on lines +250 to +266
def _get_execution_engine_config() -> str | None:
"""Get execution engine configuration for notification."""
try:
cluster_config = config.project.get_config("cluster", None, warn=False)
if not cluster_config:
return None

config_parts = []
for key, value in cluster_config.items():
if value:
config_parts.append(f"`{key}`: {value}")

if config_parts:
return "* " + " \n* ".join(config_parts)
except Exception:
pass
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Blanket dump of the whole cluster config section into a publicly-readable notification. Any credential-like key added to that section later would leak without a code change. An explicit display-key allowlist is the safer boundary.

As per coding guidelines: "Treat everything under ARTIFACT_DIR as publicly accessible; write only non-sensitive operational data".

🤖 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 `@projects/core/library/export_notifications.py` around lines 250 - 266, Update
_get_execution_engine_config to use an explicit allowlist of approved
non-sensitive cluster configuration keys, and only include values whose keys are
on that allowlist. Stop iterating and publishing every entry from
cluster_config, while preserving the existing formatting and None behavior when
no approved values are available.

Source: Coding guidelines

Comment thread projects/core/library/export_notifications.py
Comment thread projects/core/library/export_notifications.py
Comment on lines +368 to +377
except CensoringOccurredException as e:
logger.info(f"Export completed with censoring: {e}")
# Create success status with censoring flag for notification
status = {
"success": True,
"final_status": "success_with_censoring",
"censoring_occurred": True,
"message": str(e),
"backends": {},
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Censoring path throws away the real export status, so notifications lose all artifact links. run_from_orchestration_config writes status.yaml (with MLflow run_url/experiment_url) before raising CensoringOccurredException, but this handler replaces it with a bare dict lacking caliper_artifacts_export. _extract_artifact_links then emits "No direct links available" for every censored run — arguably the case where links matter most. Consider attaching the status payload to the exception (or reloading status.yaml) and merging the censoring flag in.

🤖 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 `@projects/core/library/export.py` around lines 368 - 377, The censoring
handler in run_from_orchestration_config discards the previously written export
status and its artifact links. Preserve or reload the existing status payload
when handling CensoringOccurredException, then merge success_with_censoring,
censoring_occurred, and the exception message without removing fields such as
caliper_artifacts_export, run_url, or experiment_url.

@kpouget

kpouget commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@psap-forge-bot

Copy link
Copy Markdown
🔴 Submission of skeleton failed after 17 seconds 🔴

Error: FournosJobFailureError: FOURNOS Job 'forge-skeleton-20260730-093158' failed: Resolution failed: Job has reached the specified backoff limit

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@kpouget

kpouget commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@psap-forge-bot

Copy link
Copy Markdown
🔴 Submission of skeleton failed after 17 seconds 🔴

Error: FournosJobFailureError: FOURNOS Job 'forge-skeleton-20260730-101358' failed: Resolution failed: Job has reached the specified backoff limit

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@kpouget

kpouget commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@psap-forge-bot

Copy link
Copy Markdown
🔴 Submission of skeleton failed after 17 seconds 🔴

Error: FournosJobFailureError: FOURNOS Job 'forge-skeleton-20260730-102334' failed: Resolution failed: Job has reached the specified backoff limit

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@kpouget

kpouget commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@psap-forge-bot

Copy link
Copy Markdown

⚠️ Execution of skeleton ⚠️

Artifact Links: No direct links available


Step details

  • loadNone
  • smokeNone
    002__postprocessing: ✅ parse:✅ visualize:✅ artifacts_to_kpis:✅ kpis_to_csv:⚪ artifacts_to_ai_data:✅ s3_import:✅ analyse_kpis:✅ s3_export:✅
Censoring Applied

Caliper Export: 3 file(s) contained sensitive content

3 file(s) had sensitive content sanitized and included in export:

  • 02__export-artifacts/000__ci_metadata/pull_request-comments.json
  • 00__preflight/000__ci_metadata/pull_request-comments.json
  • 01__test/000__ci_metadata/pull_request-comments.json

Sensitive content (passwords, API keys, tokens) was replaced with placeholder text.

For details, see: $ARTIFACT_DIR/censoring_report.yaml


@psap-forge-bot

Copy link
Copy Markdown
🔴 Submission of skeleton failed after 1 minute, 59 seconds 🔴

Error: FournosJobFailureError: FOURNOS Job 'forge-skeleton-20260730-111111' failed: Tasks Completed: 3 (Failed: 1, Cancelled 0), Skipped: 0

/test fournos skeleton
/clusterless
/pipeline forge-test-only

@openshift-ci

openshift-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown

@kpouget: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/fournos 4c7e8af link true /test fournos

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant