Skip to content

Commit 4c4f225

Browse files
committed
Fix: config cache corruption when filtering workflows by source repo
The loadAndMatchWorkflows function was mutating the cached YAMLConfig by replacing its Workflows slice with only the matching workflows. This caused subsequent webhook handlers for different source repos to see an incomplete config (only workflows from the first request). Fix: Create a shallow copy of the config before modifying the Workflows slice, preserving the cached original for future requests. Bug symptoms: - First webhook after cache refresh works correctly - Subsequent webhooks for different source repos fail with 'no workflows configured for source repository and branch' - workflow_count in logs shows only the count from the first request
1 parent cce7dc9 commit 4c4f225

6 files changed

Lines changed: 371 additions & 2 deletions

File tree

.claude/pr-7-review.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# PR #7 Review: `feat(operator): operator UI, GitHub PAT auth, AI rule suggester`
2+
3+
**Repo:** grove-platform/github-copier
4+
**PR:** https://github.com/grove-platform/github-copier/pull/7
5+
**Branch:** `feat/operator-ui-audit``main`
6+
**Scope:** +5,373 / −73 across 28 files
7+
**Reviewer perspective:** experienced fullstack, webserver-app focus
8+
9+
## Overview
10+
11+
This PR ships a major v0.4.0 milestone: a 5-tab operator UI backed by GitHub PAT auth with role derivation, webhook replay with per-repo permission checks, and an AI rule suggester with swappable Ollama/Anthropic providers through the Grove Foundry APIM gateway. One ~4,000 line embedded HTML bundle carries most of the frontend.
12+
13+
The architecture is sound: `LLMClient` interface with per-provider implementations, self-verifying LLM output against the in-process `PatternMatcher`, fail-closed config validation (`OPERATOR_UI_ENABLED` + `OPERATOR_AUTH_REPO`), and Secret Manager loading for the Anthropic key. The `#nosec` annotations are justified — base URLs are operator-controlled, not user-controlled.
14+
15+
---
16+
17+
## Blocking / High-priority issues
18+
19+
### 1. Permission-check soft-fail grants writer role by default — auth bypass risk
20+
**File:** `services/operator_auth.go:186-190`
21+
22+
```go
23+
if err != nil {
24+
LogWarning("GitHub permission check failed, defaulting to writer role", ...)
25+
return user, nil
26+
}
27+
```
28+
29+
The PR description's spec says *"None / 404 → denied → 401 Unauthorized"*, but the code returns **writer** on any error from `ghAPIGetRepoPermission` — including GitHub's 404 for "not a collaborator". Combined with `--allow-unauthenticated` on the Cloud Run service, **any** valid GitHub PAT lets a caller view audit logs, webhook traces, workflows, logs drawer, and invoke the AI suggester (which costs real Anthropic tokens).
30+
31+
**Fix:** on 404, return `RoleDenied` + error; only soft-fail on transient 5xx. Distinguish the two cases in `ghAPIGetRepoPermission` by inspecting the status code.
32+
33+
### 2. LLM cost exposure — `/suggest-rule` is unbounded per user
34+
**File:** `services/operator_suggest_rule.go:55`
35+
36+
Any authenticated writer can call `/operator/api/suggest-rule` with no per-user rate limit. Combined with issue #1, this is effectively "anyone with a GitHub PAT can spend Anthropic credits." Add a per-token token bucket (even generous, e.g. 30/hour) before release.
37+
38+
### 3. `/llm/status` calls real Anthropic `/v1/messages` on every refresh
39+
**File:** `services/llm_anthropic.go:105-131`
40+
41+
The ping uses `max_tokens=1` but still hits the Messages API (one input + one output token) on every status tab refresh (writers poll this for the status badge). Cache the last successful ping for ~30s, or degrade to a cheap HEAD/OPTIONS against the gateway.
42+
43+
### 4. Raw PATs stored as map keys
44+
**File:** `services/operator_auth.go:49`, `services/operator_auth.go:104`
45+
46+
`entries[token]` and `repoPerm[token+"\x00"+repo]` keep full PATs live in the heap for the 5-min TTL. A memory dump (crash, Cloud Run profile, goroutine dump) would leak every active operator's token. Hash with SHA-256 and key the maps by digest — same security semantics, no plaintext secrets in process memory.
47+
48+
---
49+
50+
## Medium — worth addressing before merge
51+
52+
### 5. Global LLM mutation surprises multi-operator use
53+
**File:** `services/operator_llm_admin.go:84-88`
54+
55+
`SetActiveModel`/`SetBaseURL` mutate the shared `o.llm` client. Two operators on the UI at once will clobber each other's model choice, and changes silently revert on restart. Either document this as "last write wins, ephemeral" in the UI or make the active model per-request.
56+
57+
### 6. Test coverage gap on the new security-critical surface
58+
59+
The diff adds ~3 test files (audit, delivery tracker, github_write_to_target) but **zero tests** for:
60+
- `services/operator_auth.go`
61+
- `services/operator_ui.go`
62+
- `services/operator_suggest_rule.go`
63+
- `services/llm_anthropic.go`
64+
- `services/llm_client.go`
65+
- `services/operator_llm_admin.go`
66+
67+
At minimum, add table-driven tests for `validateGitHubPAT` role mapping (including the 404 case from issue #1) and `verifySuggestedRule` for each transform type (move/copy/glob/regex).
68+
69+
### 7. `handleRepoPermission` swallows the real error
70+
**File:** `services/operator_ui.go:225`
71+
72+
```go
73+
canRead, _ := o.ghCache.CanUserReadRepo(ctx, userPAT, user.Login, repo)
74+
```
75+
76+
A GitHub rate-limit or 5xx is indistinguishable from "no access" to the frontend, so users see disabled replay buttons with no indication why. Return a per-repo `{allowed: bool, error?: string}` shape instead of `map[string]bool`.
77+
78+
### 8. `githubCreateVersionTag` path components aren't escaped
79+
**File:** `services/operator_ui.go:805`, `services/operator_ui.go:842`
80+
81+
Unlike `ghAPIGetRepoPermission`, this path uses raw `fmt.Sprintf` with `owner`, `repo`, `baseBranch`. The inputs are trusted (env vars), but apply the same `ghUsernameRe`/`ghRepoNameRe` + `url.PathEscape` treatment for defense-in-depth and consistency — you already established the pattern one file over.
82+
83+
### 9. Anthropic fallback model list is a maintenance hazard
84+
**File:** `services/llm_anthropic.go:26-30`
85+
86+
Hard-coding `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5-20251001` in the fallback means when those rotate you'll ship dead options to the UI. Either load from an embedded config file or trim to a single known-stable haiku alias.
87+
88+
### 10. Model name inconsistency across configs
89+
- `configs/environment.go:281``claude-haiku-4-5-20251001`
90+
- `.github/workflows/ci.yml``claude-haiku-4-5`
91+
92+
Both resolve via Anthropic aliasing, but pick one. The dated form is more reproducible; the alias drifts.
93+
94+
---
95+
96+
## Minor / nits
97+
98+
- `services/operator_ui.go:873-875`: `githubHTTPClient()` allocates a new `*http.Client` per call. Make it a package var.
99+
- `services/operator_auth.go:93-100` + `services/operator_auth.go:123-130`: cache eviction is O(n) inside the write lock; fine at 100/500, just worth knowing.
100+
- `services/web/operator/index.html` at ~4k lines is hard to review and diff. Splitting CSS/JS into sibling files and using `//go:embed web/operator/*` would make this reviewable and cacheable.
101+
- `services/operator_ui.go:461-462`: `releaseMode` is only `"disabled"` vs `"tag_create_enabled"` — consider a typed enum.
102+
- `services/operator_suggest_rule.go:344-349`: `truncate(s, n)` counts bytes not runes — cuts inside multi-byte glyphs. Use `utf8.RuneCountInString`-bounded slicing.
103+
- `services/operator_ui.go:738`: `operator_replay` deliveryID uses `time.Now().UnixMilli()` — two replays in the same ms collide. Add a short random suffix.
104+
105+
---
106+
107+
## What's solid
108+
109+
- **SSRF hardening** in `ghAPIGetRepoPermission` is textbook — whitelist regex + `url.PathEscape` + pinned host.
110+
- **Fail-closed startup** in `validateOperatorAuth` correctly prevents the "UI enabled but no auth repo" footgun.
111+
- **Self-verification** of LLM output via `PatternMatcher` before showing to the user is the right pattern — catches hallucinated rules before they ship.
112+
- **In-flight dedup** on replay + per-repo permission gating on the source repo is a well-considered authorization model.
113+
- **Streaming NDJSON** for Ollama pulls with both request-context cancel and 20-min safety timeout is handled correctly.
114+
- Scoping `write``writer` (not `operator`) based on real docs-repo access patterns is exactly the kind of call you want a human to make, and the comment explaining it is excellent.
115+
- **Dual-header auth** (`x-api-key` + `api-key`) lets one client work against native Anthropic or the Azure APIM gateway — clean solution to a real deployment-flexibility problem.
116+
117+
---
118+
119+
## Recommendation
120+
121+
**Request changes** — items 1 and 2 are release-blocking for an `--allow-unauthenticated` Cloud Run deploy. Items 3, 4, and 6 are strongly recommended before cutting v0.4.0. The rest are polish.
122+
123+
---
124+
125+
## For the next agent
126+
127+
If you're picking this up to address the findings, suggested order:
128+
129+
1. Fix issue #1 first — distinguish 404 from transient errors in `ghAPIGetRepoPermission`, return `RoleDenied` on 404. Add a test that asserts the 404 path returns denied.
130+
2. Add the token bucket for #2 (keyed by SHA-256 of PAT).
131+
3. While in `operator_auth.go`, do #4 (hash keys) in the same pass.
132+
4. Cache the `/llm/status` ping (#3) — 30s TTL stored on the `operatorUI` struct.
133+
5. Backfill tests (#6) covering the new paths.
134+
6. Sweep the mediums/minors.
135+
136+
The PR description's "Authentication & authorization" table is the source of truth for the intended role mapping — the code should match it exactly.

.claude/settings.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(gh api:*)",
5+
"Bash(go build *)",
6+
"Bash(go vet *)",
7+
"Bash(go test *)",
8+
"Bash(go env *)",
9+
"Bash(grep -r \"ObjectIDAsHexString\" \"$\\(go env GOMODCACHE\\)/go.mongodb.org/mongo-driver/v2@\"*)",
10+
"Bash(grep -n \"BSONOptions\\\\|SetBSONOptions\" \"$\\(go env GOMODCACHE\\)/go.mongodb.org/mongo-driver/v2@v2.2.0/mongo/options/clientoptions.go\")",
11+
"mcp__github__get_file_contents",
12+
"mcp__github__create_branch",
13+
"mcp__github__push_files",
14+
"mcp__github__create_pull_request",
15+
"Bash(git commit -m ' *)"
16+
]
17+
}
18+
}

.claude/settings.local.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(ls:*)",
5+
"Bash(gh run *)",
6+
"Bash(git add *)",
7+
"Bash(git commit -m ' *)"
8+
]
9+
}
10+
}

kanopy/production/secrets.yaml

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Kubernetes Secrets for github-copier - PRODUCTION
2+
# DO NOT commit this file with actual secret values!
3+
# This is a template - create secrets using kubectl or sealed-secrets
4+
5+
# IMPORTANT: Secret names are prefixed with "github-copier-" to avoid
6+
# collisions with other apps in the "docs" namespace
7+
8+
---
9+
apiVersion: v1
10+
kind: Secret
11+
metadata:
12+
name: github-copier-app-credentials
13+
namespace: docs
14+
labels:
15+
app: github-copier
16+
environment: production
17+
managed-by: manual
18+
type: Opaque
19+
stringData:
20+
# Get these from your GitHub App settings
21+
# https://github.com/organizations/YOUR_ORG/settings/apps/YOUR_APP
22+
# Using same GitHub App as staging (MongoDB's official app)
23+
GITHUB_APP_ID: "1166559"
24+
INSTALLATION_ID: "95537167"
25+
26+
---
27+
apiVersion: v1
28+
kind: Secret
29+
metadata:
30+
name: github-copier-pem
31+
namespace: docs
32+
labels:
33+
app: github-copier
34+
environment: production
35+
managed-by: manual
36+
type: Opaque
37+
stringData:
38+
# GitHub App private key (PEM file content)
39+
# Download from GitHub App settings → Generate a private key
40+
# Paste the entire contents of the .pem file here
41+
# The app reads this via GITHUB_APP_PRIVATE_KEY env var when SKIP_SECRET_MANAGER=true
42+
GITHUB_APP_PRIVATE_KEY: |
43+
-----BEGIN RSA PRIVATE KEY-----
44+
MIIEpAIBAAKCAQEA3w9mr0FYn5UCsQvurJzWQc8yc71ixt+hTl//l5yoziD/D0B/
45+
t2vYPaShFbbgaXseC84HgUzyPvVKncBrjsK3exAY5R7becXUkkuInL0AgtVnn0sa
46+
Zd9cXTKjVDpwX3dwUW6HI1p3tCUrbHm8q8umPDcWFTleDvMbp9bHVa/gUSzrmOQ/
47+
JXwueskQtI2HkIwybkUNFUdvqlMGA66jBLGW5keQwiAeSGeRvrd8p4WxXGU9vrQb
48+
KoNSbztYqXSxpflQTWv0R2R7e7YpPCmHWUCrZd3d9GBl6pEP8Kh5ppnaLyIcej24
49+
sXk51imdK24kp1lB18TaVy84537lMn820rcnxwIDAQABAoIBAQCjOovz96e4r7xW
50+
ftrbabHIWq0a0R31VjSeO9W3xqYooTDEonhTaxHEmjJex4KU7clg7hXD4uDqfWlq
51+
4yJSR98oqPDuyZPmGoShwbBosk4rb6rygG0C216lvKaUvmb5FVgV3wH5Nvyd0Q1j
52+
xfnw5YfyTIVWZrKT0gcM3TjlVVybSRvPjYvGeOLUUa0Kl0p58a+NLOdtQp/qep+3
53+
iXW061E9ZON1wS9ai1MzeCisPzbJj9mVqDIRM7dTII+v1/Sz+vQEqp2dpA9kzxf/
54+
g6BaA4kODf19e1V7bvXjpGMZpiQCD7AojHBRziUwIka/3qsx+Erc2zah24FvIGKW
55+
+6LM6RZRAoGBAPnx0LF0r5PQyBjf6jG/Gpsj3zElHConfr+Km8c8ybqnc6DGrz2n
56+
hEFPrfeDyob5518dKchBljFkxzF5GbfJhxjBBPpAgH1ZqBbImE1sv1NeSrtGY2lO
57+
AMTT6BDC5gSWzuOr8fQuytpMKa3xn7CzR+kesHrwTf1I3Q8tmCbT4ZQzAoGBAOR2
58+
2HndfEgW4ryvIaXnKGjd4m8rjHhlzxXwXyy2s4EceMf3D3E5y/pRGuZdIvk6bpCX
59+
jzcHAHMn+/KTSraqqm/VmrCTGbAAWSQ2i5RRzMDkO0j3dMQ0xOXQrEKYLHxaaqIa
60+
H9z9P/pcbT3M6g4vr3FxRbhLl1kXveTiLtlfwyodAoGAGDXADTBMQXkbqK0ntiHY
61+
peZXnB8IF10z/cGjTS0qLRZDMPONzd8IBHt/UlZFq8VRD/l3LpqLvcURNk2QnCi9
62+
GTy5CrRsvbeNfId5mSZLcfvUKUesIWsYz/fmppj6Rr+E0rC5Avn1VCfEccDRwv/a
63+
m04Jsh7Mrxf7sZitADXNx9kCgYEArBPfxRpPy7SBHwEB5QWQ6DuHm5g/e0ejjLC5
64+
3uHVnj+lsWei1/Nica25Bx3Lg9DBCmM9WYMKkbHiELlkIuW07fsDQk/pnykD+0Si
65+
KRrbj8XX6YOJDWd5jmd398jKaPdpLaNjsH2HPLl1BoFbIUhFarqYetFks5QwUT8T
66+
bmK0ivkCgYBVtE5soAjKpLuyg1IJ7us6t7+vtoaVsk/CdWm6PSp/G95UbxDVSu9U
67+
uM8yPR46jvqxGOywbDLseoAX172n29qJZKB8zzmfh73UMDmCQwYuIQ2Cbk5e7khM
68+
/oGWCRKANHdg211nDOfp/GiFJPeUAT4ccGtzVMFFN5jvdEXp2VjoWw==
69+
-----END RSA PRIVATE KEY-----
70+
71+
---
72+
apiVersion: v1
73+
kind: Secret
74+
metadata:
75+
name: github-copier-webhook-secret
76+
namespace: docs
77+
labels:
78+
app: github-copier
79+
environment: production
80+
managed-by: manual
81+
type: Opaque
82+
stringData:
83+
# Webhook secret for validating GitHub webhook signatures
84+
# Generate a strong random secret:
85+
# openssl rand -hex 32
86+
# Configure the same secret in GitHub App webhook settings
87+
WEBHOOK_SECRET: "50ed9e79a524fb8459f2f1b7d80c451871d47b300f4757c66148c872454c0061"
88+
89+
---
90+
# Optional: MongoDB URI for audit logging
91+
# Uncomment if you want to enable audit logging
92+
# apiVersion: v1
93+
# kind: Secret
94+
# metadata:
95+
# name: github-copier-mongo-uri
96+
# namespace: docs
97+
# labels:
98+
# app: github-copier
99+
# environment: production
100+
# managed-by: manual
101+
# type: Opaque
102+
# stringData:
103+
# MONGO_URI: "mongodb://username:password@host:port/database"
104+

kanopy/staging/secrets.yaml

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Kubernetes Secrets for github-copier
2+
# DO NOT commit this file with actual secret values!
3+
# This is a template - create secrets using kubectl or sealed-secrets
4+
5+
# IMPORTANT: Secret names are prefixed with "github-copier-" to avoid
6+
# collisions with other apps in the "docs" namespace
7+
8+
---
9+
apiVersion: v1
10+
kind: Secret
11+
metadata:
12+
name: github-copier-app-credentials
13+
namespace: docs
14+
labels:
15+
app: github-copier
16+
managed-by: manual
17+
type: Opaque
18+
stringData:
19+
# Get these from your GitHub App settings
20+
# https://github.com/organizations/YOUR_ORG/settings/apps/YOUR_APP
21+
GITHUB_APP_ID: "1166559"
22+
INSTALLATION_ID: "95537167"
23+
24+
---
25+
apiVersion: v1
26+
kind: Secret
27+
metadata:
28+
name: github-copier-pem
29+
namespace: docs
30+
labels:
31+
app: github-copier
32+
managed-by: manual
33+
type: Opaque
34+
stringData:
35+
# GitHub App private key (PEM file content)
36+
# Download from GitHub App settings → Generate a private key
37+
# Paste the entire contents of the .pem file here
38+
# The app reads this via GITHUB_APP_PRIVATE_KEY env var when SKIP_SECRET_MANAGER=true
39+
GITHUB_APP_PRIVATE_KEY: |
40+
-----BEGIN RSA PRIVATE KEY-----
41+
MIIEpAIBAAKCAQEA3w9mr0FYn5UCsQvurJzWQc8yc71ixt+hTl//l5yoziD/D0B/
42+
t2vYPaShFbbgaXseC84HgUzyPvVKncBrjsK3exAY5R7becXUkkuInL0AgtVnn0sa
43+
Zd9cXTKjVDpwX3dwUW6HI1p3tCUrbHm8q8umPDcWFTleDvMbp9bHVa/gUSzrmOQ/
44+
JXwueskQtI2HkIwybkUNFUdvqlMGA66jBLGW5keQwiAeSGeRvrd8p4WxXGU9vrQb
45+
KoNSbztYqXSxpflQTWv0R2R7e7YpPCmHWUCrZd3d9GBl6pEP8Kh5ppnaLyIcej24
46+
sXk51imdK24kp1lB18TaVy84537lMn820rcnxwIDAQABAoIBAQCjOovz96e4r7xW
47+
ftrbabHIWq0a0R31VjSeO9W3xqYooTDEonhTaxHEmjJex4KU7clg7hXD4uDqfWlq
48+
4yJSR98oqPDuyZPmGoShwbBosk4rb6rygG0C216lvKaUvmb5FVgV3wH5Nvyd0Q1j
49+
xfnw5YfyTIVWZrKT0gcM3TjlVVybSRvPjYvGeOLUUa0Kl0p58a+NLOdtQp/qep+3
50+
iXW061E9ZON1wS9ai1MzeCisPzbJj9mVqDIRM7dTII+v1/Sz+vQEqp2dpA9kzxf/
51+
g6BaA4kODf19e1V7bvXjpGMZpiQCD7AojHBRziUwIka/3qsx+Erc2zah24FvIGKW
52+
+6LM6RZRAoGBAPnx0LF0r5PQyBjf6jG/Gpsj3zElHConfr+Km8c8ybqnc6DGrz2n
53+
hEFPrfeDyob5518dKchBljFkxzF5GbfJhxjBBPpAgH1ZqBbImE1sv1NeSrtGY2lO
54+
AMTT6BDC5gSWzuOr8fQuytpMKa3xn7CzR+kesHrwTf1I3Q8tmCbT4ZQzAoGBAOR2
55+
2HndfEgW4ryvIaXnKGjd4m8rjHhlzxXwXyy2s4EceMf3D3E5y/pRGuZdIvk6bpCX
56+
jzcHAHMn+/KTSraqqm/VmrCTGbAAWSQ2i5RRzMDkO0j3dMQ0xOXQrEKYLHxaaqIa
57+
H9z9P/pcbT3M6g4vr3FxRbhLl1kXveTiLtlfwyodAoGAGDXADTBMQXkbqK0ntiHY
58+
peZXnB8IF10z/cGjTS0qLRZDMPONzd8IBHt/UlZFq8VRD/l3LpqLvcURNk2QnCi9
59+
GTy5CrRsvbeNfId5mSZLcfvUKUesIWsYz/fmppj6Rr+E0rC5Avn1VCfEccDRwv/a
60+
m04Jsh7Mrxf7sZitADXNx9kCgYEArBPfxRpPy7SBHwEB5QWQ6DuHm5g/e0ejjLC5
61+
3uHVnj+lsWei1/Nica25Bx3Lg9DBCmM9WYMKkbHiELlkIuW07fsDQk/pnykD+0Si
62+
KRrbj8XX6YOJDWd5jmd398jKaPdpLaNjsH2HPLl1BoFbIUhFarqYetFks5QwUT8T
63+
bmK0ivkCgYBVtE5soAjKpLuyg1IJ7us6t7+vtoaVsk/CdWm6PSp/G95UbxDVSu9U
64+
uM8yPR46jvqxGOywbDLseoAX172n29qJZKB8zzmfh73UMDmCQwYuIQ2Cbk5e7khM
65+
/oGWCRKANHdg211nDOfp/GiFJPeUAT4ccGtzVMFFN5jvdEXp2VjoWw==
66+
-----END RSA PRIVATE KEY-----
67+
68+
---
69+
apiVersion: v1
70+
kind: Secret
71+
metadata:
72+
name: github-copier-webhook-secret
73+
namespace: docs
74+
labels:
75+
app: github-copier
76+
managed-by: manual
77+
type: Opaque
78+
stringData:
79+
# Webhook secret for validating GitHub webhook signatures
80+
# Generate a strong random secret:
81+
# openssl rand -hex 32
82+
# Configure the same secret in GitHub App webhook settings
83+
WEBHOOK_SECRET: "50ed9e79a524fb8459f2f1b7d80c451871d47b300f4757c66148c872454c0061"
84+
85+
---
86+
# Optional: MongoDB URI for audit logging
87+
# Uncomment if you want to enable audit logging
88+
# apiVersion: v1
89+
# kind: Secret
90+
# metadata:
91+
# name: github-copier-mongo-uri
92+
# namespace: docs
93+
# labels:
94+
# app: github-copier
95+
# managed-by: manual
96+
# type: Opaque
97+
# stringData:
98+
# MONGO_URI: "mongodb://username:password@host:port/database"
99+

services/webhook_handler_new.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,8 +437,10 @@ func loadAndMatchWorkflows(ctx context.Context, config *configs.Config, containe
437437
"matching_count": len(matching),
438438
})
439439

440-
yamlConfig.Workflows = matching
441-
return yamlConfig, nil
440+
// Return a shallow copy with only matching workflows to avoid mutating the cached config
441+
filteredConfig := *yamlConfig
442+
filteredConfig.Workflows = matching
443+
return &filteredConfig, nil
442444
}
443445

444446
// fetchChangedFiles retrieves the files changed in a PR, logging and notifying on error.

0 commit comments

Comments
 (0)