Skip to content

feat: add github_snowflake plugin for Snowflake-backed GitHub ingestion - #139

Open
mfrancisc wants to merge 15 commits into
konflux-ci:mainfrom
mfrancisc:githubsnowflake
Open

feat: add github_snowflake plugin for Snowflake-backed GitHub ingestion#139
mfrancisc wants to merge 15 commits into
konflux-ci:mainfrom
mfrancisc:githubsnowflake

Conversation

@mfrancisc

Copy link
Copy Markdown

Jira: https://redhat.atlassian.net/browse/DPROD-1374

Summary

  • Add github_snowflake plugin that reads GitHub data from Fivetran Snowflake (GITHUB_DB.MARTS) and writes into existing _tool_github_* tables, then runs adapted GitHub convertors (same pattern as jira_snowflake).
  • MVP syncs repos, pull requests, PR commits, PR reviews, requested reviewers, and accounts for per-repo scopes — enough for PR cycle time / review metrics without GitHub API calls.
  • Includes connection JWT/externalbrowser auth, migrations, unit tests for query builders, local testing guide, and owned-plugin registration in docs/upstream-diffs.md.

Scope / trade-offs

  • Pilot coverage today: konflux-ci org in MARTS.
  • Missing PR addition/deletion/comment counts (left at 0).
  • Actions jobs table does not exist in Snowflake.
  • Issues, workflow runs, deployments deferred to Phase 2.
  • A repo should be on either the GitHub API connection or the Snowflake connection, not both.

Ingest GitHub PR data from Fivetran GITHUB_DB.MARTS into existing
_tool_github_* tables and reuse adapted GitHub convertors, following
the jira_snowflake pattern for per-repo historical sync without API rate limits.

Co-Authored-By: Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@mfrancisc
mfrancisc requested a review from a team as a code owner July 31, 2026 13:35
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 31, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:36 PM UTC · Completed 1:55 PM UTC
Commit: 701e62a · View workflow run →

@qodo-app-for-konflux-ci

Copy link
Copy Markdown

PR Summary by Qodo

Add github_snowflake plugin to ingest GitHub PR data from Snowflake (Fivetran MARTS)

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add github_snowflake plugin to sync GitHub PR data from Snowflake into existing tool tables.
• Reuse adapted GitHub convertors to produce domain code-review entities without GitHub API calls.
• Add connection CRUD, migrations, unit tests for query builders, and local testing documentation.
Diagram

graph TD
  A["DevLake pipeline runner"] --> B["github_snowflake plugin"] --> C{{"Snowflake GITHUB_DB.MARTS"}}
  B --> D[("MySQL _tool_github_* tables")] --> E[("Domain tables (code/cross)")]
  F["REST client"] --> G["github_snowflake connections API"] --> D
  subgraph Legend
    direction LR
    _svc["Service/Component"] ~~~ _db[("Database")] ~~~ _ext{{"External system"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Add a raw-table layer for Snowflake ingestion
  • ➕ Aligns with standard DevLake pattern: raw -> tool -> domain
  • ➕ Simplifies full-sync cleanup semantics (raw-data scoped)
  • ➕ Easier to re-run convertors independently from sync
  • ➖ More schema/migration work and additional storage
  • ➖ Adds extra processing step for what is already structured data
  • ➖ Longer MVP timeline
2. Refactor GitHub convertors into shared package (avoid adapted copies)
  • ➕ Reduces long-term drift between github and github_snowflake convertors
  • ➕ Less duplicated logic and easier upstream syncs
  • ➖ Cross-plugin refactor risk; may impact existing github plugin behavior
  • ➖ Requires careful API boundary design to keep plugins loosely coupled
3. Extend existing github plugin with a Snowflake-backed source mode
  • ➕ Single GitHub plugin surface area for UI/pipelines
  • ➕ Avoids separate connection types and reduces user confusion
  • ➖ Mixes two ingestion backends with different guarantees and schemas
  • ➖ Harder to enforce “repo should not exist in both backends” constraint
  • ➖ Higher regression risk to the API-based ingestion path

Recommendation: The chosen approach (a separate github_snowflake plugin mirroring jira_snowflake’s ‘direct-to-tool-tables + _raw_data_params deletion’ pattern) is the right MVP trade-off: it minimizes schema churn, reuses existing tool-layer GitHub tables, and avoids GitHub API limits. Consider a follow-up to reduce convertor drift (shared convertor helpers) if Phase 2 expands scope significantly.

Files changed (33) +2998 / -1

Enhancement (18) +2131 / -0
connection_api.goAdd connection CRUD endpoints for Snowflake GitHub connections +75/-0

Add connection CRUD endpoints for Snowflake GitHub connections

• Introduces REST handlers for POST/GET/PATCH/DELETE of SnowflakeGithubConnection records using ConnectionApiHelper. Wired by impl.ApiResources under /plugins/github_snowflake/connections.

backend/plugins/github_snowflake/api/connection_api.go

github_snowflake.goAdd plugin entrypoint for direct-run execution +43/-0

Add plugin entrypoint for direct-run execution

• Adds the main package entrypoint exporting PluginEntry and CLI flags (githubId/fullName/timeAfter). Uses runner.DirectRun to execute the plugin with per-repo options.

backend/plugins/github_snowflake/github_snowflake.go

impl.goImplement github_snowflake plugin registration, subtasks, and task preparation +224/-0

Implement github_snowflake plugin registration, subtasks, and task preparation

• Defines the plugin meta/init/task/model/migration/api interfaces, registers subtasks for sync + convert, and loads connection config. Ensures a GithubRepo scope exists, opens the Snowflake DB, and closes it after execution; also registers a minimal github stub for didgen when needed.

backend/plugins/github_snowflake/impl/impl.go

connection.goAdd SnowflakeGithubConnection model with encrypted private key +51/-0

Add SnowflakeGithubConnection model with encrypted private key

• Defines the _tool_github_snowflake_connections table schema holding Snowflake account/user/authType/privateKey/database/schema/warehouse/role. Uses DevLake field-level encryption for the PEM private key.

backend/plugins/github_snowflake/models/connection.go

convert_accounts.goConvert GitHub accounts/repo-accounts into domain accounts +150/-0

Convert GitHub accounts/repo-accounts into domain accounts

• Adds a stateful converter from _tool_github_accounts to crossdomain.Account, scoped per repo/connection. Includes a second pass to create domain accounts for orphaned repo-account records without corresponding account rows.

backend/plugins/github_snowflake/tasks/convert_accounts.go

convert_pr_commits.goConvert tool PR commit records into domain PullRequestCommit +98/-0

Convert tool PR commit records into domain PullRequestCommit

• Adds a stateful converter from _tool_github_pull_request_commits joined to PRs into code.PullRequestCommit. Performs full-sync cleanup by deleting domain rows matching the converter’s _raw_data_params when not incremental.

backend/plugins/github_snowflake/tasks/convert_pr_commits.go

convert_pr_reviews.goConvert tool PR reviews into domain PullRequestComment (type=REVIEW) +110/-0

Convert tool PR reviews into domain PullRequestComment (type=REVIEW)

• Adds conversion from _tool_github_pull_request_reviews into code.PullRequestComment with Type=REVIEW and Status derived from review state. Deletes prior REVIEW comments for the same _raw_data_params on full sync.

backend/plugins/github_snowflake/tasks/convert_pr_reviews.go

convert_prs.goConvert tool pull requests into domain PullRequest +129/-0

Convert tool pull requests into domain PullRequest

• Converts _tool_github_pull_requests into code.PullRequest, including status normalization (OPEN/MERGED/CLOSED) and key PR fields. On full sync, deletes existing domain PRs matching _raw_data_params to avoid stale records.

backend/plugins/github_snowflake/tasks/convert_prs.go

convert_repo.goConvert tool repos into domain repo/board/cicd scope records +134/-0

Convert tool repos into domain repo/board/cicd scope records

• Converts _tool_github_repos into code.Repo, ticket.Board, crossdomain.BoardRepo, and devops.CicdScope with consistent domain IDs. Enables downstream metrics that expect these domain scope entities to exist.

backend/plugins/github_snowflake/tasks/convert_repo.go

convert_reviewers.goConvert requested reviewers into domain PullRequestReviewer +100/-0

Convert requested reviewers into domain PullRequestReviewer

• Converts _tool_github_reviewers into code.PullRequestReviewer, joining via PR scope and generating reviewer account IDs. Cleans up domain reviewer rows by _raw_data_params on full sync.

backend/plugins/github_snowflake/tasks/convert_reviewers.go

shared.goAdd URL derivation and nil-safe helpers +53/-0

Add URL derivation and nil-safe helpers

• Adds helpers for deriving repo/PR URLs and handling nullable Snowflake scan values. Centralizes common transformations used by sync tasks.

backend/plugins/github_snowflake/tasks/shared.go

sync_accounts.goSync Snowflake users into _tool_github_accounts and _tool_github_repo_accounts +152/-0

Sync Snowflake users into _tool_github_accounts and _tool_github_repo_accounts

• Queries Snowflake for distinct users referenced by PRs, merges, reviews, and requested reviewers for a repo. Upserts tool-layer account and repo-account records, selecting one email per user via QUALIFY.

backend/plugins/github_snowflake/tasks/sync_accounts.go

sync_pr_commits.goSync PR commits from Snowflake into tool-layer PR commit table +117/-0

Sync PR commits from Snowflake into tool-layer PR commit table

• Fetches commit-to-PR links via COMMIT_PULL_REQUEST joined to COMMIT and PULL_REQUEST to avoid orphan links. Supports optional incremental timeAfter filtering via sync policy.

backend/plugins/github_snowflake/tasks/sync_pr_commits.go

sync_pr_reviews.goSync PR reviews from Snowflake into tool-layer review table +128/-0

Sync PR reviews from Snowflake into tool-layer review table

• Pulls PR review rows joined to repo issues and left-joins USER for author login. Supports incremental filtering by submitted timestamp when timeAfter is provided.

backend/plugins/github_snowflake/tasks/sync_pr_reviews.go

sync_pull_requests.goSync pull requests from Snowflake into _tool_github_pull_requests +180/-0

Sync pull requests from Snowflake into _tool_github_pull_requests

• Builds PR records by joining PULL_REQUEST with ISSUE and ISSUE_MERGED to reconstruct key fields split across tables. Derives PR URLs from repo fullName and supports incremental filtering by UPDATED_AT.

backend/plugins/github_snowflake/tasks/sync_pull_requests.go

sync_repos.goSync repository metadata from Snowflake into _tool_github_repos +109/-0

Sync repository metadata from Snowflake into _tool_github_repos

• Reads repository metadata from REPOSITORY by ID and derives HTML/clone URLs from FULL_NAME. Writes the result into existing GitHub tool-layer repo table with scope config support.

backend/plugins/github_snowflake/tasks/sync_repos.go

sync_reviewers.goSync requested reviewers from Snowflake into _tool_github_reviewers +122/-0

Sync requested reviewers from Snowflake into _tool_github_reviewers

• Queries REQUESTED_REVIEWER_HISTORY for user-type requests, excludes removed entries, and selects the latest request per PR/reviewer via QUALIFY. Supports optional incremental filtering by CREATED_AT.

backend/plugins/github_snowflake/tasks/sync_reviewers.go

task_data.goAdd task options, Snowflake DB opener, and state-key compatibility constants +156/-0

Add task options, Snowflake DB opener, and state-key compatibility constants

• Defines per-task options and task data (including Snowflake *sql.DB), plus JWT/externalbrowser connection logic using gosnowflake. Introduces raw-table name constants matching github plugin strings so state keys stay compatible, and includes repoShortName helper.

backend/plugins/github_snowflake/tasks/task_data.go

Tests (10) +422 / -0
main_test.goAdd test plugin stubs for didgen resolution +49/-0

Add test plugin stubs for didgen resolution

• Registers minimal github and github_snowflake PluginMeta stubs for unit tests. Enables didgen to resolve types from plugins/github/models without loading the full github plugin.

backend/plugins/github_snowflake/tasks/main_test.go

shared_test.goAdd unit tests for shared helper functions +53/-0

Add unit tests for shared helper functions

• Covers URL derivation, null helpers, and repoShortName behavior. Ensures deterministic formatting for derived GitHub URLs.

backend/plugins/github_snowflake/tasks/shared_test.go

sync_accounts_test.goAdd unit test for accounts query builder +34/-0

Add unit test for accounts query builder

• Validates the generated SQL includes expected joins/CTEs and that repoId arguments are repeated per UNION branch. Ensures reserved USER table is correctly quoted.

backend/plugins/github_snowflake/tasks/sync_accounts_test.go

sync_pr_commits_test.goAdd unit tests for PR commits query builder +42/-0

Add unit tests for PR commits query builder

• Asserts the query inner-joins PULL_REQUEST (to drop orphans) and conditionally applies the AUTHOR_DATE filter. Verifies argument ordering and timeAfter handling.

backend/plugins/github_snowflake/tasks/sync_pr_commits_test.go

sync_pr_reviews_test.goAdd unit tests for PR reviews query builder +41/-0

Add unit tests for PR reviews query builder

• Verifies presence of joins (including quoted USER) and correct conditional SUBMITTED_AT filtering. Confirms args reflect repoId plus optional timestamp.

backend/plugins/github_snowflake/tasks/sync_pr_reviews_test.go

sync_pull_requests_test.goAdd unit tests for pull request query builder +56/-0

Add unit tests for pull request query builder

• Validates required join structure and key columns, plus conditional UPDATED_AT filtering. Ensures the query aligns with the expected Snowflake MARTS table layout.

backend/plugins/github_snowflake/tasks/sync_pull_requests_test.go

sync_repos_test.goAdd unit test for repos query builder +32/-0

Add unit test for repos query builder

• Confirms the REPOSITORY query includes expected columns and filters by ID. Verifies the argument list contains the requested githubId.

backend/plugins/github_snowflake/tasks/sync_repos_test.go

sync_reviewers_test.goAdd unit tests for reviewers query builder +42/-0

Add unit tests for reviewers query builder

• Validates filtering to user reviewers, non-removed requests, and latest-per-key selection logic. Confirms conditional CREATED_AT filter and args.

backend/plugins/github_snowflake/tasks/sync_reviewers_test.go

task_data_test.goAdd unit tests for option decoding/validation +71/-0

Add unit tests for option decoding/validation

• Tests required option validation and fallback behavior between name and fullName. Ensures connectionId/githubId constraints are enforced early.

backend/plugins/github_snowflake/tasks/task_data_test.go

table_info_test.goRegister github_snowflake tables for table info coverage +2/-0

Register github_snowflake tables for table info coverage

• Adds the github_snowflake plugin to the table info test so its GetTablesInfo output is validated alongside other plugins. Ensures migrations/models remain discoverable and test-covered.

backend/plugins/table_info_test.go

Documentation (3) +362 / -1
AGENTS.mdAdd plugin agent context, conventions, and subtask order +95/-0

Add plugin agent context, conventions, and subtask order

• Documents the github_snowflake plugin purpose, layout, subtask pipeline order, and Snowflake schema assumptions. Captures key conventions (repo scope unit, auth types, and MVP limitations).

backend/plugins/github_snowflake/AGENTS.md

github-snowflake-local-testing.mdAdd end-to-end local testing guide for github_snowflake +263/-0

Add end-to-end local testing guide for github_snowflake

• Provides step-by-step instructions for verifying Snowflake access, running MySQL, configuring .env, creating a connection, running a pipeline, and validating results. Includes troubleshooting guidance for common Snowflake/auth/devlake issues.

docs/github-snowflake-local-testing.md

upstream-diffs.mdRegister github_snowflake as an owned plugin and document convertor origins +4/-1

Register github_snowflake as an owned plugin and document convertor origins

• Adds github_snowflake to the owned plugins list and notes that convertors are adapted from github/tasks. Helps upstream sync workflows avoid treating this plugin as an upstream modification.

docs/upstream-diffs.md

Other (2) +83 / -0
init_schema.goAdd initial migration for github_snowflake connections table +56/-0

Add initial migration for github_snowflake connections table

• Introduces migration version 20260731000001 that auto-migrates the connection table snapshot schema. Enables DB bootstrap via proceed-db-migration.

backend/plugins/github_snowflake/models/migrationscripts/init_schema.go

register.goRegister github_snowflake migration scripts +27/-0

Register github_snowflake migration scripts

• Registers the plugin’s migration list (currently init schema only). Provides the MigrationScripts() entry used by the framework.

backend/plugins/github_snowflake/models/migrationscripts/register.go

@qodo-app-for-konflux-ci

qodo-app-for-konflux-ci Bot commented Jul 31, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (8) 📜 Skill insights (0)

Grey Divider


Action required

1. Raw origin not populated ✓ Resolved 🐞 Bug ≡ Correctness
Description
github_snowflake sync tasks upsert _tool_github_* records without setting RawDataOrigin
(_raw_data_table/_raw_data_params), so StatefulDataConverter propagates empty raw-origin
metadata into domain rows and full-sync deletions scoped by _raw_data_params/_raw_data_table
won’t reliably remove stale domain data (e.g., removed reviewers). This causes persistent stale
domain relationships/metrics and undermines state bootstrapping that expects raw origin metadata.
Code

backend/plugins/github_snowflake/tasks/sync_pull_requests.go[R125-128]

+			MergedById:      nullInt(mergedById),
+			MergedByName:    nullStr(mergedByName),
+			Url:             derivePullRequestURL(fullName, int(number)),
+			NoPKModel:       common.NewNoPKModel(),
Relevance

●●● Strong

Correctness issue affects full-sync deletion/lineage; team has accepted similar Snowflake-sync
correctness fixes.

PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Tool-layer structs embed RawDataOrigin, but NewNoPKModel does not populate it. The stateful
converter copies the raw-origin fields from the tool-layer row into converted domain rows, and batch
cleanup relies on those fields, so leaving them empty prevents proper scoped deletion/lineage for
github_snowflake-produced domain data.

backend/core/models/common/base.go[51-82]
backend/helpers/pluginhelper/api/data_convertor_stateful.go[165-186]
backend/helpers/pluginhelper/api/batch_save_divider.go[71-93]
backend/plugins/github_snowflake/tasks/sync_pull_requests.go[103-131]
backend/plugins/github_snowflake/tasks/convert_reviewers.go[92-97]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`github_snowflake` writes directly into `_tool_github_*` tables but does not set `_raw_data_table` / `_raw_data_params` (RawDataOrigin) on the tool-layer rows it upserts. `StatefulDataConverter` copies RawDataOrigin from the tool-layer row into the domain-layer row, and batch deletion / your explicit full-sync deletions rely on these fields; as a result, stale domain rows are not cleaned up correctly.

### Issue Context
- `common.NewNoPKModel()` sets CreatedAt/UpdatedAt but leaves RawDataOrigin empty.
- `StatefulDataConverter` copies RawDataOrigin from input row to output row.
- Batch deletion logic uses `_raw_data_table` + `_raw_data_params`.

### Fix Focus Areas
- backend/plugins/github_snowflake/tasks/sync_pull_requests.go[103-130]
- backend/plugins/github_snowflake/tasks/sync_reviewers.go[70-80]
- backend/plugins/github_snowflake/tasks/sync_pr_commits.go[71-82]
- backend/plugins/github_snowflake/tasks/sync_pr_reviews.go[77-88]
- backend/plugins/github_snowflake/tasks/sync_accounts.go[62-88]
- backend/plugins/github_snowflake/tasks/sync_repos.go[67-84]

### What to change
1. For each sync subtask, compute the raw table + raw params that the corresponding convertor uses (e.g. `"_raw_" + RAW_PULL_REQUEST_TABLE` and `utils.ToJsonString(GithubApiParams{ConnectionId: ..., Name: ...})`).
2. Set `RawDataOrigin.RawDataTable` and `RawDataOrigin.RawDataParams` on every tool-layer row before `CreateOrUpdate`.
  - Example: `pr.RawDataTable = "_raw_" + RAW_PULL_REQUEST_TABLE; pr.RawDataParams = utils.ToJsonString(GithubApiParams{...})` (field names per `common.RawDataOrigin`).
3. Add/adjust unit tests (or a small integration test) to assert that a converted domain row ends up with non-empty `_raw_data_table`/`_raw_data_params` matching the expected scope.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Reviewer removal resurrected ✓ Resolved 🐞 Bug ≡ Correctness
Description
buildReviewersQuery filters out REMOVED=TRUE rows before applying ROW_NUMBER()/QUALIFY, so
when the latest event is a removal it is excluded and an older “requested” row can be incorrectly
selected and upserted, keeping reviewers marked as requested after they were removed. Additionally,
SyncReviewers only upserts returned rows and never reconciles/deletes stale
_tool_github_reviewers rows, so incorrect reviewer relationships can persist in tool and domain
layers.
Code

backend/plugins/github_snowflake/tasks/sync_reviewers.go[R106-109]

+WHERE i.REPOSITORY_ID = ?
+  AND LOWER(h.REQUESTED_REVIEWER_TYPE) = 'user'
+  AND (h.REMOVED IS NULL OR h.REMOVED = FALSE)
+`
Relevance

●●● Strong

Bug can resurrect removed reviewers; matches prior pattern of accepting query-logic corrections in
Snowflake sync tasks.

PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The SQL currently excludes removed events in the WHERE clause and only then uses `QUALIFY
ROW_NUMBER()` to select the latest remaining row, which can incorrectly surface an older non-removed
request after a removal event. The sync code then only upserts those rows and never deletes prior
tool reviewer rows, and the reviewer tool model has no removal flag to update, so stale/incorrect
reviewer relationships persist.

backend/plugins/github_snowflake/tasks/sync_reviewers.go[91-121]
backend/plugins/github/models/reviewer.go[24-39]
backend/plugins/github_snowflake/tasks/sync_reviewers.go[58-82]
backend/plugins/github_snowflake/tasks/convert_reviewers.go[62-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The requested-reviewer sync selects reviewers using REQUESTED_REVIEWER_HISTORY, but it filters `REMOVED` rows in the WHERE clause before picking the latest row per `(PULL_REQUEST_ID, REQUESTED_ID)`. If a reviewer was requested and later removed, the removal event is excluded and an older non-removed event can be selected, causing the reviewer to remain requested incorrectly.

### Issue Context
`GithubReviewer` records have no `removed` flag column to update in-place, so the sync must either (a) correctly select only reviewers whose latest event is not removed, and (b) reconcile/delete stale tool rows when they should disappear.

### Fix Focus Areas
- backend/plugins/github_snowflake/tasks/sync_reviewers.go[91-121]

### What to change
1. Rewrite the SQL to pick the latest history row first, then filter on removal status:
  - e.g. `WITH latest AS (SELECT ..., h.REMOVED, ROW_NUMBER() OVER (PARTITION BY ... ORDER BY h.CREATED_AT DESC NULLS LAST) rn FROM ... WHERE repo_id=? AND type='user') SELECT ... FROM latest WHERE rn=1 AND (REMOVED IS NULL OR REMOVED=FALSE)`.
2. Add a reconciliation step for tool-layer `_tool_github_reviewers` in full-sync mode (or always, if acceptable): delete rows for this repo scope before inserting the newly selected rows.
  - If you implement RawDataOrigin population (see other finding), you can delete by `_raw_data_table/_raw_data_params` to avoid complex joins.
3. Add a unit test for the SQL builder asserting the `REMOVED` filter is applied after latest-row selection (or that the CTE contains `REMOVED` and the outer query filters it).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Private key double-encryption ✓ Resolved 🐞 Bug ☼ Reliability
Description
The new encryptPrivateKey migrations encrypt every non-empty private_key DB value without
checking whether it is already encrypted, which can double-encrypt keys that were already saved via
serializer:encdec. After migration, GORM decrypts only one layer, leaving ciphertext instead of a
PEM key, causing Snowflake key parsing/auth to fail for affected keypair connections.
Code

backend/plugins/jira_snowflake/models/migrationscripts/encrypt_private_key.go[R58-61]

+		if row.PrivateKey == "" {
+			continue
+		}
+		encrypted, err := plugin.Encrypt(encKey, row.PrivateKey)
Relevance

●●● Strong

Likely treated as an obvious reliability bug in newly added migration/encryption path; low-risk
conditional check fix.

PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both Snowflake migrations call plugin.Encrypt on the raw DB private_key value without checking
if it’s already ciphertext, but the encdec serializer will only decrypt once; double-encrypted
values will therefore remain ciphertext after deserialization and fail downstream PEM parsing.

backend/plugins/jira_snowflake/models/migrationscripts/encrypt_private_key.go[53-69]
backend/plugins/github_snowflake/models/migrationscripts/encrypt_private_key.go[53-69]
backend/impls/dalgorm/encdec_serializer.go[41-59]
backend/core/plugin/plugin_utils.go[39-85]
backend/plugins/github_snowflake/tasks/task_data.go[125-167]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`encryptPrivateKey.Up` encrypts any non-empty `private_key` column value, even if that value is already ciphertext produced by the `encdec` serializer. This can double-encrypt keys and break decryption/parsing.

## Issue Context
- The migration reads `private_key` via a plain struct without `serializer:encdec`, so `row.PrivateKey` is the raw DB value.
- The new connection models use `serializer:encdec`, which will decrypt exactly once on read.

## Fix Focus Areas
- backend/plugins/jira_snowflake/models/migrationscripts/encrypt_private_key.go[53-73]
- backend/plugins/github_snowflake/models/migrationscripts/encrypt_private_key.go[53-73]

## Implementation guidance
- Make the migration idempotent by detecting already-encrypted values before encrypting:
 - Option A (recommended): attempt `plugin.Decrypt(encKey, row.PrivateKey)`; if it succeeds, treat the value as already encrypted and `continue` (or re-encrypt the decrypted plaintext once).
 - Option B: only encrypt when the value looks like a PEM private key (e.g., `strings.Contains(value, "BEGIN PRIVATE KEY")`), otherwise skip.
- Add/adjust unit tests (if you have a migration test harness) to cover:
 1) plaintext PEM → encrypted once,
 2) already-encrypted ciphertext → unchanged,
 3) empty value → unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Files changed outside owned dirs 📘 Rule violation ⌂ Architecture
Description
This PR changes/adds files outside the only allowed owned-plugin directories
(backend/plugins/aireview/, backend/plugins/codecov/, backend/plugins/testregistry/). This
violates the upstream-modification restriction and may create unsupported divergences.
Code

backend/plugins/github_snowflake/impl/impl.go[R1-5]

+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
Relevance

●● Moderate

Architecture/compliance rule; no close historical precedent found on restricting plugin directories.

PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1105 restricts PR changes to three owned-plugin directories, but this PR introduces the new
backend/plugins/github_snowflake/... tree (example shown in impl/impl.go).

Rule 1105: Do not modify upstream code outside owned plugin directories
backend/plugins/github_snowflake/impl/impl.go[1-5]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR modifies/adds files outside the only permitted owned plugin directories.

## Issue Context
Compliance rule 1105 allows modifications only within `backend/plugins/aireview/`, `backend/plugins/codecov/`, or `backend/plugins/testregistry/`.

## Fix Focus Areas
- backend/plugins/github_snowflake/impl/impl.go[1-5]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Accounts always look updated 🐞 Bug ➹ Performance ⭐ New
Description
SyncAccounts now assigns a freshly initialized NoPKModel (timestamps set to "now") on every
upsert, so _tool_github_accounts.updated_at is bumped each run even when the account data is
unchanged. Since ConvertAccounts incremental mode filters by _tool_github_accounts.updated_at,
this effectively forces repeated reconversion of all accounts returned by the sync query and
increases pipeline/DB load.
Code

backend/plugins/github_snowflake/tasks/sync_accounts.go[R69-70]

+			NoPKModel:    toolLayerNoPKModel(RAW_ACCOUNT_TABLE, connectionId, fullName),
+		}
Relevance

●●● Strong

Incremental/perf bug causing unnecessary reconversion; team generally accepts practical efficiency
fixes in Snowflake sync tasks.

PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The focused lines set NoPKModel using toolLayerNoPKModel, which initializes timestamps to now.
Those values are persisted via an upsert that updates all columns on conflict, and the account
convertor’s incremental path uses updated_at as its filter—so updated_at churn forces
reconversion each run.

backend/plugins/github_snowflake/tasks/sync_accounts.go[34-86]
backend/plugins/github_snowflake/tasks/shared.go[42-51]
backend/core/models/common/base.go[70-82]
backend/impls/dalgorm/dalgorm.go[262-266]
backend/plugins/github_snowflake/tasks/convert_accounts.go[73-78]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SyncAccounts` assigns `NoPKModel: toolLayerNoPKModel(...)` during every upsert. `toolLayerNoPKModel` calls `common.NewNoPKModel()` which sets `CreatedAt/UpdatedAt` to `time.Now()`. Because DAL `CreateOrUpdate` uses an upsert that updates all columns on conflict, these timestamps can be written repeatedly, making `_tool_github_accounts.updated_at` change on every run. `ConvertAccounts` uses `_tool_github_accounts.updated_at` to decide what to process incrementally, so incremental conversion becomes effectively a full conversion each run.

## Issue Context
- This was introduced by the new `NoPKModel: toolLayerNoPKModel(...)` assignments in `sync_accounts.go`.
- `ConvertAccounts` relies on `updated_at` for incremental filtering; if `updated_at` is rewritten every run, incremental work becomes unnecessarily expensive.

## Fix Focus Areas
- backend/plugins/github_snowflake/tasks/sync_accounts.go[39-86]

### Suggested implementation directions (pick one)
1) **Use a source-derived timestamp**: If Snowflake provides a stable "user updated" timestamp (e.g., `u.UPDATED_AT`), select it in `buildAccountsQuery` and set `account.UpdatedAt` to that value (and avoid changing it when source hasn’t changed).
2) **Avoid rewriting timestamps on conflict**: Replace the blind `CreateOrUpdate` upsert with logic that only updates rows when material fields change (so `updated_at` reflects meaningful changes).
3) **If neither is feasible, disable misleading incremental filtering**: Adjust the account conversion path to not depend on `_tool_github_accounts.updated_at` for incrementality (so it doesn’t pretend to be incremental but still processes everything).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Whitespace key bypasses check 🐞 Bug ☼ Reliability
Description
In snowflakehelper.Open, the keypair path only checks privateKeyPEM == "", so a whitespace-only
private key passes the required-field guard and fails later in PEM parsing with a generic parse
error instead of the explicit bad-input message. This makes misconfiguration harder to diagnose and
can lead to confusing pipeline failures for keypair auth when secrets contain only
whitespace/newlines.
Code

backend/helpers/snowflakehelper/db.go[R55-57]

+		if privateKeyPEM == "" {
+			return nil, errors.BadInput.New("privateKey is required for keypair auth")
+		}
Relevance

●●● Strong

Small, deterministic input-hardening; team often accepts clearer validation to avoid confusing
runtime errors.

PR-#107
PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Open only rejects a literal empty string, then immediately attempts to parse the PEM. PEM decoding
fails for whitespace-only input, so this configuration mistake is not caught by the explicit
required-field validation.

backend/helpers/snowflakehelper/db.go[53-61]
backend/helpers/snowflakehelper/db.go[85-88]
backend/helpers/snowflakehelper/db_test.go[48-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`snowflakehelper.Open` treats `privateKeyPEM == ""` as missing, but does not treat whitespace-only values as missing. If `privateKeyPEM` is e.g. `"   \n"`, it bypasses the required-field validation and fails later during PEM decode/parsing, producing a less direct error for a common misconfiguration.

### Issue Context
Keypair auth is the default path (`authType=="" || "keypair"`). The helper already has an explicit required-field check; it should also reject whitespace-only input consistently.

### Fix Focus Areas
- backend/helpers/snowflakehelper/db.go[53-61]
- backend/helpers/snowflakehelper/db_test.go[48-56]

### Proposed fix
1. Change the guard to something like:
  - `if strings.TrimSpace(privateKeyPEM) == "" { ...BadInput... }`
  (Keep the original `privateKeyPEM` for parsing; only trim for the emptiness check.)
2. Add a unit test analogous to `TestOpen_EmptyPrivateKeyForKeypair` that passes whitespace (e.g. `" \n\t"`) and asserts it returns the same `privateKey is required for keypair auth` error.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. NULL reviewer ID scan 🐞 Bug ☼ Reliability
Description
buildReviewersQuery can emit rows with NULL REQUESTED_ID because it lacks an `h.REQUESTED_ID IS NOT
NULL predicate. SyncReviewers scans reviewer_id` into a non-nullable int64, so a NULL value will
make rows.Scan error and abort the syncReviewers subtask.
Code

backend/plugins/github_snowflake/tasks/sync_reviewers.go[R115-116]

+    WHERE i.REPOSITORY_ID = ?
+      AND LOWER(h.REQUESTED_REVIEWER_TYPE) = 'user'
Relevance

●●● Strong

Likely runtime rows.Scan crash; team has accepted Snowflake sync query correctness/reliability fixes
before.

PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The reviewers query selects h.REQUESTED_ID AS reviewer_id without filtering NULLs, while the sync
code scans that column into an int64 (not a nullable type) and returns an error if scanning fails.
Another query in this plugin already filters REQUESTED_ID IS NOT NULL, indicating this nullability
is expected/handled elsewhere.

backend/plugins/github_snowflake/tasks/sync_reviewers.go[60-68]
backend/plugins/github_snowflake/tasks/sync_reviewers.go[102-116]
backend/plugins/github_snowflake/tasks/sync_accounts.go[129-135]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SyncReviewers` scans `reviewer_id` into an `int64`, but the Snowflake query in `buildReviewersQuery` does not exclude rows where `REQUESTED_REVIEWER_HISTORY.REQUESTED_ID` is NULL. If such a row exists, `rows.Scan` will fail and the subtask will terminate early.

## Issue Context
A similar Snowflake query in `buildAccountsQuery` already defensively filters `h.REQUESTED_ID IS NOT NULL`, suggesting the source column can be nullable.

## Fix Focus Areas
- backend/plugins/github_snowflake/tasks/sync_reviewers.go[102-117]
- backend/plugins/github_snowflake/tasks/sync_reviewers_test.go[27-36]

## Proposed fix
1. In `buildReviewersQuery`, add `AND h.REQUESTED_ID IS NOT NULL` to the inner query WHERE clause (before `QUALIFY ROW_NUMBER()`), alongside the existing `REQUESTED_REVIEWER_TYPE` filter.
2. Update `TestBuildReviewersQuery_FiltersUsersAndLatest` to assert the query contains `h.REQUESTED_ID IS NOT NULL` to prevent regression.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (5)
8. snowflakehelper not documented divergence 📘 Rule violation § Compliance
Description
New non-owned-path helper files were added, but docs/upstream-diffs.md does not explicitly list
them under a divergence entry with the required metadata. This makes upstream rebases harder to
audit and violates the requirement to document new upstream divergences.
Code

docs/upstream-diffs.md[R9-11]

+Shared helper packages added for owned plugins (e.g. `backend/helpers/snowflakehelper/`) are also
+fork additions and are not tracked as file-level upstream diffs below.
+
Relevance

●●● Strong

Team previously accepted adding upstream-diffs divergence documentation; expects explicit tracking
in docs/upstream-diffs.md (PR #99).

PR-#99

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR adds new non-owned-path helper files under backend/helpers/snowflakehelper/, but the updated
docs/upstream-diffs.md only adds a generic note and does not explicitly list these file paths in a
divergence entry with the required tracking metadata, violating PR Compliance ID 1360.

Rule 1360: Document new upstream divergences in docs/upstream-diffs.md
docs/upstream-diffs.md[9-11]
backend/helpers/snowflakehelper/db.go[1-25]
backend/helpers/snowflakehelper/db_test.go[1-30]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New files under `backend/helpers/snowflakehelper/` were added outside owned plugin directories, but they are not documented as a tracked divergence in `docs/upstream-diffs.md` with explicit file paths and the required fields (Reason, Upstream status, Upstream PR, Owner, Rebase notes).

## Issue Context
`docs/upstream-diffs.md` currently states shared helper packages are “not tracked”, which does not satisfy the rule requiring explicit divergence entries for new non-owned-path changes.

## Fix Focus Areas
- docs/upstream-diffs.md[9-11]
- backend/helpers/snowflakehelper/db.go[1-25]
- backend/helpers/snowflakehelper/db_test.go[1-30]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. DirectRun missing connectionId ✓ Resolved 🐞 Bug ≡ Correctness
Description
The github_snowflake standalone entrypoint does not accept or pass connectionId to
runner.DirectRun, but task option validation requires a non-zero connectionId. As a result,
running github_snowflake in standalone mode will always fail in DecodeAndValidateTaskOptions.
Code

backend/plugins/github_snowflake/github_snowflake.go[R29-40]

+func main() {
+	cmd := &cobra.Command{Use: "github_snowflake"}
+	githubId := cmd.Flags().IntP("githubId", "g", 0, "GitHub repository numeric ID")
+	fullName := cmd.Flags().StringP("fullName", "n", "", "GitHub repository full name (owner/repo)")
+	timeAfter := cmd.Flags().StringP("timeAfter", "a", "", "only sync records created/updated after this time (RFC3339)")
+
+	cmd.Run = func(cmd *cobra.Command, args []string) {
+		runner.DirectRun(cmd, args, PluginEntry, map[string]interface{}{
+			"githubId": *githubId,
+			"name":     *fullName,
+			"fullName": *fullName,
+		}, *timeAfter)
Relevance

●●● Strong

Standalone mode would deterministically fail without connectionId; team typically fixes missing
required IDs.

PR-#97

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The standalone CLI passes only githubId/name/fullName into DirectRun, while
DecodeAndValidateTaskOptions explicitly errors out when ConnectionId is zero.

backend/plugins/github_snowflake/github_snowflake.go[29-41]
backend/plugins/github_snowflake/tasks/task_data.go[66-77]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The plugin’s standalone `main()` (DirectRun) cannot work because it never supplies `connectionId`, which is mandatory for github_snowflake task options.

### Issue Context
`DecodeAndValidateTaskOptions` rejects `connectionId == 0`.

### Fix Focus Areas
- backend/plugins/github_snowflake/github_snowflake.go[29-40]
- backend/plugins/github_snowflake/tasks/task_data.go[66-77]

### Suggested fix
- Add `--connectionId/-c` flag, mark it required, and pass it in the options map as `"connectionId": *connectionId`.
- Consider also marking `githubId` and `fullName` required in CLI mode for parity with other plugins’ standalone entrypoints.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. table_info_test.go divergence undocumented 📘 Rule violation § Compliance
Description
backend/plugins/table_info_test.go was modified but there is no corresponding upstream-divergence
entry documenting this non-owned-path change. This increases rebase risk because the local change is
not tracked with reason/owner/status.
Code

backend/plugins/table_info_test.go[R84-87]

	checker.FeedIn("gitee/models", gitee.Gitee{}.GetTablesInfo)
	checker.FeedIn("gitextractor/models", gitextractor.GitExtractor{}.GetTablesInfo)
	checker.FeedIn("github/models", github.Github{}.GetTablesInfo)
+	checker.FeedIn("github_snowflake/models", github_snowflake.GithubSnowflake{}.GetTablesInfo)
Relevance

●●● Strong

Repo has accepted documenting upstream divergences in docs/upstream-diffs.md for non-owned-path
changes.

PR-#99

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1360 requires any modified file outside owned plugin directories (that is not already
referenced in docs/upstream-diffs.md) to be documented there. This PR modifies
backend/plugins/table_info_test.go, but the updated docs/upstream-diffs.md section shown does
not add an entry for that file.

Rule 1360: Document new upstream divergences in docs/upstream-diffs.md
backend/plugins/table_info_test.go[83-87]
docs/upstream-diffs.md[1-14]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A file outside owned plugin directories was modified without a corresponding divergence entry in `docs/upstream-diffs.md`.

## Issue Context
Compliance requires documenting new upstream divergences for modified files outside owned plugins, including reason, upstream status, owner, upstream PR link/placeholder, and rebase notes.

## Fix Focus Areas
- backend/plugins/table_info_test.go[38-44]
- backend/plugins/table_info_test.go[83-87]
- docs/upstream-diffs.md[1-20]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Cross-plugin import plugins/github/models 📘 Rule violation ⌂ Architecture
Description
New github_snowflake code imports github.com/apache/incubator-devlake/plugins/github/models,
creating a prohibited cross-plugin dependency that breaks plugin encapsulation. This increases
coupling between plugins and makes independent deployment/versioning harder.
Code

backend/plugins/github_snowflake/impl/impl.go[R20-32]

+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	coremodels "github.com/apache/incubator-devlake/core/models/common"
+	"github.com/apache/incubator-devlake/core/plugin"
+	helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+	githubmodels "github.com/apache/incubator-devlake/plugins/github/models"
+	"github.com/apache/incubator-devlake/plugins/github_snowflake/api"
+	"github.com/apache/incubator-devlake/plugins/github_snowflake/models"
+	"github.com/apache/incubator-devlake/plugins/github_snowflake/models/migrationscripts"
+	"github.com/apache/incubator-devlake/plugins/github_snowflake/tasks"
+)
Relevance

●● Moderate

Cross-plugin import policy is subjective here; no closely-matching accepted/rejected precedent
located.

PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance checklist (Rule 1110) forbids imports from backend/plugins/<other-plugin>/... to
prevent cross-plugin dependencies. In the new github_snowflake implementation and task code, files
import plugins/github/models (in at least one case aliased as githubmodels), which directly
demonstrates a dependency on a different plugin package and therefore violates the no cross-plugin
import rule.

Rule 988: Prohibit cross-imports between plugin packages
Rule 1110: No cross-plugin imports between plugin packages
backend/plugins/github_snowflake/impl/impl.go[20-32]
backend/plugins/github_snowflake/tasks/convert_accounts.go[20-29]
backend/plugins/github_snowflake/tasks/convert_repo.go[20-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`github_snowflake` imports types from another plugin package (`plugins/github/models`), which violates the cross-plugin import restriction (Rule 1110) and breaks plugin encapsulation.

## Issue Context
Compliance requires plugin packages under `plugins/<name>/...` to be self-contained and not import code from other plugins (except explicitly shared/common namespaces). The new `github_snowflake` code depends directly on `plugins/github/models` in both implementation and task files (including task code that imports `plugins/github/models` as `githubmodels`).

## Fix Focus Areas
- backend/plugins/github_snowflake/impl/impl.go[20-32]
- backend/plugins/github_snowflake/tasks/convert_accounts.go[20-29]
- backend/plugins/github_snowflake/tasks/convert_repo.go[20-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Missing swagger docs for endpoints 📘 Rule violation ⚙ Maintainability
Description
This PR introduces new github_snowflake API resources (connections CRUD) but does not add/update
any Swagger/OpenAPI documentation for them, so the public API surface is undocumented. This violates
the requirement to regenerate/update Swagger docs whenever API endpoints or signatures change.
Code

backend/plugins/github_snowflake/impl/impl.go[R212-223]

+func (p GithubSnowflake) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
+	return map[string]map[string]plugin.ApiResourceHandler{
+		"connections": {
+			"POST": api.PostConnections,
+			"GET":  api.GetConnections,
+		},
+		"connections/:connectionId": {
+			"GET":    api.GetConnection,
+			"PATCH":  api.PatchConnection,
+			"DELETE": api.DeleteConnection,
+		},
+	}
Relevance

●● Moderate

Swagger regeneration/documentation enforcement unclear; no close precedent found.

PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule requires Swagger docs to be regenerated/updated whenever API endpoints change.
The new ApiResources() map exposes new HTTP handlers for connections and
connections/:connectionId, but no Swagger annotations/docs were added alongside these new
endpoints.

Rule 987: Regenerate Swagger docs when API endpoints or signatures change
backend/plugins/github_snowflake/impl/impl.go[212-223]
backend/plugins/github_snowflake/api/connection_api.go[35-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New `github_snowflake` API endpoints were added, but Swagger/OpenAPI documentation was not updated/generated for them.

## Issue Context
Other plugins in this repo expose Swagger documentation via generated/annotated `api/swagger.go` (Swag annotations). This plugin adds connection CRUD endpoints via `ApiResources()` but has no corresponding swagger annotations/files.

## Fix Focus Areas
- backend/plugins/github_snowflake/impl/impl.go[212-223]
- backend/plugins/github_snowflake/api/connection_api.go[30-75]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

13. Incorrect quoting terminology 🐞 Bug ⚙ Maintainability
Description
The guide calls the username in SHOW GRANTS TO USER a “quoted string”, but the example uses double
quotes which are identifier quoting in Snowflake/SQL. This mismatch can confuse readers about
whether to use double quotes vs single quotes when substituting their username.
Code

docs/github-snowflake-local-testing.md[R27-28]

+-- Replace with the username returned above (quoted string, not CURRENT_USER as an identifier):
+-- SHOW GRANTS TO USER "your.username";
Relevance

●●● Strong

Team often accepts doc accuracy/clarity fixes (multiple doc corrections accepted in PR #120).

PR-#120

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The text explicitly says “quoted string” right before an example that uses double quotes, which are
identifier quoting in SQL/Snowflake.

docs/github-snowflake-local-testing.md[21-28]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`docs/github-snowflake-local-testing.md` describes the `SHOW GRANTS TO USER` argument as a “quoted string”, but the example uses double quotes which denote a delimited identifier in Snowflake/SQL, not a string literal. This is a terminology mismatch that can cause confusion when readers substitute their own username.

### Issue Context
This is in Step 1a (“Check role access in the Snowflake console”).

### Fix Focus Areas
- docs/github-snowflake-local-testing.md[21-28]

### Suggested change
Update the comment to say “double-quoted identifier” (or “delimited identifier”) and optionally add a brief warning like “don’t use single quotes here” to avoid confusion.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Broken troubleshooting table 🐞 Bug ⚙ Maintainability
Description
docs/github-snowflake-local-testing.md includes a blank line between two rows inside the
Troubleshooting Markdown table, which can terminate the table and cause subsequent rows to render
outside the table.
Code

docs/github-snowflake-local-testing.md[R270-271]

+| `failed to decode PEM block from private key` | Connection is on `keypair` with empty/invalid key | Recreate or PATCH connection with `"authType": "externalbrowser"` (local) or a valid PKCS#8 `privateKey` |
+
Relevance

●●● Strong

Docs correctness tweaks often accepted (many doc fixes accepted in PR #120; local-testing doc
updates merged in PR #130).

PR-#120
PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The troubleshooting section is formatted as a pipe-delimited Markdown table, but there is an empty
line after the failed to decode PEM block... row, before the next tool_prs populated... row,
which can break the table formatting.

docs/github-snowflake-local-testing.md[257-273]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The Troubleshooting section is a Markdown table, but there is an empty line between two `| ... |` rows. Some Markdown renderers treat a blank line as the end of the table, so later troubleshooting items may render incorrectly.

### Issue Context
This affects only documentation rendering/readability.

### Fix Focus Areas
- docs/github-snowflake-local-testing.md[269-272]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. Key errors misclassified ✓ Resolved 🐞 Bug ☼ Reliability
Description
snowflakehelper.Open wraps invalid RSA private key parsing errors with errors.Default, which is
treated as an internal error type when wrapping non-DevLake errors. This misclassifies common
configuration mistakes (empty/invalid PEM, PKCS#1 vs PKCS#8) and makes failures harder to diagnose
in github_snowflake/jira_snowflake pipelines.
Code

backend/helpers/snowflakehelper/db.go[R55-58]

+		privKey, err := ParseRSAPrivateKey(privateKeyPEM)
+		if err != nil {
+			return nil, errors.Default.Wrap(err, "failed to parse Snowflake private key")
+		}
Relevance

● Weak

Repo previously merged same errors.Default.Wrap on Snowflake private-key parse;
validation/classification changes were rejected (PR #130).

PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Open wraps private-key parse failures with errors.Default; DevLake documents that errors.Default
maps to Internal when wrapping non-Error types, so these configuration failures get the wrong
classification. Both github_snowflake and jira_snowflake now call this helper from PrepareTaskData,
so the misclassification affects both plugins.

backend/helpers/snowflakehelper/db.go[53-61]
backend/core/errors/types.go[25-33]
backend/plugins/github_snowflake/impl/impl.go[163-175]
backend/plugins/jira_snowflake/impl/impl.go[163-176]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`snowflakehelper.Open` wraps `ParseRSAPrivateKey` failures using `errors.Default`, which becomes an internal error type when wrapping a non-DevLake error. Invalid/empty/malformed keys are configuration/input problems and should be surfaced as `errors.BadInput`.

### Issue Context
This helper is now the shared Snowflake connection path for both `github_snowflake` and `jira_snowflake`.

### Fix Focus Areas
- backend/helpers/snowflakehelper/db.go[53-61]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (7)
16. Testify used for trivial asserts 📘 Rule violation ⚙ Maintainability
Description
backend/helpers/snowflakehelper/db_test.go introduces github.com/stretchr/testify/assert and
require for simple nil/error/equality checks that can be expressed with testing.T and basic if
statements. This violates the guidance to prefer Go’s standard library testing for trivial
assertions.
Code

backend/helpers/snowflakehelper/db_test.go[R27-28]

+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
Relevance

● Weak

Team regularly merges tests using testify assert/require, including similar trivial checks in
Snowflake plugin tests (PR #130).

PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1051 flags adding testify when assertions are simple and could be written with
testing.T directly. The new test file imports assert/require and uses them for basic
error/non-nil/equality checks.

Rule 1051: Prefer Go standard library testing over external assertion libraries
backend/helpers/snowflakehelper/db_test.go[20-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new Snowflake helper unit tests import and use `testify` for simple assertions, which the checklist flags as unnecessary when standard `testing` can express the same checks clearly.

## Issue Context
This is a newly added test file, so adopting stdlib-only patterns here avoids expanding dependency usage for minimal benefit.

## Fix Focus Areas
- backend/helpers/snowflakehelper/db_test.go[20-79]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


17. snowflakehelper.Open missing context 📘 Rule violation ≡ Correctness
Description
snowflakehelper.Open creates a Snowflake *sql.DB handle without accepting a caller-provided
context.Context or performing a PingContext, which prevents cancellation/timeout control for a
potentially blocking I/O operation. This violates the requirement to use context for I/O-bound
operations and can cause hangs or slow failures during task setup.
Code

backend/helpers/snowflakehelper/db.go[39]

+func Open(account, user, authType, privateKeyPEM, database, schema, warehouse, role string) (*sql.DB, errors.Error) {
Relevance

● Weak

Similar Snowflake Open helper without context was merged; no historical enforcement of
ctx/PingContext (PR #130).

PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1026 requires new/modified I/O-related functions to accept and propagate
context.Context. The new Open helper opens a Snowflake DB handle without any context parameter
and returns it without a context-aware connectivity check such as PingContext.

Rule 1026: Use context.Context for I/O and long-running operations
backend/helpers/snowflakehelper/db.go[39-77]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`snowflakehelper.Open` performs Snowflake connection setup but does not accept `context.Context` and does not validate connectivity with `PingContext`, preventing caller-controlled cancellation/timeouts.

## Issue Context
This helper is called from plugin task preparation paths, where a task context is available and should be propagated to any I/O or potentially blocking operations.

## Fix Focus Areas
- backend/helpers/snowflakehelper/db.go[33-77]
- backend/plugins/github_snowflake/impl/impl.go[163-172]
- backend/plugins/jira_snowflake/impl/impl.go[164-172]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


18. SnowflakeJiraConnection returns privateKey ✓ Resolved 📘 Rule violation ⛨ Security
Description
The jira_snowflake connection API returns SnowflakeJiraConnection objects without redacting
PrivateKey, which can leak JWT private keys in POST/GET/PATCH/list responses. Secrets must be
omitted/masked before serialization.
Code

backend/plugins/jira_snowflake/models/connection.go[38]

+	PrivateKey string `json:"privateKey" gorm:"column:private_key;type:text;serializer:encdec" mapstructure:"privateKey"`
Relevance

● Weak

Very close precedent rejected masking/sanitizing Snowflake privateKey in jira_snowflake connection
API responses.

PR-#130

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1112 requires secret fields to be sanitized in API responses.
SnowflakeJiraConnection.PrivateKey is JSON-serializable and the API handlers return the connection
object(s) directly as the response body.

Rule 1112: Secrets must be sanitized in API responses
[backend/plugins/jira_snowflake/models/connection.go[36-39]](https://github.com/konflux-ci/devlake/blob/0f

[Comment truncated to fit github's 65,536-char limit.]

Comment on lines +1 to +5
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Files changed outside owned dirs 📘 Rule violation ⌂ Architecture

This PR changes/adds files outside the only allowed owned-plugin directories
(backend/plugins/aireview/, backend/plugins/codecov/, backend/plugins/testregistry/). This
violates the upstream-modification restriction and may create unsupported divergences.
Agent Prompt
## Issue description
The PR modifies/adds files outside the only permitted owned plugin directories.

## Issue Context
Compliance rule 1105 allows modifications only within `backend/plugins/aireview/`, `backend/plugins/codecov/`, or `backend/plugins/testregistry/`.

## Fix Focus Areas
- backend/plugins/github_snowflake/impl/impl.go[1-5]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review

Findings

Medium

  • [error-handling-gap] backend/helpers/snowflakehelper/db.go:86Open returns *sql.DB without calling db.Ping() to verify the connection is reachable. sql.Open only validates DSN syntax; bad credentials or an unreachable Snowflake host would surface as confusing errors on the first sync query rather than at connection setup. This matches the existing jira_snowflake pattern and is standard Go database/sql behavior, but adding db.PingContext(ctx) would improve error locality.

Low

  • [operational-constraint] backend/plugins/github_snowflake/AGENTS.md:68 — Documented constraint that a repo must not be configured in both a GitHub API connection and a github_snowflake connection is not enforced at runtime. Domain ID duplication could result if both are configured for the same repo.
  • [missing-DependencyTables] backend/plugins/github_snowflake/tasks/sync_pull_requests.go:17 — Sync subtask metas (SyncPullRequestsMeta, SyncPrCommitsMeta, etc.) do not declare DependencyTables or ProductTables. The framework cannot track table ownership for sync tasks. This matches the jira_snowflake pattern.
  • [test-coverage-gap] backend/plugins/github_snowflake/tasks/convert_accounts.go:78convertOrphanedRepoAccounts uses a LEFT JOIN with IS NULL filter but has no unit tests covering the orphan-handling logic.
  • [validation-message-format] backend/plugins/github_snowflake/tasks/task_data.go:89 — Validation error messages inconsistently include the invalid value (e.g., connectionId includes the value via fmt.Sprintf, name does not).
Previous run

Review

Findings

Medium

  • [error-handling-gap] backend/plugins/github_snowflake/impl/impl.go:133 — In PrepareTaskData, snowflakehelper.Open is called before the function returns the task data. While no error paths currently exist after Open succeeds (the return is immediate), this pattern is fragile: any future code added between Open and the return could leak the *sql.DB connection, since Close() only runs after all subtasks complete, not on PrepareTaskData failures.
    Remediation: Move snowflakehelper.Open to the end of PrepareTaskData (after all validation/DB lookups succeed), or add a deferred cleanup path.

Low

  • [logic-error] backend/plugins/github_snowflake/tasks/sync_pull_requests.go:87GithubUpdatedAt falls back to createdAt when Snowflake's UPDATED_AT is null. The convertor's incremental filter uses github_updated_at >= ?, which could skip PRs with null UPDATED_AT whose creation date predates the watermark. In practice this is a narrow edge case affecting very old/inactive PRs.

  • [edge-case] backend/plugins/github_snowflake/tasks/sync_reviewers.go:60 — On incremental sync, previously synced reviewer records are never deleted when a reviewer is un-requested. The Snowflake query correctly filters out removed reviewers, but stale rows from prior syncs remain in _tool_github_reviewers.

  • [encryption-transition] backend/plugins/jira_snowflake/models/connection.go:38 — The encryption mechanism changes from encrypt:"yes" to serializer:encdec. The accompanying migration handles the transition, but operators with existing connections should verify the migration ran successfully.

  • [scope-creep] backend/plugins/jira_snowflake/models/connection.go — The PR bundles a breaking API change to jira_snowflake (switching PrivateKey from json:"privateKey" to json:"-" and changing the encryption mechanism) with the github_snowflake feature. While this is deliberate security hardening (GET responses should not expose decrypted private keys), consider noting the breaking change explicitly for existing consumers.

  • [naming-inconsistency] backend/plugins/github_snowflake/tasks/shared.go:51nullStr/nullInt helpers use different names from the identical stringVal function in the sibling jira_snowflake plugin. Consider aligning names or extracting into the shared snowflakehelper package.

  • [interface-assertion] backend/plugins/github_snowflake/impl/impl.go:43github_snowflake includes plugin.CloseablePluginTask in its compile-time interface assertion, while jira_snowflake omits it despite also implementing Close. Consider adding it to jira_snowflake for consistency.

  • [edge-case] backend/plugins/github_snowflake/tasks/sync_accounts.go:57 — The QUALIFY ROW_NUMBER() ordering by ue.EMAIL NULLS LAST lacks a tiebreaker, so users with multiple emails may get a different email on different sync runs.

  • [api-contract] backend/plugins/github_snowflake/api/connection_api.go:37PostConnections returns HTTP 200 instead of 201 on creation. Consistent with jira_snowflake but diverges from REST conventions.

  • [missing-validation] backend/helpers/snowflakehelper/db.go:45Open does not validate that account, user, database, or schema are non-empty. The connection model has validate:"required" tags at the API layer, but direct calls (CLI, tests) bypass that validation.

  • [stale-doc] docs/research/snowflake-jira-plugin.md:188 — The connection model code sample still shows encrypt:"yes" and json:"privateKey", which no longer match the current implementation.

Previous run (2)

Review

Findings

High

  • [merge conflict markers] docs/upstream-diffs.md — The file contains unresolved git merge conflict markers (<<<<<<< HEAD, =======, >>>>>>> main). This will ship a broken markdown file and indicates the PR branch was not properly rebased or the conflict was not resolved before committing.
    Remediation: Resolve the merge conflict by choosing the appropriate content. The intended content appears to be the union of both additions: mention both backend/helpers/snowflakehelper/ and backend/pkg/ packages.

Medium

  • [query correctness / ambiguity] backend/plugins/github_snowflake/tasks/convert_accounts.go:88 — In the ConvertAccounts Input function, the dal.Where clause references repo_github_id without table qualification. The FROM is _tool_github_accounts (which does NOT have a repo_github_id column), and the LEFT JOIN is on _tool_github_repo_accounts (which DOES have it). The LEFT JOIN means accounts without a matching repo_account row would also be returned (with NULL repo_github_id), which would not match the WHERE filter — effectively making this an INNER JOIN by accident. The unqualified column reference is fragile.
    Remediation: Qualify the column as gra.repo_github_id to make the intent explicit, and consider whether an INNER JOIN is more appropriate here.

Low

  • [error handling gap] backend/helpers/snowflakehelper/db.go:82sql.Open does not actually establish a connection — it only validates arguments. A misconfigured DSN (wrong account, unreachable host) will not be caught until the first query. The Open function returns immediately without verifying connectivity, so callers in PrepareTaskData will not discover connection problems until the first sync subtask runs.
    Remediation: Consider adding db.PingContext(ctx) after sql.Open to verify the connection is reachable before returning.

  • [potential data loss on incremental sync] backend/plugins/github_snowflake/tasks/sync_reviewers.go — The buildReviewersQuery applies a timeAfter filter on h.CREATED_AT inside the subquery that feeds into QUALIFY ROW_NUMBER(). This could resurface removed reviewers in narrow edge cases where the full removal history straddles the timeAfter boundary.

  • [scope-creep-risk] backend/plugins/github_snowflake/AGENTS.md — The AGENTS.md warns about not configuring the same repo in both a GitHub API connection and a github_snowflake connection, but this constraint is not enforced at runtime. Misconfiguration would cause domain ID duplication and data integrity issues.
    Remediation: Consider adding a runtime check in PrepareTaskData that detects when a repo (by githubId) is configured in both connection types and returns a BadInput error.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (3)

Review

Findings

Medium

  • [behavioral change in refactored code] backend/helpers/snowflakehelper/db.go:59 — The old jira_snowflake OpenSnowflakeDB treated ALL authType values that were not "externalbrowser" as keypair auth (silent fallthrough). The new shared Open function rejects unknown authType values with an error. If any existing jira_snowflake connection row in production has a non-canonical auth_type value, that connection will stop working after this upgrade.
    Remediation: Verify no production _tool_jira_snowflake_connections rows have auth_type values other than "", "keypair", or "externalbrowser". Alternatively, treat unknown authType as "keypair" with a warning log.

  • [data exposure] backend/plugins/jira_snowflake/models/connection.go:38 — The PR changes the PrivateKey field from json:"privateKey" to json:"-" (suppressing API exposure — a positive security improvement) and changes the encryption mechanism from encrypt:"yes" to serializer:encdec. The encryptPrivateKey migration correctly handles the transition with a double-encryption guard, but this mechanism change deserves validation against a database with pre-existing jira_snowflake connections to confirm private keys are correctly encrypted.
    Remediation: Test the migration against a database with pre-existing jira_snowflake connections to verify private keys are correctly encrypted.

Low

  • [error handling gap] backend/helpers/snowflakehelper/db.go:86sql.Open returns a lazy connection pool without dialing the server. Credential or connectivity errors surface only during the first query, producing confusing errors far from connection setup. This is pre-existing behavior from jira_snowflake preserved in the shared helper. Consider adding db.PingContext() after sql.Open for eager validation.

  • [test adequacy] backend/plugins/github_snowflake/tasks — Unit tests cover query builder functions but not the SELECT column-to-rows.Scan variable order mapping. Column order mismatches would silently produce incorrect data. This is inherent to the hand-written SQL+Scan pattern and best addressed by integration tests.

  • [edge case] backend/plugins/github_snowflake/tasks/sync_accounts.go:107buildAccountsQuery deduplicates users by alphabetically first non-null email via QUALIFY ROW_NUMBER(). The chosen email is deterministic (alphabetical) but arbitrary from a business perspective.

  • [secrets handling] backend/helpers/snowflakehelper/db.go:45 — The decrypted private key PEM is passed as a Go string (immutable, cannot be zeroed in memory). This is standard Go behavior consistent with other DevLake plugins.

  • [scope-coherence] backend/plugins/jira_snowflake/models/migrationscripts/encrypt_private_key.go — The PR adds an encryptPrivateKey migration to jira_snowflake and changes its connection model encryption tags (encrypt:"yes"serializer:encdec, json:"privateKey"json:"-"). These consequential security-relevant changes to an existing plugin are easy to overlook in a PR titled "add github_snowflake plugin." Consider adding a bullet to the PR summary calling out the jira_snowflake encryption migration.

Previous run (4)

Review

Findings

Medium

  • [data-integrity] backend/plugins/github_snowflake/tasks/sync_accounts.go:46SyncAccounts creates GithubAccount rows with NoPKModel: common.NewNoPKModel(), leaving _raw_data_table and _raw_data_params empty. Every other sync task in this plugin (sync_pull_requests, sync_pr_commits, sync_pr_reviews, sync_reviewers) uses toolLayerNoPKModel(...) to populate these fields. The same inconsistency applies to GithubRepoAccount rows on line 61. The plugin's own AGENTS.md (§ Key conventions) documents that sync tasks should populate RawDataOrigin so convertors can delete domain records by _raw_data_params on full sync. Without it, stale accounts from deleted Snowflake users persist indefinitely and cannot be scoped for cleanup.
    Remediation: Replace common.NewNoPKModel() with toolLayerNoPKModel(RAW_ACCOUNT_TABLE, connectionId, fullName) for both GithubAccount and GithubRepoAccount. Add fullName := data.Options.Name at the top of the function (matching the pattern in other sync tasks).

Low

  • [error-handling] backend/helpers/snowflakehelper/db.go:82sql.Open only validates the driver name and DSN syntax; it does not establish a TCP connection to Snowflake. Misconfigured accounts, wrong credentials, or unreachable Snowflake instances will only surface when the first sync subtask runs its query. For externalbrowser auth, the SSO browser pop-up appears mid-subtask instead of at connection time. This matches the pre-existing jira_snowflake behavior but the new shared helper is an opportunity to improve.
    Remediation: After sql.Open, add db.PingContext(context.Background()) and close/return error on failure for fail-fast connectivity validation.
Previous run (5)

Review

Findings

Medium

  • [error-handling-gap] backend/helpers/snowflakehelper/db.go:60 — The Open function treats an empty authType the same as "keypair", calling ParseRSAPrivateKey(privateKeyPEM) even when privateKeyPEM is empty. This produces a generic "failed to decode PEM block" error instead of a clear validation message. The SnowflakeGithubConnection model also lacks a validate:"required" tag on PrivateKey.
    Remediation: Add an early check in the keypair case: if privateKeyPEM == "" { return nil, errors.BadInput.New("privateKey is required for keypair auth") }

  • [logic-error] backend/plugins/github_snowflake/tasks/convert_prs.go:107 — The full-sync deletion db.Delete(&code.PullRequest{}, dal.Where("_raw_data_params = ?", ...)) will never match any domain records. Sync tasks write tool-layer records with NoPKModel: common.NewNoPKModel() which leaves _raw_data_params empty. The StatefulDataConverter copies this empty value to domain records, so the delete targeting JSON-encoded params matches zero rows. Stale domain records accumulate across re-runs. The same issue affects convert_pr_commits.go, convert_pr_reviews.go, and convert_reviewers.go. Note: this is the same pattern used by jira_snowflake — the fix should ideally apply to both plugins.

Low

  • [data-exposure] backend/plugins/github_snowflake/tasks/shared.go:26 — URL construction functions (deriveRepoHTMLUrl, deriveRepoCloneUrl, derivePullRequestURL) interpolate fullName without character-class validation. While validateOwnerRepo ensures one slash with non-empty parts, characters like ?, #, or newlines could produce malformed URLs in tool-layer tables. Risk is limited since fullName comes from admin-specified pipeline options.

  • [data-exposure] backend/plugins/github_snowflake/models/connection.go:41 — The PATCH endpoint may overwrite the stored encrypted private key with an empty string if the request body omits privateKey. Since PrivateKey has json:"-", a GET-modify-PATCH workflow would naturally omit it. Verify that ConnectionApiHelper.Patch performs a merge rather than full replacement.

  • [edge-case] backend/plugins/github_snowflake/tasks/sync_accounts.go:102GithubAccount.Id is int populated from int64. On 32-bit systems, large GitHub user IDs could overflow. Practically a non-issue for 64-bit deployments.

  • [logic-error] backend/plugins/github_snowflake/tasks/sync_accounts.go:81SyncAccounts does not support the timeAfter sync policy filter, unlike other sync tasks (sync_pull_requests, sync_pr_commits, sync_pr_reviews, sync_reviewers). Accounts always perform a full scan.

  • [missing-validation] backend/plugins/github_snowflake/models/connection.go:32AuthType is not validated at connection creation. Invalid values (e.g., typo "keypar") are accepted and only fail at pipeline runtime in snowflakehelper.Open. Same gap exists in jira_snowflake.

  • [encryption-safety] backend/plugins/github_snowflake/models/migrationscripts/encrypt_private_key.go:59 — The migration detects already-encrypted values by attempting plugin.Decrypt and skipping on success. Standard DevLake pattern; theoretical false-positive risk is negligible. Migration correctly rejects empty ENCRYPTION_SECRET.


Labels: Large new plugin PR (44 files, 3400+ lines) requiring thorough human review across auth handling, Snowflake integration, and domain model conversion.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (6)

Review

Findings

Medium

  • [double-encryption] backend/plugins/jira_snowflake/models/migrationscripts/encrypt_private_key.go:53 — The jira_snowflake encryptPrivateKey migration reads all rows and encrypts non-empty private_key values without an idempotency check. While migration history suggests values are stored as plaintext (migration structs used bare gorm:"column:private_key" without encryption tags), the migration does not guard against already-encrypted values. If a value were already encrypted (e.g., via manual intervention or a different code path), re-encryption would corrupt the data, making the private key permanently unrecoverable.
    Remediation: Add an idempotency check — attempt plugin.Decrypt(encKey, row.PrivateKey) first; if it succeeds, use the decrypted plaintext as input to plugin.Encrypt; if it fails, encrypt directly.

  • [unqualified-column] backend/plugins/github_snowflake/tasks/convert_accounts.go:64 — The ConvertAccounts query uses repo_github_id in the WHERE clause without table qualification. This column exists on _tool_github_repo_accounts (joined as gra), not on _tool_github_accounts. MySQL resolves this correctly since only the joined table has the column, but it is fragile and would break if GithubAccount ever gains a similarly-named column.
    Remediation: Qualify the column: gra.repo_github_id = ? AND _tool_github_accounts.connection_id = ?.

  • [null-updated-at] backend/plugins/github_snowflake/tasks/sync_pull_requests.go — The time filter AND pr.UPDATED_AT > ? in buildPullRequestsQuery excludes PRs with NULL UPDATED_AT from incremental syncs. In SQL, NULL > ? evaluates to NULL (falsy), so PRs that have never been updated after creation will be silently missed by time-filtered syncs.
    Remediation: Use AND COALESCE(pr.UPDATED_AT, pr.CREATED_AT, i.CREATED_AT) > ?.

Low

  • [double-encryption-fresh-deploy] backend/plugins/github_snowflake/models/migrationscripts/encrypt_private_key.go:53 — The encryptPrivateKey migration is introduced alongside initSchema (which already uses serializer:encdec). DevLake runs all migrations sequentially at startup before the API is available, so no connections can be created between the two — but adding an idempotency guard would be defensive.

  • [naming-inconsistency] backend/plugins/github_snowflake/tasks/shared.go:51 — The helpers nullStr/nullInt use different names than jira_snowflake's stringVal for the same pattern. While they are private and have slightly different signatures, consistent naming aids cross-plugin readability.

  • [interface-assertion] backend/plugins/github_snowflake/impl/impl.go:35 — github_snowflake asserts plugin.CloseablePluginTask in its compile-time interface check, but jira_snowflake omits it despite also implementing Close(). Adding it to jira_snowflake would align the sibling plugins.

  • [scope-creep] backend/plugins/jira_snowflake/models/migrationscripts/encrypt_private_key.go — The jira_snowflake encryption mechanism change (connection model + migration) is bundled with the github_snowflake feature PR. This is coherent with the shared helper extraction but should be noted in the PR description.

  • [edge-case] backend/plugins/github_snowflake/tasks/convert_prs.go:74 — When HeadRepoId is NULL (0 via nullInt), repoIdGen.Generate(connectionId, 0) produces a phantom domain ID. Consistent with the upstream github plugin behavior.

  • [dead-code] backend/plugins/github_snowflake/tasks/convert_prs.go:79 — The "MERGED" and "OPEN" uppercase state checks are unreachable: Snowflake ISSUE.STATE uses lowercase GitHub API values ("open", "closed").

  • [missing-validation] backend/helpers/snowflakehelper/db.go:56 — When authType is "keypair" with an empty privateKeyPEM, the error is "failed to decode PEM block" rather than a clear "private key required for keypair auth" message.

  • [scope-creep] backend/plugins/jira_snowflake/models/connection.go — The PrivateKey JSON tag changed from json:"privateKey" to json:"-", suppressing the key from API responses. This is a security improvement but a behavior change for jira_snowflake not mentioned in the PR description.

  • [scope-creep] backend/plugins/jira_snowflake/jira_snowflake.go — CLI flags added (connectionId) and existing flags marked as required. Beneficial but beyond the PR's stated scope.

  • [filename-mismatch] backend/plugins/github_snowflake/tasks/convert_reviewers.go — File named convert_reviewers.go but exports ConvertReviewsMeta/ConvertReviews, breaking the filename-to-symbol alignment pattern used in jira_snowflake.

  • [missing-progress] backend/plugins/github_snowflake/tasks/sync_pull_requests.go — No SetProgress() calls in any sync task, unlike jira_snowflake which reports progress every 500 rows. For large repos, there is no UI feedback during sync.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (7)

Review

Findings

Medium

  • [logic-error] backend/plugins/github_snowflake/tasks/sync_reviewers.go:96 — The WHERE clause filters out rows where REMOVED = TRUE before the QUALIFY ROW_NUMBER() window function runs. If a reviewer was requested (REMOVED=FALSE) then later removed (REMOVED=TRUE), the removal row is excluded by WHERE, and QUALIFY picks the older "requested" row as the latest. The reviewer erroneously appears as still requested in _tool_github_reviewers, propagating incorrect data to domain-layer PullRequestReviewer records.
    Remediation: Move the REMOVED filter after QUALIFY — wrap the current query in a subquery/CTE that first finds the latest row per (PULL_REQUEST_ID, REQUESTED_ID), then apply WHERE (REMOVED IS NULL OR REMOVED = FALSE) on the outer query.

  • [api-contract-violation] backend/plugins/github_snowflake/models/connection.go:44 — The PrivateKey field uses json:"-" which prevents json.Unmarshal from populating it. Input binding works only because DevLake uses mapstructure.Decode. If any future code path uses json.Unmarshal directly on the request body to populate this struct, the private key would be silently dropped. This is intentional (prevents key leakage in GET responses) and follows the same pattern applied to jira_snowflake in this PR.

  • [error-handling-gap] backend/plugins/github_snowflake/tasks/sync_pull_requests.go:135 — Snowflake IDs are cast from int64 to int (e.g., GithubId: int(githubId)). The upstream GithubPullRequest.GithubId field is declared as int. On 32-bit platforms, GitHub IDs exceeding int32 range would overflow. This is a known limitation inherited from the upstream model definition; the same pattern applies to all sync tasks.

Low

  • [edge-case] backend/plugins/github_snowflake/tasks/sync_pull_requests.go:126buildPullRequestsQuery uses pr.UPDATED_AT > ? as the time filter. For PRs with NULL UPDATED_AT (created before timeAfter but never updated), the comparison returns NULL (falsy), silently excluding those PRs from incremental sync.
    Remediation: Use COALESCE(pr.UPDATED_AT, COALESCE(pr.CREATED_AT, i.CREATED_AT)) > ? to handle NULLs.

  • [data-exposure] backend/plugins/github_snowflake/models/migrationscripts/encrypt_private_key.go:52 — The encrypt migration does not guard against double-encryption. DevLake's migration versioning prevents re-runs under normal operations, but a manual version reset or rollback/replay could corrupt encrypted values. Same issue in backend/plugins/jira_snowflake/models/migrationscripts/encrypt_private_key.go:52.
    Remediation: Add an idempotency check — attempt plugin.Decrypt first; only encrypt if decryption fails.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (8)

Review

Findings

Medium

  • [error-handling-gap] backend/plugins/github_snowflake/models/migrationscripts/encrypt_private_key.go — The encryptPrivateKey migration iterates cursor.Next() but never checks cursor.Err() after the loop exits. If the cursor encounters a database error during iteration, cursor.Next() returns false and the loop exits silently — the migration reports success despite the error, potentially leaving some private keys unencrypted. The identical issue exists in backend/plugins/jira_snowflake/models/migrationscripts/encrypt_private_key.go.
    Remediation: Add if err := cursor.Err(); err != nil { return errors.Default.Wrap(err, "iterating connections cursor") } after the for loop, before the final return nil, in both files.

  • [data-exposure] backend/plugins/github_snowflake/models/connection.go:37 — The PrivateKey field uses json:"privateKey" without suppression. GET /connections endpoints return the decrypted RSA private key in API responses. While serializer:encdec correctly encrypts at rest, GORM transparently decrypts on read, so JSON serialization exposes the plaintext key. Other DevLake plugins follow this same pattern for tokens, but an RSA private key is higher-value than a rotatable API token. Same issue applies to jira_snowflake.
    Remediation: Add json:"-" to suppress from API responses, or implement a custom MarshalJSON that redacts the field.

Low

  • [double-encryption-risk] backend/plugins/jira_snowflake/models/migrationscripts/encrypt_private_key.go:38 — The encryptPrivateKey migration encrypts every non-empty private_key value unconditionally without checking if the value is already encrypted. If migration history is lost, re-running would double-encrypt. For github_snowflake on fresh installs, the table starts empty so this is effectively a no-op. For jira_snowflake, upgrading from encrypt:"yes" to serializer:encdec is the intended use case.

  • [integer-overflow] backend/plugins/github_snowflake/tasks/shared.go:36nullInt converts int64 to int. On 32-bit platforms this truncates silently, but the downstream models are typed as int and 32-bit builds are unlikely for this project.

  • [edge-case] backend/plugins/github_snowflake/tasks/convert_prs.go:75 — PR state mapping handles open/OPEN/MERGED/closed explicitly; other case variations fall through to CLOSED, which is a safe default.

  • [stale-data] backend/plugins/github_snowflake/tasks/sync_pull_requests.go:35 — Sync tasks use CreateOrUpdate but never delete stale tool-layer records. Domain tables are cleaned by convertors. Acceptable for MVP.

  • [fail-closed] backend/plugins/github_snowflake/tasks/task_data.go:86 — Empty authType falls through to keypair case; parseRSAPrivateKey("") fails, so the code is fail-closed (safe). Adding upfront validation in connection creation would improve the error message.

  • [input-validation] backend/plugins/github_snowflake/tasks/task_data.go:55validateOwnerRepo checks structure but not character set. Risk is minimal as values come from authenticated pipeline options.

  • [authorization-scope] No ADR documents this architectural addition. Consider creating one per CLAUDE.md section 3.2.

  • [file-naming] backend/plugins/github_snowflake/tasks/convert_accounts.go — Convertor file naming (convert_*.go) follows the jira_snowflake convention but differs from upstream github plugin (*_convertor.go). Internal consistency with the _snowflake plugin family is maintained.

  • [missing-cli-docs] docs/github-snowflake-local-testing.md — CLI flags --connectionId, --githubId, --fullName are introduced but the testing guide only shows API-based workflow.

  • [missing-cli-docs] backend/plugins/jira_snowflake/AGENTS.md--connectionId added and --projectKeys made required in jira_snowflake.go CLI but not documented.

  • [encryption-change] docs/jira-snowflake-local-testing.mdjira_snowflake connection model changed from encrypt:"yes" to serializer:encdec with new encryption migration. Testing guide doesn't mention this upgrade path.

  • [missing-api-docs] docs/github-snowflake-local-testing.md — GET, PATCH, DELETE connection endpoints are not documented in the testing guide.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (9)

Looks good to me — well-structured new plugin that follows the established jira_snowflake pattern with comprehensive query builder tests and documentation. A few minor items below for follow-up consideration.

Review

Findings

Low

  • [data-exposure] backend/plugins/github_snowflake/api/connection_api.go:57 — GET /connections returns the full SnowflakeGithubConnection struct including the decrypted PrivateKey field. This follows the pre-existing DevLake-wide pattern (jira_snowflake, github plugin RefreshToken, etc.) so is not a regression, but warrants a codebase-wide fix to redact secrets from connection list responses.

  • [edge-case] backend/plugins/github_snowflake/tasks/convert_prs.go:96 — PR state mapping handles "open" / "OPEN" but only checks lowercase "closed" in the merged-via-closed path. If Snowflake data returns "CLOSED" (uppercase), a merged PR would be classified as CLOSED instead of MERGED. Use strings.EqualFold for state comparisons.

  • [missing-validation] backend/plugins/github_snowflake/models/connection.go:32AuthType field lacks a validation tag; invalid values are accepted at connection creation but rejected at pipeline runtime. Add validate:"omitempty,oneof=keypair externalbrowser".

  • [missing-validation] backend/plugins/github_snowflake/models/connection.go:35 — When AuthType is keypair (or empty/default), PrivateKey is required but not validated at connection creation time. Add conditional validation.

  • [logic-error] backend/plugins/github_snowflake/tasks/task_data.go:103OpenSnowflakeDB calls sql.Open without db.Ping(), deferring credential/connectivity errors to the first subtask query. Same pattern as jira_snowflake; adding db.Ping() would improve fail-fast behavior.

  • [missing-feature] backend/plugins/github_snowflake/tasks/sync_accounts.go:35SyncAccounts does not honor syncPolicy.TimeAfter unlike all other sync tasks. The accounts CTE query makes this non-trivial, but worth documenting the intentional omission or adding support in a follow-up.

  • [concurrent-use-risk] backend/plugins/github_snowflake/AGENTS.md:103 — No runtime detection if the same repo is configured in both GitHub API and github_snowflake connections (domain ID duplication risk). Currently documented as a warning only.

  • [cli-consistency] backend/plugins/jira_snowflake/jira_snowflake.go:32jira_snowflake CLI now requires --connectionId flag (previously optional). Verify existing automation passes this flag.

  • [interface-compliance] backend/plugins/github_snowflake/impl/impl.go:36github_snowflake implements plugin.CloseablePluginTask while jira_snowflake does not; minor cross-plugin inconsistency.

Previous run (10)

Review

Findings

Medium

  • [double-encryption / migration idempotency] backend/plugins/jira_snowflake/models/migrationscripts/encrypt_private_key.go:62 — The encryptPrivateKey migration unconditionally encrypts every non-empty private_key value without checking whether it is already encrypted. If the migration runs a second time (e.g., due to a migration-history reset), previously-encrypted values will be double-encrypted and become permanently unrecoverable. The same issue exists in the github_snowflake counterpart at backend/plugins/github_snowflake/models/migrationscripts/encrypt_private_key.go:62.
    Remediation: Before calling plugin.Encrypt, attempt plugin.Decrypt(encKey, row.PrivateKey). If decryption succeeds, skip the row — it is already encrypted.

  • [missing incremental sync support] backend/plugins/github_snowflake/tasks/sync_accounts.go:37 — All other sync subtasks (syncPullRequests, syncPrCommits, syncPrReviews, syncReviewers) honor SyncPolicy.TimeAfter by adding time-based filters to the Snowflake query. syncAccounts does not and always performs a full scan of all user accounts referenced by the repository.
    Remediation: Read subtaskCtx.TaskContext().SyncPolicy().TimeAfter and pass it to buildAccountsQuery, adding an appropriate time filter.

Low

  • [init_schema serializer tag] backend/plugins/github_snowflake/models/migrationscripts/init_schema.go:46 — The initSchema snapshot struct includes serializer:encdec on PrivateKey. While practically harmless (both migrations run in sequence with no API available between them), best practice for migration snapshots is to represent the plain schema state before encryption was added.

  • [data-exposure] backend/plugins/github_snowflake/models/connection.go:36 — The PrivateKey field uses json:"privateKey", exposing the decrypted RSA key in all connection API responses. This follows the existing DevLake framework pattern (all plugins expose credentials in GET responses), but RSA private keys for Snowflake JWT auth are high-value credentials worth considering for redaction. Same applies to jira_snowflake/models/connection.go:38.

  • [input-validation] backend/plugins/github_snowflake/models/connection.go:33AuthType field has no validate struct tag. Invalid values (e.g., typos like "keypai") are accepted and stored at the API layer, only rejected at pipeline runtime in OpenSnowflakeDB.
    Remediation: Add validate:"omitempty,oneof=keypair externalbrowser".

  • [scope-creep] backend/plugins/jira_snowflake/ — The PR bundles three changes to jira_snowflake beyond the stated scope: (1) encryption model migration from encrypt:"yes" to serializer:encdec, (2) CLI connectionId flag addition and required-flag markings, (3) OpenSnowflakeDB switch refactor. These are reasonable consistency improvements but should be noted in the PR description.

  • [code-duplication] backend/plugins/github_snowflake/tasks/task_data.go:99OpenSnowflakeDB and parseRSAPrivateKey are full copies from jira_snowflake (~60 lines). Consider extracting to a shared helpers/snowflakehelper/ package.

  • [progress-reporting] backend/plugins/github_snowflake/tasks/sync_pull_requests.go — None of the sync tasks call subtaskCtx.SetProgress() for periodic progress reporting, unlike jira_snowflake/tasks/sync_issues.go which reports every 500 rows.

  • [naming-inconsistency] backend/plugins/github_snowflake/tasks/shared.go:39 — Helper nullStr has a different name than the equivalent stringVal in jira_snowflake/tasks/sync_issues.go:258.

  • [interface-assertion] backend/plugins/github_snowflake/impl/impl.go — Includes plugin.CloseablePluginTask in compile-time assertion; jira_snowflake omits it despite implementing Close(). Consider adding it to jira_snowflake for compile-time safety.

  • [stale-code-snippet] docs/research/snowflake-jira-plugin.md:188 — Code snippet shows encrypt:"yes" on PrivateKey field, which this PR changes to serializer:encdec.


Labels: PR adds a new Snowflake-backed GitHub data ingestion plugin

Previous run (11)

Review

Findings

High

  • [secrets handling] backend/plugins/github_snowflake/models/connection.go:36 — The PrivateKey field uses encrypt:"yes" as a struct tag, but this tag has no runtime processor in this codebase. The actual at-rest encryption mechanism is gorm:"serializer:encdec", which is missing from this field. As a result, the RSA private key PEM is stored in plaintext in the MySQL database (_tool_github_snowflake_connections.private_key column). All other plugins that store credentials (tokens, passwords) use gorm:"serializer:encdec" on their sensitive fields. The same bug exists in jira_snowflake.
    Remediation: Add gorm:"column:private_key;serializer:encdec" to the PrivateKey field in models/connection.go and the corresponding migration model in migrationscripts/init_schema.go.

Medium

  • [nil-deref] backend/plugins/github_snowflake/tasks/sync_pull_requests.go:64 — The variable updatedAt is declared as time.Time (non-pointer) but is scanned from pr.UPDATED_AT, which can be NULL in Snowflake. Scanning a SQL NULL into a non-pointer time.Time causes a scan error, aborting the entire sync for the repository. Other nullable columns in the same function (closedAt, mergedAt, isDraft, etc.) correctly use pointer types.
    Remediation: Change updatedAt to *time.Time and handle the nil case when assigning to pr.GithubUpdatedAt (e.g., dereference with a zero-value fallback).

Low

  • [domain layer orphan accumulation] backend/plugins/github_snowflake/tasks/sync_pull_requests.go:94 — Sync tasks create tool-layer rows using common.NewNoPKModel() which leaves RawDataOrigin fields empty. The convertors include explicit full-sync deletion that partially mitigates this, but incremental mode will not clean up items deleted from the Snowflake source. Matches the existing jira_snowflake accepted pattern.

  • [data-exposure] backend/plugins/github_snowflake/api/connection_api.go:55GetConnections/GetConnection endpoints return the full connection struct including the PrivateKey field via json:"privateKey". This follows the framework pattern used by all DevLake plugins, but the RSA private key is more sensitive than a rotatable API token. See also: [secrets handling] finding above.

  • [code-organization] backend/plugins/github_snowflake/tasks/task_data.go:119OpenSnowflakeDB and parseRSAPrivateKey are verbatim copies from jira_snowflake/tasks/task_data.go. Consider extracting to a shared package to reduce maintenance surface.

  • [code-organization] backend/plugins/github_snowflake/tasks/task_data.go:153repoShortName function is defined and tested but never called from production code. The same logic is implemented inline in impl/impl.go PrepareTaskData using a manual byte-scanning loop.
    Remediation: Use the repoShortName function from task_data.go in impl.go instead of the inline loop.

  • [dead-code] backend/plugins/github_snowflake/impl/impl.go:127 — The short name extraction has a redundant outer guard (if idx := len(op.Name) - 1; idx >= 0). The idx variable is never used inside the block — the inner for-loop re-derives i := len(op.Name) - 1.

  • [authentication] backend/plugins/github_snowflake/tasks/task_data.go:113AuthType accepts any string; values other than "externalbrowser" silently fall through to keypair auth. A typo like "keypai" would fail with an opaque PEM-parsing error instead of a clear validation message.

  • [input-validation] backend/plugins/github_snowflake/tasks/task_data.go:92Name/FullName (expected owner/repo format) is not validated for format correctness. A malformed value produces invalid URLs but does not cause crashes.

  • [import-ordering] backend/plugins/table_info_test.go:41 — The github_snowflake import is inserted between github and githubGraphql, breaking alphabetical ordering. github_snowflake should sort after github_graphql.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

@rsoaresd rsoaresd left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Excellent work 🚀 Still testing locally, but it seems all good 🚀 Just minor suggestions:

timeAfter := cmd.Flags().StringP("timeAfter", "a", "", "only sync records created/updated after this time (RFC3339)")

cmd.Run = func(cmd *cobra.Command, args []string) {
runner.DirectRun(cmd, args, PluginEntry, map[string]interface{}{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

should we also pass the connectionId here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

good point! done in 7b025bf

### Minimal real-run setup

1. Start MySQL: `podman compose -f docker-compose-dev.yml up -d mysql`
2. Ensure `.env` has `DB_URL`, `ENCRYPTION_SECRET`, and (for local API) `AUTH_ENABLED=false`

@rsoaresd rsoaresd Aug 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not sure if it is relevant, but env.example mentions DB_URL=mysql://merico:merico@mysql:3306/lake?charset=utf8mb4&parseTime=True&loc=UTC, I needed to change to DB_URL=mysql://merico:merico@localhost:3306/lake?charset=utf8mb4&parseTime=True&loc=UTC

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

makes sense, I've improved a bit the docs , see 7b025bf

Encrypt Snowflake private keys at rest via gorm encdec (including
jira_snowflake), handle nullable PR updated_at, and tighten AuthType
and owner/repo validation.

Co-Authored-By: Cursor Grok 4.5 <noreply@example.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 3, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 10:33 AM UTC · Ended 10:39 AM UTC
Commit: 701e62a · View workflow run →

Require connectionId for standalone runs (and jira_snowflake) and note
that native make run needs localhost in DB_URL.

Co-Authored-By: Cursor Grok 4.5 <noreply@example.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 3, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 10:40 AM UTC · Ended 10:57 AM UTC
Commit: 701e62a · View workflow run →

@qodo-app-for-konflux-ci

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7b025bf

@fullsend-ai-review
fullsend-ai-review Bot dismissed their stale review August 3, 2026 10:56

Superseded by updated review

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment enhancement New feature or request labels Aug 3, 2026
@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:40 AM UTC · Completed 10:56 AM UTC
Commit: 701e62a · View workflow run →

Document that github migrations must create shared _tool_github_* tables
before pipelines run, and add troubleshooting for the common failures.

Co-Authored-By: Cursor Grok 4.5 <noreply@example.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 3, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 12:18 PM UTC · Ended 12:37 PM UTC
Commit: 701e62a · View workflow run →

@qodo-app-for-konflux-ci

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 171f31b

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added ready-for-merge All reviewers approved — ready to merge and removed requires-manual-review Review requires human judgment labels Aug 3, 2026
@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:18 PM UTC · Completed 12:37 PM UTC
Commit: 701e62a · View workflow run →

Add console verification steps and troubleshooting for missing role
grants before local plugin testing.

Co-Authored-By: Cursor Grok 4.5 <noreply@example.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 3, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 12:51 PM UTC · Ended 1:06 PM UTC
Commit: 701e62a · View workflow run →

@qodo-app-for-konflux-ci

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 64fdf81

@qodo-app-for-konflux-ci

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit cf67860

@fullsend-ai-review
fullsend-ai-review Bot dismissed stale reviews from themself August 6, 2026 10:07

Superseded by updated review

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the requires-manual-review Review requires human judgment label Aug 6, 2026
@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 9:53 AM UTC · Completed 10:07 AM UTC
Commit: 701e62a · View workflow run →

Align SyncAccounts with other sync tasks so tool-layer account rows carry
_raw_data_params for full-sync domain cleanup.

Co-Authored-By: Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 6, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 11:29 AM UTC · Ended 11:46 AM UTC
Commit: 701e62a · View workflow run →

@qodo-app-for-konflux-ci

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 0f1041e

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:29 AM UTC · Completed 11:46 AM UTC
Commit: 701e62a · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 13, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:50 PM UTC · Completed 2:08 PM UTC

Commit: 1e64447 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot removed the requires-manual-review Review requires human judgment label Aug 13, 2026
repoId := data.Options.GithubId
fullName := data.Options.Name

query, args := buildAccountsQuery(repoId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

buildAccountsQuery takes no timeAfter parameter, so every run fetches all accounts. Is it the plan for the downstream convertor to reconvert all accounts every run? Not a blocker at pilot scale but might be worth adding a timestamp filter and conditional update before scaling.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That is intentional for now since a naive timeAfter on user rows won’t shrink much, most of the cost is the repo_users UNION over PR/review history.

Also I think accounts don’t have a natural “updated for this repo” timestamp and filtering on USER/USER_EMAIL alone can miss new associations (same user appears on a new PR).

I would leave it unfiltered for the time being, and improve/optimize it later if we observe any issues/slowness.

WDYT?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Makes sense — good point about the missing associations. Agree its fine to leave as-is for now.

u.LOGIN AS author_name,
im.MERGED_AT,
im.ACTOR_ID AS merged_by_id,
mu.LOGIN AS merged_by_name

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since they are missing from MARTS additions, deletions, and comment counts aren't in this SELECT. Some of our dashboard metrics use PR size (additions/deletions) and we may like to use them more going forward. Is there any future path to getting these from Fivetran?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That's is a gap in the available data indeed. For this we will have to ask the Fivetran data platform whether additions/deletions (and comment totals) can be added to MARTS or a side table.

But I haven't started this conversation yet with that team.

@rsoaresd rsoaresd left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Amazing work!! 🚀 I tested with GITHUB_MARTS_GROUP role and it works very well!

…tream-diffs conflict

Make ConvertAccounts filter on gra.repo_github_id with an INNER JOIN so the query is unambiguous. Keep both the snowflakehelper and pkg/ notes when resolving the merge conflict in docs/upstream-diffs.md.

Co-Authored-By: Cursor Grok 4.6 <noreply@example.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 19, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:30 AM UTC · Completed 10:45 AM UTC

Commit: 9ee3c25 · View workflow run →

@fullsend-ai-review
fullsend-ai-review Bot dismissed their stale review August 19, 2026 10:45

Superseded by updated review

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the requires-manual-review Review requires human judgment label Aug 19, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 20, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 2:01 PM UTC · Completed 2:21 PM UTC

Commit: 9ee3c25 · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

// ParseRSAPrivateKey parses a PKCS#8 PEM-encoded RSA private key.
func ParseRSAPrivateKey(pemStr string) (*rsa.PrivateKey, error) {
block, _ := pem.Decode([]byte(pemStr))
if block == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] error-handling-gap

Open returns *sql.DB without calling db.Ping() to verify the connection is reachable. sql.Open only validates DSN syntax; bad credentials or unreachable Snowflake host surface as confusing errors on the first sync query. Matches existing jira_snowflake pattern and is standard Go database/sql behavior.

Suggested fix: Consider adding db.PingContext(ctx) after sql.Open to fail fast on configuration errors. This requires accepting a context.Context parameter.

- **Scope unit is `GithubRepo`** (numeric `githubId` + `fullName` owner/repo).
- **No raw-table layer**: writes directly to `_tool_github_*`. Sync tasks populate
`RawDataOrigin` (`_raw_data_table` / `_raw_data_params`) on tool-layer rows so
convertors can delete domain records by `_raw_data_params` on full sync

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] operational-constraint

Documented constraint that a repo must not be configured in both a GitHub API connection and a github_snowflake connection is not enforced at runtime. Domain ID duplication could result.

Suggested fix: Consider adding a runtime check in PrepareTaskData that queries existing GithubRepo scopes for the same githubId on a different connection type.

See the License for the specific language governing permissions and
limitations under the License.
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] missing-DependencyTables

Sync subtask metas do not declare DependencyTables or ProductTables. The framework cannot track table ownership for sync tasks. Matches the jira_snowflake pattern.

Suggested fix: Add ProductTables declarations to sync subtask metas.

if since != nil {
clauses = append(clauses, dal.Where("_tool_github_accounts.updated_at >= ?", since))
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] test-coverage-gap

convertOrphanedRepoAccounts uses a LEFT JOIN with IS NULL filter but has no unit tests for the orphan-handling logic.

Suggested fix: Add unit tests for the orphan repo accounts handling.

return &op, nil
}

// validateOwnerRepo checks that name is in "owner/repo" format.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] validation-message-format

Validation error messages inconsistently include the invalid value. connectionId includes the value via fmt.Sprintf, but name does not.

Suggested fix: Update all validation errors to consistently include or exclude the invalid value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request requires-manual-review Review requires human judgment Review effort 4/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants