Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 87 additions & 87 deletions .flox/env/manifest.lock
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,93 @@
"group": "cmake",
"priority": 5
},
{
"attr_path": "depot-cli",
"broken": false,
"derivation": "/nix/store/8hnwcq7wrlzwizkrxjhib7bx82llp5xi-depot-cli-2.102.9.drv",
"description": "Official CLI for the Depot Docker image builder",
"install_id": "depot-cli",
"license": "MIT",
"locked_url": "https://github.com/flox/nixpkgs?rev=6774f7bc253789b113a4f39285dc0fa100abeacc",
"name": "depot-cli-2.102.9",
"pname": "depot-cli",
"rev": "6774f7bc253789b113a4f39285dc0fa100abeacc",
"rev_count": 1077996,
"rev_date": "2026-09-22T03:11:10Z",
"scrape_date": "2026-09-23T04:58:05.803991Z",
"stabilities": [
"unstable"
],
"unfree": false,
"version": "2.102.9",
"outputs_to_install": [
"out"
],
"outputs": {
"out": "/nix/store/1bi1y13kr74rlmj8j4vslq993j1vdm5c-depot-cli-2.102.9"
},
"system": "aarch64-darwin",
"group": "depot",
"priority": 5
},
{
"attr_path": "depot-cli",
"broken": false,
"derivation": "/nix/store/zkw5dmf93pfr1wm7f2622g7lyw8yz10w-depot-cli-2.102.9.drv",
"description": "Official CLI for the Depot Docker image builder",
"install_id": "depot-cli",
"license": "MIT",
"locked_url": "https://github.com/flox/nixpkgs?rev=6774f7bc253789b113a4f39285dc0fa100abeacc",
"name": "depot-cli-2.102.9",
"pname": "depot-cli",
"rev": "6774f7bc253789b113a4f39285dc0fa100abeacc",
"rev_count": 1077996,
"rev_date": "2026-09-22T03:11:10Z",
"scrape_date": "2026-09-23T05:29:39.537297Z",
"stabilities": [
"unstable"
],
"unfree": false,
"version": "2.102.9",
"outputs_to_install": [
"out"
],
"outputs": {
"out": "/nix/store/ld8k0nmfl02ysjkkr9l2m5i3hy893sn3-depot-cli-2.102.9"
},
"system": "aarch64-linux",
"group": "depot",
"priority": 5
},
{
"attr_path": "depot-cli",
"broken": false,
"derivation": "/nix/store/vjh6m3hgsafr06qx6ajfmva818rfwq2f-depot-cli-2.102.9.drv",
"description": "Official CLI for the Depot Docker image builder",
"install_id": "depot-cli",
"license": "MIT",
"locked_url": "https://github.com/flox/nixpkgs?rev=6774f7bc253789b113a4f39285dc0fa100abeacc",
"name": "depot-cli-2.102.9",
"pname": "depot-cli",
"rev": "6774f7bc253789b113a4f39285dc0fa100abeacc",
"rev_count": 1077996,
"rev_date": "2026-09-22T03:11:10Z",
"scrape_date": "2026-09-23T06:06:17.967455Z",
"stabilities": [
"unstable"
],
"unfree": false,
"version": "2.102.9",
"outputs_to_install": [
"out"
],
"outputs": {
"out": "/nix/store/y9pf0adrvbwj3ki0ab9nch76p9y1d05v-depot-cli-2.102.9"
},
"system": "x86_64-linux",
"group": "depot",
"priority": 5
},
{
"attr_path": "emscripten",
"broken": false,
Expand Down Expand Up @@ -3511,93 +3598,6 @@
"system": "x86_64-linux",
"group": "uv",
"priority": 5
},
{
"attr_path": "depot-cli",
"broken": false,
"derivation": "/nix/store/8hnwcq7wrlzwizkrxjhib7bx82llp5xi-depot-cli-2.102.9.drv",
"description": "Official CLI for the Depot Docker image builder",
"install_id": "depot-cli",
"license": "MIT",
"locked_url": "https://github.com/flox/nixpkgs?rev=6774f7bc253789b113a4f39285dc0fa100abeacc",
"name": "depot-cli-2.102.9",
"pname": "depot-cli",
"rev": "6774f7bc253789b113a4f39285dc0fa100abeacc",
"rev_count": 1077996,
"rev_date": "2026-09-22T03:11:10Z",
"scrape_date": "2026-09-23T04:58:05.803991Z",
"stabilities": [
"unstable"
],
"unfree": false,
"version": "2.102.9",
"outputs_to_install": [
"out"
],
"outputs": {
"out": "/nix/store/1bi1y13kr74rlmj8j4vslq993j1vdm5c-depot-cli-2.102.9"
},
"system": "aarch64-darwin",
"group": "depot",
"priority": 5
},
{
"attr_path": "depot-cli",
"broken": false,
"derivation": "/nix/store/zkw5dmf93pfr1wm7f2622g7lyw8yz10w-depot-cli-2.102.9.drv",
"description": "Official CLI for the Depot Docker image builder",
"install_id": "depot-cli",
"license": "MIT",
"locked_url": "https://github.com/flox/nixpkgs?rev=6774f7bc253789b113a4f39285dc0fa100abeacc",
"name": "depot-cli-2.102.9",
"pname": "depot-cli",
"rev": "6774f7bc253789b113a4f39285dc0fa100abeacc",
"rev_count": 1077996,
"rev_date": "2026-09-22T03:11:10Z",
"scrape_date": "2026-09-23T05:29:39.537297Z",
"stabilities": [
"unstable"
],
"unfree": false,
"version": "2.102.9",
"outputs_to_install": [
"out"
],
"outputs": {
"out": "/nix/store/ld8k0nmfl02ysjkkr9l2m5i3hy893sn3-depot-cli-2.102.9"
},
"system": "aarch64-linux",
"group": "depot",
"priority": 5
},
{
"attr_path": "depot-cli",
"broken": false,
"derivation": "/nix/store/vjh6m3hgsafr06qx6ajfmva818rfwq2f-depot-cli-2.102.9.drv",
"description": "Official CLI for the Depot Docker image builder",
"install_id": "depot-cli",
"license": "MIT",
"locked_url": "https://github.com/flox/nixpkgs?rev=6774f7bc253789b113a4f39285dc0fa100abeacc",
"name": "depot-cli-2.102.9",
"pname": "depot-cli",
"rev": "6774f7bc253789b113a4f39285dc0fa100abeacc",
"rev_count": 1077996,
"rev_date": "2026-09-22T03:11:10Z",
"scrape_date": "2026-09-23T06:06:17.967455Z",
"stabilities": [
"unstable"
],
"unfree": false,
"version": "2.102.9",
"outputs_to_install": [
"out"
],
"outputs": {
"out": "/nix/store/y9pf0adrvbwj3ki0ab9nch76p9y1d05v-depot-cli-2.102.9"
},
"system": "x86_64-linux",
"group": "depot",
"priority": 5
}
]
}
19 changes: 10 additions & 9 deletions frontend/src/lib/integrations/GitHubIntegrationHelpers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { initKeaTests } from '~/test/init'

import { useRepositories } from './GitHubIntegrationHelpers'

const REPOS = [{ id: 1, name: 'posthog', full_name: 'PostHog/posthog', pushed_at: '2026-01-02T00:00:00Z' }]
// A mixed-case short name, so a key that lowercases every mode fails the default-mode test below.
const REPOS = [{ id: 1, name: 'HouseWatch', full_name: 'PostHog/HouseWatch', pushed_at: '2026-01-02T00:00:00Z' }]

function OptionKeysProbe({ valueKey }: { valueKey?: 'name' | 'full_name' }): JSX.Element {
const { options, loading } = useRepositories(1, { valueKey })
Expand All @@ -32,18 +33,20 @@ describe('useRepositories', () => {
cleanup()
})

// The emitted `$github_event_received` event carries the qualified name (owner/repo). A picker
// that keys its option on the short name instead compiles a repository filter that no
// delivery can ever match - the bug this option exists to avoid.
it('keys options on the qualified name when valueKey is full_name', async () => {
// The emitted `$github_event_received` event carries the qualified name (owner/repo), lowercased
// to match the repository filter the API stores. A picker that keys its option on the short name
// compiles a repository filter that no delivery can ever match. One that keeps GitHub's casing
// leaves the stored value matching no option, so the picker offers it as a custom value beside
// the real repository and drops that repository's rich label.
it('keys options on the lowercased qualified name when valueKey is full_name', async () => {
render(
<Provider>
<OptionKeysProbe valueKey="full_name" />
</Provider>
)
await act(() => new Promise((r) => setTimeout(r, 500)))

expect(screen.getByTestId('option-keys')).toHaveTextContent('PostHog/posthog')
expect(screen.getByTestId('option-keys').textContent).toBe('posthog/housewatch')
})

it('keys options on the short name by default, unchanged for existing callers', async () => {
Expand All @@ -54,8 +57,6 @@ describe('useRepositories', () => {
)
await act(() => new Promise((r) => setTimeout(r, 500)))

const content = screen.getByTestId('option-keys')
expect(content).toHaveTextContent('posthog')
expect(content.textContent).not.toBe('PostHog/posthog')
expect(screen.getByTestId('option-keys').textContent).toBe('HouseWatch')
})
})
10 changes: 9 additions & 1 deletion frontend/src/lib/integrations/GitHubIntegrationHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,15 @@ export function useRepositories(
// Most-recently-pushed first so the repo the user is working in floats to the top.
[...repositories]
.sort((a, b) => pushedAtMs(b.pushed_at) - pushedAtMs(a.pushed_at))
.map((r) => ({ key: r[valueKey], label: r.full_name, labelComponent: <RepoOptionLabel repo={r} /> })),
// A qualified-name key is lowercased because the stored value is. The API lowercases
// a repository filter on save, while GitHub reports `full_name` in the owner's
// casing. Compared as-is, the stored value matches no option, so LemonInputSelect
// shows it as a custom value beside the real repository and drops the rich label.
.map((r) => ({
key: valueKey === 'full_name' ? r[valueKey].toLowerCase() : r[valueKey],
label: r.full_name,
labelComponent: <RepoOptionLabel repo={r} />,
})),
[repositories, valueKey]
)

Expand Down
39 changes: 39 additions & 0 deletions products/workflows/backend/api/hog_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,44 @@
prop["value"] = [item.split("|")[0] if isinstance(item, str) else item for item in value]


# Lowercasing only preserves meaning for an operator that compares the value as a literal
# string. `str.lower()` is not a case transform for a pattern - it turns "\D" into "\d", which
# inverts what the pattern matches, and "(?P<name>" into "(?p<name>", which RE2 refuses - and a
# presence operator carries the operator string rather than a repository name. The property
# compiler treats a missing operator as exact, as `_has_exact_string_filter` does below.
_LITERAL_REPOSITORY_OPERATORS = frozenset({"exact", "is_not"})


def _lowercase_repository_properties(properties: object) -> None:
if not isinstance(properties, list):
return
for prop in properties:
if not isinstance(prop, dict) or prop.get("key") != "repository":
continue
if (prop.get("operator") or "exact") not in _LITERAL_REPOSITORY_OPERATORS:
continue
value = prop.get("value")
if isinstance(value, str):
prop["value"] = value.lower()
elif isinstance(value, list):
prop["value"] = [item.lower() if isinstance(item, str) else item for item in value]
Comment thread
posthog[bot] marked this conversation as resolved.
Comment thread
posthog[bot] marked this conversation as resolved.


def _normalize_github_repository_filters(filters: dict) -> None:
"""Lowercase every `repository` filter value in place, to match the lowercased delivery property.

GitHub treats "PostHog/posthog" and "posthog/posthog" as the same repository, but the exact
filter does not, so a name typed in the wrong case compiles to a trigger that never fires.
The compiler ANDs the conditions on an event entry with the global ones, so a repository
filter written on the entry decides whether the trigger fires too.
"""
_lowercase_repository_properties(filters.get("properties"))
events = filters.get("events")
for event in events if isinstance(events, list) else []:
if isinstance(event, dict) and event.get("id") == "$github_event_received":
_lowercase_repository_properties(event.get("properties"))


# Exact is the only operator that names channels. Channel ids are opaque (C0...), so
# substring and regex matching can't narrow meaningfully and patterns like ".*" or "C"
# match every channel; presence operators match every message and carry the operator
Expand Down Expand Up @@ -1402,7 +1440,7 @@
}
)

def _validate_create_task_action(self, inputs: dict) -> None:

Check warning on line 1443 in products/workflows/backend/api/hog_flow.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

lint:complexity

`_validate_create_task_action` has cyclomatic complexity 14 (warn >10)

Check warning on line 1443 in products/workflows/backend/api/hog_flow.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

`_validate_create_task_action` has cyclomatic complexity 14 (warn >10)
"""Save-time checks for the "Create AI task" step beyond input shape: whether the
chosen connectors, model and repository are actually usable, and the parallel-run
limit is sane - so a misconfigured step fails here instead of only when it fires."""
Expand Down Expand Up @@ -1500,7 +1538,7 @@
{"template_id": "Run scout is only available in the project's main environment."}
)

def validate(self, data):

Check warning on line 1541 in products/workflows/backend/api/hog_flow.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

lint:complexity

`validate` has cyclomatic complexity 78 (warn >10)

Check warning on line 1541 in products/workflows/backend/api/hog_flow.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

`validate` has cyclomatic complexity 78 (warn >10)
is_draft = self.context.get("is_draft")
# Drafts from the web builder stay lenient (incomplete graphs save fine); programmatic callers
# (MCP/API) get full validation even on drafts so a broken or unsupported config fails at create
Expand Down Expand Up @@ -1674,6 +1712,7 @@
}
)
if _subscribes_to("$github_event_received"):
_normalize_github_repository_filters(filters)
if not is_draft and not _has_exact_string_filter(filters, "repository"):
raise serializers.ValidationError(
{
Expand Down Expand Up @@ -1903,7 +1942,7 @@

return data

def _validate_delay(self, data: dict, strict: bool) -> None:

Check warning on line 1945 in products/workflows/backend/api/hog_flow.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

lint:complexity

`_validate_delay` has cyclomatic complexity 15 (warn >10)

Check warning on line 1945 in products/workflows/backend/api/hog_flow.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

`_validate_delay` has cyclomatic complexity 15 (warn >10)
"""A delay waits either a fixed span or until a date carried by the person or event, never both."""
config = data.get("config") or {}
delay_until = config.get("delay_until")
Expand Down Expand Up @@ -3223,7 +3262,7 @@
"email_sending_resumed_at",
]

def validate(self, data):

Check warning on line 3265 in products/workflows/backend/api/hog_flow.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

lint:complexity

`validate` has cyclomatic complexity 24 (warn >10)

Check warning on line 3265 in products/workflows/backend/api/hog_flow.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

`validate` has cyclomatic complexity 24 (warn >10)
instance = cast(Optional[HogFlow], self.instance)
is_draft = self.context.get("is_draft")

Expand Down Expand Up @@ -4676,7 +4715,7 @@
context["event_source"] = get_event_source(self.request)
return context

def safely_get_queryset(self, queryset: QuerySet) -> QuerySet:

Check warning on line 4718 in products/workflows/backend/api/hog_flow.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

lint:complexity

`safely_get_queryset` has cyclomatic complexity 12 (warn >10)

Check warning on line 4718 in products/workflows/backend/api/hog_flow.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

`safely_get_queryset` has cyclomatic complexity 12 (warn >10)
if self.action == "list":
# `id` breaks ties so LIMIT/OFFSET paging stays stable: rows sharing an updated_at can
# otherwise repeat on one page and never appear on another.
Expand Down
50 changes: 50 additions & 0 deletions products/workflows/backend/api/test/test_hog_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2910,6 +2910,56 @@ def test_hog_flow_internal_event_trigger_stores_the_bare_slack_channel_id(self):
stored = response.json()["trigger"]["filters"]["properties"][0]["value"]
assert stored == ["C0ALERTS"]

def test_hog_flow_github_trigger_stores_the_repository_lowercased(self):
# GitHub deliveries carry the lowercased full name, so a filter typed in the org's
# casing compiles to an exact match that never fires. The compiler ANDs the conditions
# on the event entry with the global ones, so a repository named there needs it too.
trigger_action = {
"id": "trigger_node",
"name": "trigger_1",
"type": "trigger",
"config": {
"type": "internal-event",
"filters": {
"events": [
{
"id": "$github_event_received",
"type": "events",
"properties": [
{
"key": "repository",
"value": "PostHog/PostHog",
"operator": "exact",
"type": "event",
}
],
}
],
"properties": [
{"key": "repository", "value": ["PostHog/PostHog"], "operator": "exact", "type": "event"},
{"key": "event_type", "value": ["issues"], "operator": "exact", "type": "event"},
# A pattern is not a literal, so lowercasing it would change what it
# matches, or stop it compiling at all.
{
"key": "repository",
"value": "(?P<owner>.+)/PostHog",
"operator": "regex",
"type": "event",
},
],
},
},
}

hog_flow = {"name": "Test GitHub Flow", "status": "active", "actions": [trigger_action]}

response = self.client.post(f"/api/projects/{self.team.id}/hog_flows", hog_flow)
assert response.status_code == 201, response.json()
stored_filters = response.json()["trigger"]["filters"]
assert stored_filters["properties"][0]["value"] == ["posthog/posthog"]
assert stored_filters["events"][0]["properties"][0]["value"] == "posthog/posthog"
assert stored_filters["properties"][2]["value"] == "(?P<owner>.+)/PostHog"

@staticmethod
def _slack_trigger_action(properties: list[dict]) -> dict:
return {
Expand Down
4 changes: 3 additions & 1 deletion products/workflows/backend/github_workflow_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ def _event_properties(event_type: str, payload: dict[str, Any], *, integration_i
"integration_id": integration_id,
"event_type": event_type,
"action": payload.get("action"),
"repository": repository.get("full_name"),
# Lowercased on both sides of the match (the API lowercases a repository filter on save), so
# "PostHog/posthog" and "posthog/posthog" name the same repository like they do on GitHub.
"repository": (repository.get("full_name") or "").lower() or None,
Comment thread
posthog[bot] marked this conversation as resolved.
# A string, not a boolean: GitHub deliveries never reach ClickHouse, so a filter has no
# stored property definition to coerce "true"/"false" back into a real boolean, and an
# exact match against a raw boolean here would never match.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ def test_oversized_delivery_sheds_the_raw_payload_but_still_emits(produce, integ
properties = produce.call_args.args[1].properties
assert properties["github_event"] == {"truncated": True}
# The flat fields a trigger filters on still deliver, so the run isn't lost.
assert properties["repository"] == "PostHog/posthog"
assert properties["repository"] == "posthog/posthog"
assert properties["event_type"] == "push"


Expand Down Expand Up @@ -273,7 +273,7 @@ def test_properties_carry_what_a_filter_needs(produce, integration) -> None:
properties = produce.call_args.args[1].properties
assert properties["event_type"] == "issues"
assert properties["action"] == "opened"
assert properties["repository"] == "PostHog/posthog"
assert properties["repository"] == "posthog/posthog"
assert properties["title"] == "The database is on fire"
assert properties["github_event"] == ISSUE_EVENT

Expand Down
Loading