Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f081273
Add Conversation V2 and activity replies
harshach Aug 4, 2026
b2508a7
Update generated TypeScript types
github-actions[bot] Aug 4, 2026
f07a293
Preserve conversation reactions on patch
harshach Aug 4, 2026
d9889a9
Fix Conversation V2 CI regressions
harshach Aug 4, 2026
525e8fa
Fix Task V2 UI test mocks
harshach Aug 4, 2026
8f00460
Merge branch 'main' into harshach/activity-replies
harshach Aug 4, 2026
c8b0038
fix: address conversation v2 CI regressions
harshach Aug 4, 2026
0ad853b
Merge branch 'main' into harshach/activity-replies
harshach Aug 4, 2026
ae7cc6b
fix(ui): preserve conversations in activity feeds
harshach Aug 4, 2026
4ceb999
Fix playwright tests
aniketkatkar97 Aug 5, 2026
caa1108
Merge branch 'main' into harshach/activity-replies
mohityadav766 Aug 6, 2026
cd6cbd7
perf(migration): page legacy thread scans in v200 migrations
harshach Aug 6, 2026
4590bd7
fix(notifications): skip malformed webhook receivers individually
harshach Aug 6, 2026
53333dc
Merge branch 'main' into harshach/activity-replies
harshach Aug 6, 2026
9b91ace
Merge remote-tracking branch 'origin/main' into harshach/activity-rep…
aniketkatkar97 Aug 7, 2026
041d982
Merge branch 'main' into harshach/activity-replies
aniketkatkar97 Aug 7, 2026
3192e13
Merge branch 'main' into harshach/activity-replies
harshach Aug 7, 2026
093ade6
test(search): assert column FQN search parity and ranking, not a sing…
harshach Aug 8, 2026
2b1f1a5
fix(ingestion): narrow the paged conversation response before mapping it
harshach Aug 8, 2026
0787270
Merge branch 'main' into harshach/activity-replies
harshach Aug 8, 2026
61a3297
fix(alerts): resolve test suites from the store for test-suite domain…
harshach Aug 8, 2026
531f476
Merge branch 'main' into harshach/activity-replies
harshach Aug 8, 2026
a18d26f
fix(prefect): construct schema types instead of passing raw values
harshach Aug 8, 2026
3d501bf
fix(prefect): keep _parse_timestamp's int contract, convert at the ca…
harshach Aug 9, 2026
da9a1aa
ci(integration): give the parallel lane 8 workers instead of 4
harshach Aug 9, 2026
80e6b1f
fix(tests): stop namespace cleanup paying a 5s poll delay per root
harshach Aug 9, 2026
7ecb054
Revert "ci(integration): give the parallel lane 8 workers instead of 4"
harshach Aug 9, 2026
ed84f03
Merge branch 'main' into harshach/activity-replies
harshach Aug 9, 2026
3cbbbf2
Merge remote-tracking branch 'origin/main' into resolve/activity-repl…
sonika-shah Aug 10, 2026
06d9e69
fix(ingestion): read the paged conversation shape defensively
harshach Aug 10, 2026
7de3e37
Merge remote-tracking branch 'origin/main' into harshach/activity-rep…
harshach Aug 21, 2026
19391fd
Merge remote-tracking branch 'origin/main' into harshach/activity-rep…
harshach Aug 24, 2026
7e929d8
fix: resolve post-merge backend compilation
harshach Aug 24, 2026
08c9407
fix: preserve user preferences on soft delete
harshach Aug 24, 2026
8aa5f06
Merge remote-tracking branch 'origin/main' into harshach/activity-rep…
harshach Aug 24, 2026
52fb0ff
fix: handle Vertica dialect type drift
harshach Aug 24, 2026
9aaaa08
fix: parse incident migration fixture statements
harshach Aug 24, 2026
12e3e74
Merge branch 'main' into harshach/activity-replies
karanh37 Aug 25, 2026
ced85bd
Merge branch 'main' into harshach/activity-replies
harsh-vador Aug 25, 2026
391fc98
Merge branch 'main' into harshach/activity-replies
harsh-vador Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/playwright/impact-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@
"openmetadata-ui/src/main/resources/ui/src/components/Auth/**",
"openmetadata-ui/src/main/resources/ui/src/pages/Login/**"
],
"projects": ["chromium", "Basic", "SearchRBAC", "ImportExport"],
"projects": ["chromium", "Basic", "Ingestion", "SearchRBAC", "ImportExport"],
"specs": [
"playwright/e2e/**/*Permission*.spec.ts",
"playwright/e2e/Pages/Login*.spec.ts",
Expand Down
15 changes: 9 additions & 6 deletions .github/scripts/select_playwright_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ def is_mapped_file(path: str, impact_map: dict[str, Any]) -> bool:
)


def remove_delegated_specs(
selected: dict[str, set[str]], delegated_patterns: list[str]
) -> None:
for spec in list(selected):
if matches(spec, delegated_patterns):
del selected[spec]


def write_github_output(path: Path, plan: dict[str, Any]) -> None:
direct_changed_specs = plan.get("directChangedSpecs", [])
lineage_representative_only = (
Expand Down Expand Up @@ -144,12 +152,7 @@ def main() -> None:
for entry in impact_map["canary"]:
add_selection(selected, entry, repo_root)

delegated_patterns = impact_map.get("delegatedSpecs", [])
selected = {
spec: projects
for spec, projects in selected.items()
if not matches(spec, delegated_patterns)
}
remove_delegated_specs(selected, impact_map.get("delegatedSpecs", []))

plan = {
"version": 1,
Expand Down
69 changes: 69 additions & 0 deletions .github/scripts/tests/test_playwright_ci_planning.py
Original file line number Diff line number Diff line change
Expand Up @@ -2375,6 +2375,75 @@ def test_changed_visual_regression_spec_is_delegated_not_selected(tmp_path, monk
assert selection["unmappedFiles"] == []


def test_impact_mapping_cannot_reselect_a_delegated_spec(tmp_path, monkeypatch):
selector = load_script("select_playwright_tests")
spec_dir = tmp_path / selector.UI_ROOT / "playwright/e2e/Features"
spec_dir.mkdir(parents=True)
spec_path = spec_dir / "Delegated.spec.ts"
spec_path.write_text("test('delegated', () => undefined);\n")
impact_map = tmp_path / "impact-map.json"
impact_map.write_text(
json.dumps(
{
"smoke": [],
"canary": [],
"delegatedSpecs": ["playwright/e2e/Features/Delegated.spec.ts"],
"sharedInfrastructure": [],
"mappings": [
{
"sources": ["src/rdf/**"],
"projects": ["chromium"],
"specs": ["playwright/e2e/Features/Delegated.spec.ts"],
}
],
}
)
)
changed = tmp_path / "changed.txt"
changed.write_text("src/rdf/Processor.java\n")
output = tmp_path / "selection.json"
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("GITHUB_OUTPUT", raising=False)
monkeypatch.setattr(
sys,
"argv",
[
"select_playwright_tests.py",
"--event-name",
"pull_request_target",
"--changed-files",
str(changed),
"--impact-map",
str(impact_map),
"--output",
str(output),
],
)

selector.main()

selection = json.loads(output.read_text())
assert selection["selectors"] == []


def test_security_impact_mapping_includes_ingestion_permission_specs():
selector = load_script("select_playwright_tests")
impact_map = json.loads(
(SCRIPTS.parents[0] / "playwright/impact-map.json").read_text()
)
mapping = next(
entry
for entry in impact_map["mappings"]
if "openmetadata-service/src/main/java/org/openmetadata/service/security/**"
in entry["sources"]
)

assert "Ingestion" in mapping["projects"]
assert selector.matches(
"playwright/e2e/Flow/ServiceCreationPermissions.spec.ts", mapping["specs"]
)


def test_summary_reconciles_results_and_evaluates_performance_independently():
# The playwright-summary job lives in the postgres PR caller (not the
# reusable) so branch protection can require its unprefixed check name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,18 @@ ON DUPLICATE KEY UPDATE
test_case_incident.createdAt = LEAST(test_case_incident.createdAt, VALUES(createdAt)),
updatedAt = VALUES(updatedAt),
latestRecordId = VALUES(latestRecordId);

-- Activity comments are retained indefinitely unless an administrator explicitly configures a
-- positive retention period. Preserve any value already chosen by an administrator.
UPDATE installed_apps
SET json = JSON_INSERT(json, '$.appConfiguration.activityCommentsRetentionPeriod', 0)
WHERE name = 'DataRetentionApplication';

UPDATE apps_marketplace
SET json = JSON_INSERT(json, '$.appConfiguration.activityCommentsRetentionPeriod', 0)
WHERE name = 'DataRetentionApplication';

UPDATE entity_extension
SET json = JSON_INSERT(json, '$.appConfiguration.activityCommentsRetentionPeriod', 0)
WHERE extension LIKE 'app.version.%'
AND json->>'$.name' = 'DataRetentionApplication';
98 changes: 98 additions & 0 deletions bootstrap/sql/migrations/native/2.1.0/mysql/schemaChanges.sql
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,104 @@ CREATE TABLE IF NOT EXISTS test_case_incident (
INDEX idx_tci_updated (updatedAt)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- Conversation V2 stores bounded roots and replies as schema-first JSON. Indexed mentions and
-- domains remain normalized because they participate in filters and authorization.
CREATE TABLE IF NOT EXISTS conversation_entity (
id varchar(36) GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.id'))) STORED NOT NULL,
source varchar(16) GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.source'))) STORED NOT NULL,
entityType varchar(64) GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.entityRef.type'))) STORED NOT NULL,
entityId varchar(36) GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.entityRef.id'))) STORED NOT NULL,
entityFqnHash varchar(768) CHARACTER SET ascii COLLATE ascii_bin,
about varchar(2048) GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.about'))) STORED NOT NULL,
aboutFqnHash varchar(768) CHARACTER SET ascii COLLATE ascii_bin,
activityEventId varchar(36) GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.activityEventId'))) STORED,
creatorId varchar(36) GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.createdBy.id'))) STORED,
createdAt bigint GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.createdAt'))) STORED NOT NULL,
updatedAt bigint GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.updatedAt'))) STORED NOT NULL,
resolved tinyint(1) GENERATED ALWAYS AS
(json_extract(json, _utf8mb4'$.resolved')) STORED NOT NULL,
replyCount int GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.replyCount'))) STORED NOT NULL,
json json NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_conversation_activity_event (activityEventId),
KEY idx_conversation_entity (entityType, entityId, updatedAt, id),
KEY idx_conversation_entity_fqn (entityFqnHash, updatedAt, id),
KEY idx_conversation_about (aboutFqnHash, updatedAt, id),
KEY idx_conversation_creator (creatorId, updatedAt, id),
KEY idx_conversation_source_updated (source, updatedAt, id),
KEY idx_conversation_created (createdAt, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE IF NOT EXISTS conversation_reply (
id varchar(36) GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.id'))) STORED NOT NULL,
conversationId varchar(36) GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.conversationId'))) STORED NOT NULL,
authorId varchar(36) GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.author.id'))) STORED NOT NULL,
createdAt bigint GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.createdAt'))) STORED NOT NULL,
updatedAt bigint GENERATED ALWAYS AS
(json_unquote(json_extract(json, _utf8mb4'$.updatedAt'))) STORED NOT NULL,
json json NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_conversation_reply_conversation
FOREIGN KEY (conversationId) REFERENCES conversation_entity(id) ON DELETE CASCADE,
KEY idx_conversation_reply_cursor (conversationId, createdAt, id),
KEY idx_conversation_reply_author (authorId, createdAt, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE IF NOT EXISTS conversation_mention (
conversationId varchar(36) NOT NULL,
targetType varchar(16) NOT NULL,
targetId varchar(36) NOT NULL,
mentionedEntityType varchar(64) NOT NULL,
mentionedEntityId varchar(36) NOT NULL,
createdAt bigint NOT NULL,
PRIMARY KEY (targetType, targetId, mentionedEntityType, mentionedEntityId),
CONSTRAINT fk_conversation_mention_conversation
FOREIGN KEY (conversationId) REFERENCES conversation_entity(id) ON DELETE CASCADE,
KEY idx_conversation_mention_lookup
(mentionedEntityType, mentionedEntityId, createdAt, conversationId),
KEY idx_conversation_mention_conversation (conversationId, targetType, targetId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE IF NOT EXISTS conversation_domain (
conversationId varchar(36) NOT NULL,
domainId varchar(36) NOT NULL,
PRIMARY KEY (conversationId, domainId),
CONSTRAINT fk_conversation_domain_conversation
FOREIGN KEY (conversationId) REFERENCES conversation_entity(id) ON DELETE CASCADE,
KEY idx_conversation_domain_lookup (domainId, conversationId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

SET @drop_conversation_activity_timestamp_ddl = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'conversation_entity'
AND column_name = 'activityTimestamp'
),
'ALTER TABLE conversation_entity DROP COLUMN activityTimestamp',
'SELECT 1'
)
);
PREPARE drop_conversation_activity_timestamp_stmt
FROM @drop_conversation_activity_timestamp_ddl;
EXECUTE drop_conversation_activity_timestamp_stmt;
DEALLOCATE PREPARE drop_conversation_activity_timestamp_stmt;
-- Pipeline-backed lineage is the only relationship lookup whose selective identifier lives in JSON.
-- Pairing it with relation serves every pipeline lineage path without widening the generic table schema.
CREATE INDEX idx_entity_relationship_pipeline_relation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,24 @@ ON CONFLICT (stateId) DO UPDATE SET
createdAt = LEAST(test_case_incident.createdAt, EXCLUDED.createdAt),
updatedAt = EXCLUDED.updatedAt,
latestRecordId = EXCLUDED.latestRecordId;

-- Activity comments are retained indefinitely unless an administrator explicitly configures a
-- positive retention period. Preserve any value already chosen by an administrator.
UPDATE installed_apps
SET json = jsonb_set(
json::jsonb, '{appConfiguration,activityCommentsRetentionPeriod}', '0'::jsonb, true)
WHERE name = 'DataRetentionApplication'
AND NOT jsonb_exists(json::jsonb #> '{appConfiguration}', 'activityCommentsRetentionPeriod');

UPDATE apps_marketplace
SET json = jsonb_set(
json::jsonb, '{appConfiguration,activityCommentsRetentionPeriod}', '0'::jsonb, true)
WHERE name = 'DataRetentionApplication'
AND NOT jsonb_exists(json::jsonb #> '{appConfiguration}', 'activityCommentsRetentionPeriod');

UPDATE entity_extension
SET json = jsonb_set(
json::jsonb, '{appConfiguration,activityCommentsRetentionPeriod}', '0'::jsonb, true)
WHERE extension LIKE 'app.version.%'
AND json::jsonb ->> 'name' = 'DataRetentionApplication'
AND NOT jsonb_exists(json::jsonb #> '{appConfiguration}', 'activityCommentsRetentionPeriod');
92 changes: 92 additions & 0 deletions bootstrap/sql/migrations/native/2.1.0/postgres/schemaChanges.sql
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,98 @@ CREATE INDEX IF NOT EXISTS idx_tci_fqn ON test_case_incident (entityFQNHash);
CREATE INDEX IF NOT EXISTS idx_tci_assignee ON test_case_incident (assignee, testCaseResolutionStatusType);
CREATE INDEX IF NOT EXISTS idx_tci_updated ON test_case_incident (updatedAt);

-- Conversation V2 stores bounded roots and replies as schema-first JSON. Indexed mentions and
-- domains remain normalized because they participate in filters and authorization.
CREATE TABLE IF NOT EXISTS conversation_entity (
id character varying(36) GENERATED ALWAYS AS ((json ->> 'id'::text)) STORED NOT NULL,
source character varying(16) GENERATED ALWAYS AS ((json ->> 'source'::text)) STORED NOT NULL,
entityType character varying(64) GENERATED ALWAYS AS
((json #>> '{entityRef,type}'::text[])) STORED NOT NULL,
entityId character varying(36) GENERATED ALWAYS AS
((json #>> '{entityRef,id}'::text[])) STORED NOT NULL,
entityFqnHash character varying(768),
about character varying(2048) GENERATED ALWAYS AS ((json ->> 'about'::text)) STORED NOT NULL,
aboutFqnHash character varying(768),
activityEventId character varying(36) GENERATED ALWAYS AS
((json ->> 'activityEventId'::text)) STORED,
creatorId character varying(36) GENERATED ALWAYS AS
((json #>> '{createdBy,id}'::text[])) STORED,
createdAt bigint GENERATED ALWAYS AS
(((json ->> 'createdAt'::text))::bigint) STORED NOT NULL,
updatedAt bigint GENERATED ALWAYS AS
(((json ->> 'updatedAt'::text))::bigint) STORED NOT NULL,
resolved boolean GENERATED ALWAYS AS
(((json ->> 'resolved'::text))::boolean) STORED NOT NULL,
replyCount integer GENERATED ALWAYS AS
(((json ->> 'replyCount'::text))::integer) STORED NOT NULL,
json jsonb NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_conversation_activity_event UNIQUE (activityEventId)
);

CREATE INDEX IF NOT EXISTS idx_conversation_entity
ON conversation_entity (entityType, entityId, updatedAt DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_conversation_entity_fqn
ON conversation_entity (entityFqnHash, updatedAt DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_conversation_about
ON conversation_entity (aboutFqnHash, updatedAt DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_conversation_creator
ON conversation_entity (creatorId, updatedAt DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_conversation_source_updated
ON conversation_entity (source, updatedAt DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_conversation_created
ON conversation_entity (createdAt, id);

CREATE TABLE IF NOT EXISTS conversation_reply (
id character varying(36) GENERATED ALWAYS AS ((json ->> 'id'::text)) STORED NOT NULL,
conversationId character varying(36) GENERATED ALWAYS AS
((json ->> 'conversationId'::text)) STORED NOT NULL,
authorId character varying(36) GENERATED ALWAYS AS
((json #>> '{author,id}'::text[])) STORED NOT NULL,
createdAt bigint GENERATED ALWAYS AS
(((json ->> 'createdAt'::text))::bigint) STORED NOT NULL,
updatedAt bigint GENERATED ALWAYS AS
(((json ->> 'updatedAt'::text))::bigint) STORED NOT NULL,
json jsonb NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_conversation_reply_conversation
FOREIGN KEY (conversationId) REFERENCES conversation_entity(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_conversation_reply_cursor
ON conversation_reply (conversationId, createdAt, id);
CREATE INDEX IF NOT EXISTS idx_conversation_reply_author
ON conversation_reply (authorId, createdAt, id);

CREATE TABLE IF NOT EXISTS conversation_mention (
conversationId character varying(36) NOT NULL,
targetType character varying(16) NOT NULL,
targetId character varying(36) NOT NULL,
mentionedEntityType character varying(64) NOT NULL,
mentionedEntityId character varying(36) NOT NULL,
createdAt bigint NOT NULL,
PRIMARY KEY (targetType, targetId, mentionedEntityType, mentionedEntityId),
CONSTRAINT fk_conversation_mention_conversation
FOREIGN KEY (conversationId) REFERENCES conversation_entity(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_conversation_mention_lookup
ON conversation_mention (mentionedEntityType, mentionedEntityId, createdAt, conversationId);
CREATE INDEX IF NOT EXISTS idx_conversation_mention_conversation
ON conversation_mention (conversationId, targetType, targetId);

CREATE TABLE IF NOT EXISTS conversation_domain (
conversationId character varying(36) NOT NULL,
domainId character varying(36) NOT NULL,
PRIMARY KEY (conversationId, domainId),
CONSTRAINT fk_conversation_domain_conversation
FOREIGN KEY (conversationId) REFERENCES conversation_entity(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_conversation_domain_lookup
ON conversation_domain (domainId, conversationId);

ALTER TABLE conversation_entity DROP COLUMN IF EXISTS activityTimestamp;
-- Pipeline-backed lineage is the only relationship lookup whose selective identifier lives in JSON.
-- The partial index avoids write amplification for relationships that have no pipeline metadata.
CREATE INDEX IF NOT EXISTS idx_entity_relationship_pipeline_relation
Expand Down
Loading
Loading