Skip to content

Release: MCP server, LangGraph incident agent, inference/RAG service, Azure infra#42

Merged
ivanprytula merged 97 commits into
mainfrom
develop
Jul 13, 2026
Merged

Release: MCP server, LangGraph incident agent, inference/RAG service, Azure infra#42
ivanprytula merged 97 commits into
mainfrom
develop

Conversation

@ivanprytula

Copy link
Copy Markdown
Owner

Summary

Brings main up to current develop — 95 commits since the last release (PR #15, 2026-06-14). Highlights:

  • MCP server (Phase 5): exposes the observatory's toolset (source registry, scorecards, drift detection, agent runs) to AI agents via FastMCP
  • LangGraph incident-triage agent: real HITL (human-in-the-loop) review workflow, auto-created on breaking/critical drift
  • Inference service: embeddings/RAG microservice on its own dedicated pgvector Postgres instance (ADR-015)
  • JWT auth required on observations/agent routers
  • Azure infra migration: multi-cloud sandboxes (AWS/Azure/GCP), K3s local Kubernetes sandbox, Redpanda Helm chart fixes
  • CI fix (this branch's tip): unblocks unit tests (optional langgraph import, matching the existing aiokafka pattern) and makes Docker build/push/scan skip cleanly instead of failing when ACR credentials aren't configured yet — no image build/push happens from this PR or from main until Azure Container Registry secrets are added to the repo

Test plan

  • Unit tests pass (DATABASE_URL_TEST=sqlite+aiosqlite:///:memory: uv run pytest -q -m unit)
  • Full CI green on develop tip (5fe86fc): lint, CodeQL, unit tests, integration tests, pip-audit, CI gate all passed; Docker build/push/scan report skipped (no ACR credentials configured yet — expected, not a failure)
  • Docs already current: baseline-checklist.md, application-architecture.md, mvp-roadmap.md all updated in the same commit that shipped the MCP server

🤖 Generated with Claude Code

ivanprytula and others added 30 commits June 15, 2026 00:13
- Introduced `local-https-setup.md` for configuring HTTPS in local development using mkcert.
- Created `local-url-matrix.md` to standardize local API and service URLs.
- Added `references.md` to compile official documentation and resources for core backend, database, ops, observability, security, AI integration, and data processing.
- Updated `sandbox-aws-profile.md` to reflect changes in command usage for Floci.
- Added `system-requirements.md` detailing system packages and installation steps for Ubuntu/Debian.
- Removed outdated `tech-map.md` as its content is no longer relevant.
…security checklist to remove unnecessary S3 encryption command; enhance environment setup documentation with detailed variable guidance and remove outdated file
… optional groups, remove unused

- Create services/dashboard/pyproject.toml
- Update dashboard Dockerfile to use own pyproject.toml
…alongside DashboardApiError in every panel's data-fetching hook.
… Azure provisioning

Fix Python 2 except syntax in drift_events panel and new contract
snapshot job. Add scheduled contract snapshot ingestion that fetches
source responses and runs drift detection automatically. Add Azure
Free Tier provisioning script (B1s VM + PostgreSQL Flexible Server).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add services/dashboard as a uv workspace member alongside ingestor.
Switch Justfile ops recipe to use the ingress compose profile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add register() to AuthResource API client, do_register() to
AuthManager, and a Sign Up tab in the auth sidebar alongside Login.
Includes client-side validation for required fields, password match,
and minimum length.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15 tests covering both job functions: inactive source skip, circuit
breaker open (pre-check and mid-call), successful probe with sample
persistence, upstream failure (503), network errors, URL construction,
snapshot with/without drift detection, HTTP errors, non-dict responses,
and JSON decode failures. Also fixes except syntax bug in
run_source_contract_snapshot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ivanprytula and others added 24 commits July 10, 2026 17:16
512Mi was below what the chart's own entry-point template requires
(redpanda 409Mi + reserve 201Mi = 610Mi), so the release failed at
template execution before creating any resources. Bump to 2Gi, close
to the chart's own upstream default (2.5Gi), for comfortable headroom
on a local single-node dev cluster.
…rams

docs/02-architecture/architecture.md (last updated April, ~2.5 months
stale) described an aspirational multi-service design — processor/
inference/analytics services, MongoDB, Qdrant, AWS ECS Fargate — none
of which exist; only ingestor+dashboard are implemented, and the real
cloud target is an Azure VM with Terraform now owned entirely by the
sibling infra repo. Archive it alongside the other two already-archived
architecture docs.

Add two lean, current-state replacements instead of one large doc, so
each can be updated independently as services/infra actually change:
- application-architecture.md: system context + container diagram,
  plus a router/feature map distinguishing MVP-active from
  present-but-deferred code (grounded in the actual routers/ directory
  and audit-gaps.md, not the old doc's claims).
- infrastructure-architecture.md: one diagram, two zones — local
  playground (this repo) vs real cloud (api-observatory-infra) —
  matching the boundary rule fixed earlier this session.

Both include a "How to Update" section, and CLAUDE.md's Plan
Maintenance triggers now point to them so they don't go stale the same
way.
k3s-deploy-infra installs the Bitnami postgresql chart under release
name "postgresql" (for chart in postgresql redis), which Bitnami's
naming convention collapses to service name "postgresql" (release
name == chart name, no "<release>-<chart>" suffix) — confirmed by the
chart's own NOTES output ("postgresql.data-zoo.svc.cluster.local").
secret.example.yaml pointed DATABASE_URL at "postgres-postgresql"
instead, a service that never existed, so /readyz's `SELECT 1` check
never succeeded and the ingestor rollout timed out at 0/2 ready.
namespace.yaml enforces pod-security.kubernetes.io/enforce: restricted,
which requires seccompProfile.type: RuntimeDefault (or Localhost) on
every pod. ingestor/dashboard never set it, so every pod-creation
attempt was rejected by admission control after the namespace label
landed — 0/2 and 0/1 ready with no pods ever appearing, not even
crash-looping ones. postgresql-0/redis-master-0/redpanda-0 were
unaffected only because they were created before this apply added the
restricted label to the (previously unlabeled) namespace; PSA doesn't
evict already-running pods retroactively.
Three separate bugs surfaced during local k3s validation:

1. ingestor probes hit /health and /readyz via the pod IP directly;
   kubelet's default Host header doesn't match TrustedHostMiddleware's
   allowlist (main.py), so every probe got 400 and the startupProbe
   killed the container after 6 failures. Set an explicit Host header
   on all three probes instead of loosening the middleware.

2. dashboard-deployment.yaml declared containerPort 8003 and probed
   /health + /readyz — but the container actually runs Streamlit on
   8501 (per its Dockerfile) with only /_stcore/health. This manifest
   had never been exercised end-to-end before. Fixed port and probe
   paths; the Service's external port 8003 is unchanged since it's a
   separate mapping via the named "http" port.

3. k3s-up never ran migrations, unlike the docker-compose flow's
   separate `just migrate` step — ingestor logs showed
   UndefinedTableError for source_profiles on every startup. Add a
   one-off Job (migrate-job.yaml) running `alembic upgrade head`
   against the same secret, wired into k3s-up between k3s-secret and
   k3s-deploy so the schema exists before the app pods start.
docker-compose.yml sets SERVICE_VERSION explicitly (default 0.1.0);
the k8s overlay never did, and the repo-root VERSION file fallback
isn't copied into the Docker image, so get_service_version() raised
an unhandled RuntimeError on every /health call — the probe fix
alone wasn't enough, health itself was throwing 500s. Add
service_version to the shared ConfigMap and wire it in via
configMapKeyRef, mirroring the existing ingestor_url pattern.
…tion

libs/version.py is deliberately strict/fail-fast — it exists for
cross-service version-consensus checks (do all services agree on the
same contracts version before a coordinated rollout, or has one
drifted). But /health and /readyz, which Kubernetes uses to decide
whether to kill or de-register a pod, called it directly in the
request path. Discovered when /health crashed with an unhandled
RuntimeError during local k3d validation (no SERVICE_VERSION, no
VERSION file in the image) — a missing version string shouldn't be
able to take down a healthy pod's liveness probe.

- /health: drop version reporting entirely, matching its own
  docstring's promise to be lightweight and dependency-free.
- /readyz: keep version as best-effort context, but resolve contracts
  version defensively (falls back to "unknown" + a log warning instead
  of raising); service version was already safely resolved via
  settings.app_version's default.
- New GET /version: the actual deliberate consensus check. Calls the
  strict resolver unwrapped and fails loudly (500) on misconfiguration
  — that failure is the point, for CI/release tooling/operators to
  query before declaring a rollout or to detect drift.
… action

TREE_SHA=$(git rev-parse HEAD^{tree} | cut -c1-7) was independently
duplicated in 4 places across ci.yml (two build jobs), cd-dev.yml, and
release.yml. Extract it once into .github/actions/resolve-tree-sha so
there's a single source of truth for how image tags are computed —
pure DRY cleanup, no behavior change. cd-dev.yml's adoption (plus its
new rollback-input override) is a separate commit since it also wires
SERVICE_VERSION through to the deployed container.
CD pulled and tagged the correct tree-<SHA> image but never set
SERVICE_VERSION when starting docker compose — the running container
self-reported whatever was last written into the VM's .env at
provisioning time, not the SHA actually running. /health (soon
/version) on the real deployed VM was disconnected from reality.

- Write SERVICE_VERSION=tree-${TREE_SHA} into the VM's .env before
  `docker compose up -d`, matching the image tag exactly.
- After the existing health check, curl /version and assert its
  `service` field matches the deployed tag — a deploy that "succeeds"
  but reports the wrong version now fails CD instead of shipping
  silently.
- Add a workflow_dispatch trigger with an optional tree_sha input so a
  known-good prior release can be redeployed on demand (rollback)
  without reverting commits — same approval gate and health/version
  checks as a normal deploy. The automatic workflow_run path is
  unchanged when no input is given.
- Adopt the resolve-tree-sha composite action (previous commit) for
  the "compute from current HEAD" path; the manual tree_sha input
  bypasses it entirely when supplied.
…e wrong field

render_freshness_heatmap read SourceProfile.updated_at as "last probe
time" — that field only changes when the source's registration record
itself is edited (name, interval, etc.), not when the scheduler
actually probes it. Discovered live: a source with probes firing every
60s showed a frozen "Last probe" timestamp from registration time and
a permanent "stale" badge. /health/jobs-metrics already tracks the
real per-job last_run_at (used elsewhere via HealthResource) — wire
that in instead, keyed by the same "probe_source_{id}" job name the
scheduler registers.

Also fixes a latent sign inconsistency in the same function: the
string-timestamp branch computed (last - now), the datetime-object
branch computed (now - last) — opposite signs meaning a stale source
and a fresh one could get swapped depending on which type flowed
through. Both branches now consistently compute (now - last), and
naive ISO timestamps (the API returns no "Z"/offset) are explicitly
treated as UTC instead of being silently misinterpreted as local time.
Read jinfo.get("total_executions", 0) and jinfo.get("error_count", 0)
— neither key exists in /health/jobs-metrics' actual response shape
(success_count/failure_count, per health_ingestion_jobs.py). .get()'s
default silently masked the mismatch, so every job showed zero runs
regardless of real activity, even ones firing every 60s. Discovered
live: probe_source_1 clearly had multiple successful runs (confirmed
via logs and the now-fixed freshness heatmap) but this panel still
read 0.
register_source_probe_jobs was only ever called once, at app startup
(main.py lifespan) — sources created afterward via POST /api/v1/sources
were silently never scheduled until the next restart. Discovered live
during an e2e walkthrough: a newly-registered source's scorecard
stayed at sample_count=0 despite the source being reachable, because
no probe job existed for it yet.

Wire the scheduler into source_registry.py the same way it's already
injected into health_ingestion_jobs.py (module-level set_scheduler()
called from the lifespan), then call the existing (idempotent —
already skips jobs that exist) register_source_probe_jobs() right
after a source is created. Best-effort: a registration failure here
logs a warning but doesn't fail the source-creation request, matching
the same tolerance the startup path already has.
JobScheduler.job() (the @scheduler.job(...) decorator) only ever wrote
to self._jobs — the actual APScheduler.add_job() wiring happened
exclusively inside start()'s one-time loop at boot. A previous commit
made register_source_probe_jobs() callable dynamically (after a source
is registered via the API, post-startup), but that only updated the
bookkeeping dict; the live scheduler engine never learned about the
new job. Confirmed live: registering a second source showed
total_scheduler_jobs go from 4 to 6 in the logs, but the new
probe_source_2/contract_snapshot_source_2 jobs never actually fired.

Extract the per-job wiring (trigger-disabled check, handler wrapping,
add_job, logging) out of start()'s loop into _activate_job(), and have
the job() decorator call it immediately whenever the scheduler is
already running — so a job registered post-boot is live immediately
instead of only taking effect on the next process restart.

Added a regression test that registers a job after start() and
asserts it's present in the live APScheduler engine (not just
self._jobs) — verified it fails on the old code (AssertionError: assert
None is not None) and passes with this fix.
…ritical drift

Phase 1 of the AI-augmented observatory plan: unifies the drift-detection
pipeline with the Observation/AgentRun models instead of leaving them as two
disconnected demos. create_contract_snapshot now creates an Observation
(tags=["incident", severity]) + a pending AgentRun in the same transaction
as the drift event, whenever severity=="critical" or event_type=="breaking".
No agent processing happens yet — that lands in Phase 3 once the LangGraph
graph exists to actually consume pending runs.

Also: named Postgres volume in docker-compose.dev.yml so local data persists
across restarts (base docker-compose.yml stays cloud-deploy-ready), and Bruno
collection fixes/additions (contracts/4 had body:none silently dropping its
payload, contracts/5 had a malformed meta block that crashed the CLI runner,
both referenced a stale {{token}} var) so the drift-trigger flow is
verifiable end-to-end via `bru run`.

uv.lock relocked by this sandbox's uv/Python 3.14 environment (drops a
stale exclude-newer options block); unrelated to Phase 1 dependencies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d pgvector Postgres

Phase 2 of the AI-augmented observatory plan. New services/inference/ — FastAPI
on :8001 with POST /index and POST /search matching the contract
services/ingestor/vector_search.py already expected, so /analyze's RAG context
retrieval is real instead of silently falling back to "no context available".

Backed by pgvector, not Qdrant: embeddings via fastembed (ONNX Runtime,
CPU-only) rather than sentence-transformers, since this environment's torch
build pulls a full CUDA dependency chain (~2GB) unconditionally on Linux —
disproportionate for a free-tier Azure VM. Runs on its own dedicated Postgres
instance (inference-db, port 5433) rather than sharing the ingestor's db —
real per-service database ownership, own Alembic migration history.

Documentation debt closed along the way: ADR-002 (the old "Qdrant primary"
decision) turned out to be orphaned — it referenced a parent doc that only
exists in docs/_archive/, left over from an abandoned pre-MVP design that was
never cleaned up. Superseded by ADR-015, which documents the actual decision
and explicitly leaves Qdrant on the table as a future option once there's a
concrete need, not gated on budget alone. mvp-roadmap.md, decisions.md, and
application-architecture.md's container diagram updated to match reality.
Also de-linked three pre-existing dead ADR references (005/006/008) in
decisions.md that docs-quality-markdown caught once this file was touched.

Verified live end-to-end multiple ways: local uvicorn, the real Docker
container, and the ingestor's actual vector_search.search_observation_documents()
call over the Docker network — not just the inference service in isolation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 3 of the AI-augmented observatory plan. New services/ingestor/agent/ —
five linear nodes (classify_severity -> retrieve_similar_incidents ->
draft_analysis -> human_review -> notify), checkpointed to the ingestor's
own Postgres via langgraph-checkpoint-postgres so the human-in-the-loop
pause survives process restarts. Auto-triggered by critical/breaking drift
events (fire-and-forget from contract_drift.py, lazy-imported so a missing
`ai` extra degrades to a warning, not a crash). Resume via
POST /api/v1/agent/runs/{run_id}/resume.

Anthropic, not OpenAI: the plan's "structured OpenAI output" assumed a key
that isn't available here — claude-haiku-4-5 for the cheap classify step,
claude-sonnet-4-5 for the analysis a human actually reviews. New
anthropic_* settings mirror the existing openai_* pattern exactly.

Verified live end-to-end against the real stack with a real API key, twice
(approve + reject): incident triggered -> AgentRun reached awaiting_review
with a coherent root-cause analysis -> resumed via a separate HTTP call ->
status correctly flipped. RAG paid off organically: the second incident's
analysis referenced the first one by name, retrieved from what the first
run had indexed moments earlier.

Also: fixed a cross-suite test bug found while combining regression runs —
services/inference/tests/'s cleanup fixture did DROP SCHEMA public CASCADE,
silently wiping the ingestor's own tables when both suites share the
session-scoped test Postgres; scoped it to just its own tables instead, and
fixed the mirror-image gap in the ingestor's own fixture (missing
CREATE EXTENSION vector after its schema reset). Docker: added explicit
`AS runtime` stage aliases to the two Dockerfiles that were still missing
them (services/inference/Dockerfile already had it from Phase 2).

Docs: corrected ADR-012 (was live, unlike the orphaned ADR-002 — fixed
stale OpenAI model names and a "conditional routing" description that was
never actually built), mvp-roadmap.md, audit-gaps.md gap #7 -> resolved,
application-architecture.md, and this plan's own Phase 3 section text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 4 of the AI-augmented observatory plan - closes audit-gaps.md gap
🟠#6 for these two routers using the jwt_role_guard pattern already
established in contract_drift.py/source_registry.py/scorecards.py
(JwtDep for reads, WriterJwtDep/ReviewerJwtDep for writes, AdminJwtDep
for hard-delete). The dedicated auth-mechanism teaching routes in
observations.py (/secure/*, /auth/login, /batch/protected) and all of
observations_v2.py are left untouched by design.

Also spoof-proofs the HITL resume endpoint: reviewer_user_id is no
longer a client-supplied body field. It's derived server-side from the
caller's authenticated JWT (sub -> User.id via the same lookup
routers/auth.py already uses), degrading to None rather than trusting
any client input when the subject has no matching user.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d/docs

4 Streamlit session-state properties in adapter.py (agent_run_id,
agent_result, agent_hitl_paused, agent_stream_events) were scaffolding
for the same abandoned /enrich//stream design as the just-deleted
bruno/z-agent/ collection - confirmed unreferenced anywhere in the
dashboard and not part of the UIAdapter Protocol contract.
learning-paths.md's "Agent Enrichment -> HITL Review tab" claim was
false (no such tab exists) and its other agent-track bullets described
the same aspirational design rather than the real Phase 3 nodes/flow;
corrected to point at the real API flow and the working bruno/agent/
collection. Also fixed a stale "4 services" compose count (Phase 2
added inference/inference-db).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The client fixture tried to force docs-auth off for tests via
os.environ.setdefault(...), but load_dotenv() runs first and already
populates those keys from .env, so setdefault silently no-ops -
API_V1_BEARER_TOKEN had no such attempt at all. Any dev following the
documented `cp .env.example .env` setup got 4 spurious local test
failures (docs/redoc/openapi/batch-protected assuming auth was
disabled) that stayed invisible in CI, where .env doesn't exist.
Force these to blank instead, matching the plain-assignment pattern
already used for ENVIRONMENT/CACHE_ENABLED two lines above. Also flip
.env.example's own DOCS_USERNAME/DOCS_PASSWORD default to blank,
matching its pre-existing comment ("leave empty to allow public
access") that the shipped default never actually followed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…script

CLAUDE.md duplicated the global ACROSS principles verbatim; replace with a
pointer to ~/.claude/CLAUDE.md. Docker sandboxes only read project-level
config, so add scripts/refresh-sandbox-claude.sh to regenerate a
self-contained .claude/CLAUDE.md (gitignored) by concatenating the global
file with this repo's root CLAUDE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New services/mcp/ - a stdio FastMCP server wrapping 11 tools (sources,
scorecards, contract drift, agent-run status/approve-reject) as thin
authenticated HTTP calls against the ingestor's real API, never
in-process repository imports. Logs in as a dedicated mcp-service
account via the actual /auth/token flow (auth_client.py), dogfooding
Phase 4's JWT auth rather than bypassing it - per-loop cached token
with expiry tracking, single forced re-login+retry on a 401.

scripts/register_mcp_service_user.py handles the one-time account
setup (idempotent). No docker-compose entry or port for v1 (stdio only,
port 8006 reserved for a future streamable-http mode).

Verified two ways: 16 unit tests (respx-mocked HTTP) plus a real
end-to-end run against the live stack - real login, real reads, and a
real 409 correctly propagated from resume_agent_run on a non-approvable
run. Along the way, fixed a real bug in this script (a .local email
placeholder failing strict validation) and flagged (but did not fix,
out of scope) a pre-existing auth.py bug where login 500s if Cache is
merely unreachable instead of failing open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
create_session() only guarded against _session_client being None
(Cache never connected) — a live client whose write raises (e.g. Cache
merely unreachable) was uncaught, so POST /auth/token 500s on any
transient Cache outage even though the JWT itself doesn't need Cache.
create_refresh_token() right next to it already wraps its Cache call
the same way; this brings create_session() in line with that existing
fail-open pattern. Found live while verifying the new MCP server
(services/mcp/) against the real stack.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…jobs without ACR creds

Unit test collection failed because nodes.py imported langgraph at module
scope unguarded, unlike the existing aiokafka optional-import pattern in
events.py. Guarded it the same way, and marked the two graph-execution
tests in test_graph.py with importorskip (they need a real langgraph
runtime, not just an unblocked import).

Docker build/push/scan jobs were hard-failing since no ACR secrets/vars
are configured yet. They now skip cleanly via `if: vars.ACR_LOGIN_SERVER
!= ''` and the CI gate accepts skipped as well as success for those three
checks — CodeQL and integration tests remain strictly required. Building
will resume automatically once Azure credentials are added.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread scripts/tools/generate-secrets.py Fixed
Comment thread scripts/tools/generate-secrets.py Fixed
Comment thread scripts/tools/generate-secrets.py Fixed
Comment thread services/ingestor/routers/observations.py Fixed
ivanprytula and others added 2 commits July 13, 2026 12:28
…ream

CodeQL flagged py/stack-trace-exposure: on failure, the SSE error event
yielded the raw OpenAI SDK exception (str(exc)) straight into the
client-facing response body. The real exception is already logged
server-side (logger.error); the client now gets a generic message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CodeQL flagged py/clear-text-logging-sensitive-data on generate-secrets.py:
_print_secret printed each freshly-generated secret value straight to
stdout, which can land in terminal scrollback, screen-recording tools, or
shared sessions.

Values now go to .generated.secrets.env (mode 0600, truncated at the start
of each run so stale secrets from a prior invocation never linger),
gitignored so it can never be committed. Only the env var name and a
"wrote to <path>" confirmation print to stdout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
global _output_file_started
mode = "w" if not _output_file_started else "a"
with _OUTPUT_ENV_PATH.open(mode, encoding="utf-8") as f:
f.write(f"{env_var}={value}\n")


def _print_secret(env_var: str, value: str, label: str, desc: str) -> None:
print(f"# ── {label} ──")

def _print_secret(env_var: str, value: str, label: str, desc: str) -> None:
print(f"# ── {label} ──")
print(f"# {desc}")
print(f"# ── {label} ──")
print(f"# {desc}")
_write_secret_line(env_var, value)
print(f"# Wrote {env_var} to {_OUTPUT_ENV_PATH}")
@ivanprytula ivanprytula self-assigned this Jul 13, 2026
@ivanprytula ivanprytula merged commit 5251ee1 into main Jul 13, 2026
23 of 24 checks passed
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.

2 participants