Skip to content

feat(application): add continuous recording to 503 media capture service - #848

Open
Alain Uyidi (auyidi1) wants to merge 5 commits into
mainfrom
feat/continuous-video-capture
Open

Alain Uyidi (auyidi1) wants to merge 5 commits into
mainfrom
feat/continuous-video-capture

Conversation

@auyidi1

@auyidi1 Alain Uyidi (auyidi1) commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request

Description

Adds a continuous recording mode to 503-media-capture-service. The service records back-to-back segments from an RTSP stream with ffmpeg into the ACSA cloud-backed volume and writes a JSON metadata file next to each segment. ACSA uploads both to Blob Storage, where the video query API from #846 lists them by camera and hour. The PR also adds mock RTSP cameras for 508-media-connector and an ADR.

This is the third focused replacement for #468 (after #846 and #847).

Related Issue

Replaces part of #468.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Blueprint modification or addition
  • Component modification or addition
  • Documentation update
  • CI/CD pipeline change
  • Other (please describe):

Implementation Details

Breaking Change

Triggered clips now go to {MEDIA_CLOUD_SYNC_DIR}/{CAMERA_ID}/{YYYY}/{MM}/{DD}/{HH}/ instead of {MEDIA_CLOUD_SYNC_DIR}/{YYYY-MM-DD}/. Continuous segments and triggered clips therefore share one prefix per camera and hour. Nothing else in the repository reads the old layout, and the README calls out the change.

Continuous Recording

  • Opt-in: enabled with CONTINUOUS_RECORDING_ENABLED=true (chart: mediaCapture.continuousRecording.enabled, default false). Triggered mode remains the default.
  • Segments: each segment is re-encoded to 360p H.264 with AAC to bound CPU, memory, and upload size. ffmpeg writes to <segment>.partial, and the file is renamed only after ffmpeg succeeds, so ACSA never sees a half-written segment under a final name. Partial files from an interrupted run are deleted at startup.
  • Metadata file: camera_id, location, segment_start, segment_end, duration_seconds, and file_name. These are the fields the query API reads.
  • Local retention: segment and metadata files older than LOCAL_RETENTION_HOURS are deleted; 0 disables cleanup. Only segment_* files are deleted, never triggered clips. Directories are removed only when they're empty and older than the cutoff.

Review Round 1 (Katrien De Graeve (@katriendg))

  • Startup cleanup can no longer delete another recorder's segment. Startup and each cleanup run delete only partial files that haven't changed for 2 minutes. An active ffmpeg rewrites its file continuously, and 2 minutes covers the 10 s RTSP I/O timeout plus the 60 s grace period. In continuous mode, the chart also uses the Recreate rollout strategy and fails to render with replicaCount above 1, so recorders for one camera never overlap.
  • Metadata now reflects recorded footage.
    • After ffmpeg exits, ffprobe measures the file. The window ends when ffmpeg finishes and starts the measured duration earlier, so connection time isn't counted. The segment file name uses the same start.
    • A recording under 1 second, including an empty file from a successful ffmpeg exit, is rejected.
    • A shorter-than-requested segment keeps its actual duration and logs a warning.
  • Retention covers both formats. Expired MP4 and MKV segments and their sidecars are deleted whatever the current OUTPUT_FORMAT is. Triggered clips are still preserved.
Issue in #468 This PR
ffmpeg ran through blocking std::process::Command::output() inside an async function, holding a Tokio worker for the whole segment tokio::process::Command with kill_on_drop, plus a timeout of segment length + 60 s
ffmpeg stderr was logged on failure, and it echoes the RTSP URL including credentials User information in URLs is redacted before logging; tested with a credentialed URL
CAMERA_ID was used in file system paths without validation Validated against [A-Za-z0-9_-]{1,128}, the same rule as the video query API, in both modes
OUTPUT_FORMAT was passed to ffmpeg -f unchecked Limited to mp4 or mkv
expect() panicked on missing environment variables Startup errors are returned instead
Cleanup deleted videos but left their .json files, so directories were never removed Metadata files and stale partial files are deleted too
Interrupted segments stayed under final names without metadata Partial-file staging and startup cleanup
Unused hash-prefixed path helper plus the md5 dependency, contradicting the query API's prefix layout Removed; Cargo.toml and Cargo.lock are unchanged
Chart defaulted to continuous mode, moved feedDelaySeconds under the new block (so VIDEO_FEED_DELAY_SECONDS rendered empty), set CAMERA_ID only in continuous mode, and added an unused AZURE_STORAGE_CONNECTION_STRING Default false; feedDelaySeconds left in place; mediaCapture.camera.id and location set in both modes; no storage connection string
yaml/media-capture-mqtt-triggered.yaml with a site-specific registry, IP address, camera names, and topics Dropped; the chart is the deployment path
docker-compose.yml changed a pinned Mosquitto image to :latest Not included
Mock cameras as bare Pods in azure-iot-operations, with an unpinned image Deployments in a mock-cameras namespace, image pinned by digest, running as non-root with a read-only root file system, all capabilities dropped, and resource limits
508 README rewrite of the media connector asset schema Not included; see Additional Notes
708-line ADR describing hash-prefixed paths, MQTT and stitching query paths, an SDK, and planning phases that aren't implemented Rewritten to the implemented decision; the 503/508 responsibility split from the proposed project ADR 006 is folded in instead of adding a project ADR

Testing Performed

  • Terraform plan/apply
  • Blueprint deployment test
  • Unit tests
  • Integration tests
  • Bug fix includes regression test (see Test Policy)
  • Manual validation
  • Other:

Validation Steps

  1. Round 1 adds 7 tests, and cargo test --locked passes all 36:

    • an active partial file survives startup cleanup while an abandoned one is removed;
    • retention after format changes in both directions;
    • delayed startup, an early-ending stream with a successful exit, empty output, and length tolerance;
    • ffprobe output parsing;
    • a real ffprobe measurement of a generated clip, skipped when ffmpeg isn't installed.

    End to end against MediaMTX with a stream that stops mid-segment, the recorder wrote a 10.0 s segment and a 3.0 s segment. Their metadata matched the ffprobe durations, and the start times excluded the connection delay. A stream with a long keyframe interval made ffmpeg exit 0 with an empty file, which is now rejected.

  2. Ran the rust-tests.yml job steps in an Ubuntu 24.04 container with the same apt packages and stable Rust:

    • cargo test --locked: 29 tests pass, 9 of them new. They cover camera ID validation, credential redaction, output formats, the path layout, metadata fields, partial-file handling, and cleanup.
    • cargo clippy --all-targets: no warnings in new or changed code. The remaining warnings are already on main.
    • rustfmt --check passes on the new files.
  3. End to end with MediaMTX and an ffmpeg test source:

    • 10-second segments are recorded as 360p H.264 + AAC with matching metadata files.
    • Stopping mid-segment leaves only a .partial file, which the next start removes.
    • A credentialed URL to a missing stream logs the 404 without the password.
  4. helm lint passes. helm template renders CAMERA_ID, VIDEO_FEED_DELAY_SECONDS, and the continuous variables correctly in both modes.

  5. kubeconform -strict validates all 7 mock camera resources. The /live.sdp/<route> stream path was confirmed against the pinned image running as non-root with a read-only root file system.

  6. markdownlint, markdown-table-formatter, cspell, and yamllint pass. The frontmatter warnings and two dead links reported for the changed files are in unchanged lines on main, except the 508 README date, which this PR fixes. Gitleaks finds no leaks.

Checklist

  • I have updated the documentation accordingly
  • I have added tests to cover my changes
  • All new and existing tests passed
  • I have run terraform fmt on all Terraform code
  • I have run terraform validate on all Terraform code
  • I have run az bicep format on all Bicep code
  • I have run az bicep build to validate all Bicep code
  • I have checked for any sensitive data/tokens that should not be committed
  • Lint checks pass (run applicable linters for changed file types)

Security Review

  • No credentials, secrets, or tokens are hardcoded or logged
  • RBAC and identity changes follow least-privilege principles
  • No new network exposure or public endpoints introduced without justification
  • Dependency additions or updates have been reviewed for known vulnerabilities
  • Container image changes use pinned digests or SHA references

Additional Notes

  • Mock camera credentials: the manifest uses the fixed test credentials admin/password, matching 508-media-connector/docker-compose.yml, and creates cluster-internal services only.
  • Not tested on a live cluster: no ACSA volume or Azure IoT Operations cluster was used.
  • Follow-up outside this PR: the 508 README and full-multi-node-cluster/terraform/alert-dataflow.tfvars.example describe media connector tasks with datasets and intervalSeconds. Configure the media connector documents streams with autostart, snapshotsPerSecond, and duration, and storage destinations for file paths. feat(application): add video capture query blueprint, 520-video-query-api, and ONVIF camera tooling #468's rewrite of that section mixed both schemas, so it isn't included here.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

📚 Documentation Health Report

Generated on: 2026-10-06 08:48:01 UTC

📈 Documentation Statistics

Category File Count
Main Documentation 224
Infrastructure Components 232
Blueprints 40
GitHub Resources 26
AI Assistant Guides (Copilot) 17
Total 539

🏗️ Three-Tree Architecture Status

  • ✅ Bicep Documentation Tree: Auto-generated navigation
  • ✅ Terraform Documentation Tree: Auto-generated navigation
  • ✅ README Documentation Tree: Manual README organization

🔍 Quality Metrics

  • Frontmatter Validation:
    success
  • Link Validation: success

This report is automatically generated by the Documentation Automation workflow.

@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.58566% with 275 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.62%. Comparing base (981eaa3) to head (72b9231).

Files with missing lines Patch % Lines
...s/media-capture-service/src/continuous_recorder.rs 66.04% 235 Missing ⚠️
.../media-capture-service/src/multi_trigger_binary.rs 0.00% 35 Missing ⚠️
...ce/services/media-capture-service/src/camera_id.rs 88.46% 3 Missing ⚠️
...services/media-capture-service/src/mqtt_handler.rs 0.00% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #848      +/-   ##
==========================================
+ Coverage   42.15%   44.62%   +2.46%     
==========================================
  Files          42       45       +3     
  Lines        7334     8139     +805     
==========================================
+ Hits         3092     3632     +540     
- Misses       4242     4507     +265     
Flag Coverage Δ
rust 44.62% <66.58%> (+2.46%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
.../services/media-capture-service/src/acsa_writer.rs 100.00% <100.00%> (ø)
...ervices/media-capture-service/src/multi_trigger.rs 95.61% <100.00%> (+0.01%) ⬆️
...vices/media-capture-service/src/video_processor.rs 13.51% <100.00%> (+0.58%) ⬆️
...services/media-capture-service/src/video_writer.rs 61.47% <100.00%> (+2.52%) ⬆️
...services/media-capture-service/src/mqtt_handler.rs 0.00% <0.00%> (ø)
...ce/services/media-capture-service/src/camera_id.rs 88.46% <88.46%> (ø)
.../media-capture-service/src/multi_trigger_binary.rs 0.00% <0.00%> (ø)
...s/media-capture-service/src/continuous_recorder.rs 66.04% <66.04%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for adding continuous recording and the accompanying unit tests. A few inline comments for the recording correctness issues noted inline: startup cleanup can remove another pod's active segment, metadata assumes the requested duration rather than the footage actually captured, and retention leaves old-format videos behind while deleting their sidecars.


// Segments left by an interrupted earlier run are incomplete
let camera_path = self.config.output_base_path.join(&self.config.camera_id);
match remove_partial_segments(&camera_path).await {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we distinguish abandoned partial files from files another recorder is still writing before deleting them? This removes every .partial file for the camera, including an active recording owned by another pod. The chart uses the default rolling-update strategy, so pods can overlap even with replicaCount: 1; additional replicas share the same camera directory too.

A local probe confirmed that cleanup unlinks an open recording file and the original writer then cannot rename it to its final path. Please enforce exclusive ownership per camera or make cleanup ownership-aware, and add a regression test proving startup cannot delete another active recorder's segment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in c83e654. Partial cleanup is now ownership-safe without a lock. Startup, and every cleanup run, only deletes partial files that haven't been modified for 2 minutes. An active ffmpeg keeps rewriting its partial file, and 2 minutes covers the 10 s RTSP I/O timeout plus the 60 s ffmpeg grace period. That works even when the volume is shared across nodes, where file locks may not hold. startup_keeps_active_partial_and_removes_abandoned_one keeps a writer appending to one partial file while startup cleanup runs, then renames it after cleanup, and checks that only the idle partial was removed. To avoid overlap at all, continuous mode now uses the Recreate rollout strategy, and the chart fails to render with replicaCount above 1.

}
fs::rename(&partial, &output).await?;

let segment_end = segment_start + chrono::Duration::from_std(self.config.segment_duration)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we validate the actual captured duration before publishing this metadata? -t limits FFmpeg's output duration; a successful exit does not prove that the requested duration was recorded. An early stream EOF can produce a shorter valid file, but this code still advertises the full configured interval. Also, segment_start is captured before connecting to the camera, so connection/setup time shifts the reported start away from the captured footage.

Please derive the interval from capture timing and measured media duration, and explicitly handle short or empty output. Add tests for delayed startup and an early-ending stream, including a successful process exit with less footage than requested, so time-range queries do not report coverage that the video does not contain.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in c83e654. After ffmpeg exits, ffprobe measures the file. The window ends when ffmpeg finished and starts the measured duration earlier, so connection and setup time is excluded. The segment file name now uses that start, keeping it consistent with the metadata. Recordings under 1 s are rejected, which covers an empty file from a successful exit. A short segment keeps its real duration and logs a warning. Tests cover delayed startup, an early-ending stream with a successful exit, empty and near-empty output, and tolerance. A real ffprobe test measures a generated clip. End to end against MediaMTX, a stream that stopped mid-segment produced a 3.0 s segment with 3 s metadata. A long keyframe interval reproduced your exact case: ffmpeg exited 0 with an empty file, and it's now rejected instead of published.


fn start_cleanup_task(&self, retention: Duration) {
let camera_path = self.config.output_base_path.join(&self.config.camera_id);
let extension = self.config.output_format.extension();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can retention handle both supported video formats, regardless of the current recording setting? After switching OUTPUT_FORMAT from mp4 to mkv, cleanup only matches MKV videos, but is_segment_file still matches every JSON sidecar. A local probe confirmed that an expired MP4 remains while its metadata is deleted.

This leaves old-format recordings outside the application's retention policy and separates them from their metadata. Please clean up expired MP4 and MKV segments consistently while preserving triggered clips, and add regression coverage for format changes in both directions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in c83e654. Retention now matches segment_* videos in both supported formats (mp4 and mkv) whatever the current OUTPUT_FORMAT is, plus their .json sidecars and stale partial files. Triggered clips don't use the segment_ prefix and are still preserved. retention_cleans_every_video_format_after_format_changes expires an MP4 and an MKV segment with sidecars alongside triggered clips in both formats and recent segments, and checks that only the expired segments and sidecars are removed.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

📚 Documentation Health Report

Generated on: 2026-10-07 17:53:45 UTC

📈 Documentation Statistics

Category File Count
Main Documentation 224
Infrastructure Components 232
Blueprints 40
GitHub Resources 26
AI Assistant Guides (Copilot) 17
Total 539

🏗️ Three-Tree Architecture Status

  • ✅ Bicep Documentation Tree: Auto-generated navigation
  • ✅ Terraform Documentation Tree: Auto-generated navigation
  • ✅ README Documentation Tree: Manual README organization

🔍 Quality Metrics

  • Frontmatter Validation:
    success
  • Link Validation: success

This report is automatically generated by the Documentation Automation workflow.

Alain Uyidi (auyidi1) and others added 3 commits October 8, 2026 04:32
- record back-to-back ffmpeg segments with JSON metadata to the ACSA volume
- write hourly camera paths shared with triggered clips and the query API
- validate CAMERA_ID, redact RTSP credentials, rename only complete segments
- add chart values, mock RTSP cameras for 508, and an ADR

🎥 - Generated by Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hip-safe

- derive segment window from ffprobe duration and ffmpeg finish time
- reject empty recordings; name segments after the actual footage start
- remove only idle partial files so active recorders keep their segments
- apply retention to mp4 and mkv segments; Recreate strategy in continuous mode

🎥 - Generated by Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- write in-progress segments to a staging dir on the same volume
- verify staging is outside the synced dir and on the same filesystem
- require a camera ID in the chart when continuous recording is enabled
- add MEDIA_STAGING_DIR and mediaCapture.storage.stagingDir

🎥 - Generated by Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@auyidi1
Alain Uyidi (auyidi1) force-pushed the feat/continuous-video-capture branch from c83e654 to a19632e Compare October 8, 2026 11:35
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

📚 Documentation Health Report

Generated on: 2026-10-08 11:39:20 UTC

📈 Documentation Statistics

Category File Count
Main Documentation 224
Infrastructure Components 232
Blueprints 40
GitHub Resources 26
AI Assistant Guides (Copilot) 17
Total 539

🏗️ Three-Tree Architecture Status

  • ✅ Bicep Documentation Tree: Auto-generated navigation
  • ✅ Terraform Documentation Tree: Auto-generated navigation
  • ✅ README Documentation Tree: Manual README organization

🔍 Quality Metrics

  • Frontmatter Validation:
    success
  • Link Validation: success

This report is automatically generated by the Documentation Automation workflow.

Alain Uyidi (auyidi1) added a commit that referenced this pull request Oct 8, 2026
…849)

# Pull Request

## Description

Lets `111-assets` express media connector file-system tasks, and moves
the media connector examples and the 508 README to the documented
`streams` format.

[Configure the media
connector](https://learn.microsoft.com/azure/iot-operations/discover-manage-assets/howto-use-media-connector)
configures tasks as asset streams. Each stream has a
`streamConfiguration` (`taskType`, `autostart`, `format`,
`snapshotsPerSecond`, `duration`) and a destination: `Mqtt` with
`topic`, or `Storage` with `path`. This PR fixes four problems:

* **`111-assets` couldn't express a `Storage` destination.** The stream
destination type allowed only `topic`, `retain`, and `qos`. Terraform
silently dropped `path`, and Bicep warned with `BCP037`.
* **The 508 README and `alert-dataflow.tfvars.example` used the old
`datasets` format.** They also used settings the media connector doesn't
document (`intervalSeconds`, `quality`, `storagePath`,
`durationSeconds`), and paths like `/clips`, which the connector can't
write. Learn requires a mounted volume root or a path under `/tmp`.
* **The 508 README device examples didn't match `namespaced_devices`.**
They used `endpoint`, `target_address`, and `username_secret_ref`, and
pointed to a `media-connector-assets.tfvars.example` that doesn't exist.
* **The `111-assets` CI wrapper's `namespaced_assets` variable had
drifted from the component.** It had no `event_groups` or
`management_groups` and used old field names, so Terraform would
silently drop ONVIF PTZ actions and event groups. This was noted as a
follow-up in #847.

## Related Issue

Follow-up to #847 and #848.

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [x] Blueprint modification or addition
- [x] Component modification or addition
- [x] Documentation update
- [ ] CI/CD pipeline change
- [ ] Other (please describe):

## Implementation Details

* **Component (Terraform):**
`namespaced_assets[].streams[].destinations[].configuration` adds
optional `path`. `main.tf` now omits unset destination settings instead
of sending them as `null`.
* **Component (Bicep):** a new exported `AssetStreamDestination` type
adds optional `path`; `AssetStream.destinations` uses it. Dataset
destinations are unchanged.
* **Blueprints:** `full-multi-node-cluster`,
`minimum-single-node-cluster`, and `dual-peered-single-node-cluster`
keep `namespaced_assets` identical to the component. The Bicep
blueprints import the component types.
* **CI wrapper:** `src/100-edge/111-assets/ci/terraform`
`namespaced_assets` is replaced with the component definition.
* **`alert-dataflow.tfvars.example`:** media assets now use `streams`:
  * snapshots to MQTT;
* a 30-second MKV clip and snapshots to `Storage` paths under `/tmp`,
with a comment explaining that the blueprint's connector templates don't
mount a volume;
  * credential references in `<secret>/<key>` form.
* **508 README:**
* Device, asset, and scenario examples are rewritten to the component
schema.
  * The task-type table lists the documented settings and destinations.
  * `Storage` path rules are explained.
* The live-streaming scenario uses `az iot ops ns asset media stream
add`, because the RTSP media server settings vary by connector version.
* The README's `ms.date` changes to ISO format; #848 makes the same
edit.
* **Generated READMEs** are regenerated with `npm run tf-docs` and `npm
run bicep-docs`.

## Testing Performed

- [x] Terraform plan/apply
- [ ] Blueprint deployment test
- [ ] Unit tests
- [ ] Integration tests
- [ ] Bug fix includes regression test (see [Test
Policy](docs/contributing/testing-validation.md))
- [x] Manual validation
- [x] Other: Bicep build and parameter compilation; TFLint

## Validation Steps

1. `terraform fmt -check`, `terraform init -backend=false`, and
`terraform validate` pass for the component and the CI wrapper. TFLint
with `.tflint.hcl` passes for the component, the CI wrapper, and the
three blueprints.
2. `terraform plan` on a root module with every
`full-multi-node-cluster` variable definition:
* accepts `alert-dataflow.tfvars.example` and each of the five new HCL
snippets in the 508 README;
* shows, via the same expression `main.tf` uses, that request bodies
carry only `path` for `Storage` destinations and only the set fields for
`Mqtt` destinations.
   * The example and the new snippets also pass `terraform fmt -check`.
3. Bicep:
* `bicep build` passes for the `111-assets` component and CI wrapper and
for the `full-multi-node-cluster`, `minimum-single-node-cluster`, and
`only-edge-iot-ops` blueprints.
* `bicep build-params` with a `Storage` destination compiles and passes
`path` through. On `main`, the same parameters warn with `BCP037`.
   * `bicep format` leaves `types.bicep` unchanged.
4. `scripts/tf-docs-check.sh` and `scripts/bicep-docs-check.sh` report
no pending updates.
5. markdownlint, markdown-table-formatter, cspell, and
markdown-link-check pass for the 508 README. Gitleaks finds no leaks.

## Checklist

- [x] I have updated the documentation accordingly
- [ ] I have added tests to cover my changes
- [ ] All new and existing tests passed
- [x] I have run `terraform fmt` on all Terraform code
- [x] I have run `terraform validate` on all Terraform code
- [x] I have run `az bicep format` on all Bicep code
- [x] I have run `az bicep build` to validate all Bicep code
- [x] I have checked for any sensitive data/tokens that should not be
committed
- [x] Lint checks pass (run applicable linters for changed file types)

## Security Review

- [x] No credentials, secrets, or tokens are hardcoded or logged
- [x] RBAC and identity changes follow least-privilege principles
- [x] No new network exposure or public endpoints introduced without
justification
- [x] Dependency additions or updates have been reviewed for known
vulnerabilities
- [ ] Container image changes use pinned digests or SHA references

## Additional Notes

* **Not deployed to a live cluster.**
* **Possible follow-up:** let the Akri connector template module mount a
volume, so file-system tasks can write to persistent storage instead of
`/tmp`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

📚 Documentation Health Report

Generated on: 2026-10-08 13:22:16 UTC

📈 Documentation Statistics

Category File Count
Main Documentation 225
Infrastructure Components 233
Blueprints 40
GitHub Resources 26
AI Assistant Guides (Copilot) 17
Total 541

🏗️ Three-Tree Architecture Status

  • ✅ Bicep Documentation Tree: Auto-generated navigation
  • ✅ Terraform Documentation Tree: Auto-generated navigation
  • ✅ README Documentation Tree: Manual README organization

🔍 Quality Metrics

  • Frontmatter Validation:
    success
  • Link Validation: success

This report is automatically generated by the Documentation Automation workflow.

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

📚 Documentation Health Report

Generated on: 2026-10-08 13:40:05 UTC

📈 Documentation Statistics

Category File Count
Main Documentation 225
Infrastructure Components 233
Blueprints 40
GitHub Resources 26
AI Assistant Guides (Copilot) 17
Total 541

🏗️ Three-Tree Architecture Status

  • ✅ Bicep Documentation Tree: Auto-generated navigation
  • ✅ Terraform Documentation Tree: Auto-generated navigation
  • ✅ README Documentation Tree: Manual README organization

🔍 Quality Metrics

  • Frontmatter Validation:
    success
  • Link Validation: success

This report is automatically generated by the Documentation Automation workflow.

This branch has not been deployed

No deployments
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.

4 participants