Skip to content

fix: add SQS migrated project link back to source dashboard#484

Merged
okorach-sonar merged 1 commit into
release-1-1from
fix/issue-418-migrated-project-link
Jul 10, 2026
Merged

fix: add SQS migrated project link back to source dashboard#484
okorach-sonar merged 1 commit into
release-1-1from
fix/issue-418-migrated-project-link

Conversation

@okorach-sonar

Copy link
Copy Markdown
Contributor

Summary

Fixes #418

Test plan

  • go build ./...
  • go vet ./...
  • go test ./... (full suite green)
  • New unit test TestProjectSourceLinkURL covering the URL builder
  • New integration tests TestSetProjectSourceLink (create path) and TestSetProjectSourceLinkSkipsExisting (idempotency)

Adds a "SQS migrated project" link to every migrated SonarQube Cloud project, pointing back to the original SonarQube Server project's dashboard, so Support can identify migrated projects and trace them to their source (#418).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@okorach-sonar
okorach-sonar requested a review from a team as a code owner July 10, 2026 18:58
Comment on lines +1086 to +1092
func projectSourceLinkURL(baseURL, projectKey string) string {
if baseURL == "" || projectKey == "" {
return ""
}
base := strings.TrimRight(baseURL, "/")
return fmt.Sprintf("%s/dashboard?id=%s", base, url.QueryEscape(projectKey))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Edge Case: Idempotency may miss escaped project keys, creating duplicate links

In runSetProjectSourceLink, the built link URL uses url.QueryEscape(projectKey), so a project key containing special characters (e.g. the very common Maven-style com.company:project) becomes .../dashboard?id=com.company%3Aproject. The idempotency check compares the freshly-built linkURL against l.URL returned by e.Cloud.Projects.ListLinks with an exact string match (l.URL == linkURL). If SonarQube Cloud stores/returns the URL in a decoded/normalized form (e.g. %3A decoded back to :) the comparison fails and a re-run creates a duplicate "SQS migrated project" link.

Note the sibling issue-sync idempotency helper (TestSourceLinkIdempotencyHelpers) explicitly guards against HTML-escaped URL variants, indicating the team already knows cloud can return transformed URLs — but the project-link path does no such normalization. The two tests only exercise keys with no special chars (proj1), so this gap is untested.

Suggested fix: normalize both sides before comparison (e.g. compare on decoded URL or match on link name alone since the migration only ever creates one link with migratedProjectLinkName), or add a test case with a :-containing project key.

Was this helpful? React with 👍 / 👎

Comment on lines +1105 to +1115
err := forEachMigrateItem(ctx, e, "setProjectSourceLink", "createProjects",
func(ctx context.Context, item json.RawMessage, w *common.ChunkWriter) error {
cloudKey := extractField(item, "cloud_project_key")
if cloudKey == "" {
return nil
}
baseURL := resolveSourceBaseURL(e, extractField(item, "server_url"))
linkURL := projectSourceLinkURL(baseURL, extractField(item, "key"))
if linkURL == "" {
return 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.

💡 Performance: resolveSourceBaseURL re-reads getServerSettings per project

runSetProjectSourceLink calls resolveSourceBaseURL(e, ...) inside the forEachMigrateItem callback, which runs concurrently once per migrated project. resolveSourceBaseURL in turn calls readExtractItems(e, "getServerSettings")structure.ReadExtractData, which reads and parses the entire getServerSettings extract from disk on every invocation. For a migration with N projects this re-reads/re-parses the same file N times (and concurrently). The server base URL is effectively per-server (usually a single source server), so this is redundant work.

This mirrors the existing issue/hotspot-sync pattern (also once per project), so it is not a regression, but it is worth resolving the base URL once (or memoizing per server_url) before the loop to avoid repeated disk I/O and JSON parsing on large migrations.

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 0 resolved / 2 findings

Adds a link to source dashboards for migrated SQS projects, including new validation logic and integration tests. Consider refining the idempotency check to handle URL-encoded keys and caching server settings to avoid redundant lookups.

💡 Edge Case: Idempotency may miss escaped project keys, creating duplicate links

📄 go/internal/migrate/tasks_associate.go:1086-1092 📄 go/internal/migrate/tasks_associate.go:1122-1128

In runSetProjectSourceLink, the built link URL uses url.QueryEscape(projectKey), so a project key containing special characters (e.g. the very common Maven-style com.company:project) becomes .../dashboard?id=com.company%3Aproject. The idempotency check compares the freshly-built linkURL against l.URL returned by e.Cloud.Projects.ListLinks with an exact string match (l.URL == linkURL). If SonarQube Cloud stores/returns the URL in a decoded/normalized form (e.g. %3A decoded back to :) the comparison fails and a re-run creates a duplicate "SQS migrated project" link.

Note the sibling issue-sync idempotency helper (TestSourceLinkIdempotencyHelpers) explicitly guards against HTML-escaped URL variants, indicating the team already knows cloud can return transformed URLs — but the project-link path does no such normalization. The two tests only exercise keys with no special chars (proj1), so this gap is untested.

Suggested fix: normalize both sides before comparison (e.g. compare on decoded URL or match on link name alone since the migration only ever creates one link with migratedProjectLinkName), or add a test case with a :-containing project key.

💡 Performance: resolveSourceBaseURL re-reads getServerSettings per project

📄 go/internal/migrate/tasks_associate.go:1105-1115 📄 go/internal/migrate/helpers.go:104-106

runSetProjectSourceLink calls resolveSourceBaseURL(e, ...) inside the forEachMigrateItem callback, which runs concurrently once per migrated project. resolveSourceBaseURL in turn calls readExtractItems(e, "getServerSettings")structure.ReadExtractData, which reads and parses the entire getServerSettings extract from disk on every invocation. For a migration with N projects this re-reads/re-parses the same file N times (and concurrently). The server base URL is effectively per-server (usually a single source server), so this is redundant work.

This mirrors the existing issue/hotspot-sync pattern (also once per project), so it is not a regression, but it is worth resolving the base URL once (or memoizing per server_url) before the loop to avoid repeated disk I/O and JSON parsing on large migrations.

🤖 Prompt for agents
Code Review: Adds a link to source dashboards for migrated SQS projects, including new validation logic and integration tests. Consider refining the idempotency check to handle URL-encoded keys and caching server settings to avoid redundant lookups.

1. 💡 Edge Case: Idempotency may miss escaped project keys, creating duplicate links
   Files: go/internal/migrate/tasks_associate.go:1086-1092, go/internal/migrate/tasks_associate.go:1122-1128

   In `runSetProjectSourceLink`, the built link URL uses `url.QueryEscape(projectKey)`, so a project key containing special characters (e.g. the very common Maven-style `com.company:project`) becomes `.../dashboard?id=com.company%3Aproject`. The idempotency check compares the freshly-built `linkURL` against `l.URL` returned by `e.Cloud.Projects.ListLinks` with an exact string match (`l.URL == linkURL`). If SonarQube Cloud stores/returns the URL in a decoded/normalized form (e.g. `%3A` decoded back to `:`) the comparison fails and a re-run creates a duplicate "SQS migrated project" link.
   
   Note the sibling issue-sync idempotency helper (`TestSourceLinkIdempotencyHelpers`) explicitly guards against HTML-escaped URL variants, indicating the team already knows cloud can return transformed URLs — but the project-link path does no such normalization. The two tests only exercise keys with no special chars (`proj1`), so this gap is untested.
   
   Suggested fix: normalize both sides before comparison (e.g. compare on decoded URL or match on link name alone since the migration only ever creates one link with `migratedProjectLinkName`), or add a test case with a `:`-containing project key.

2. 💡 Performance: resolveSourceBaseURL re-reads getServerSettings per project
   Files: go/internal/migrate/tasks_associate.go:1105-1115, go/internal/migrate/helpers.go:104-106

   `runSetProjectSourceLink` calls `resolveSourceBaseURL(e, ...)` inside the `forEachMigrateItem` callback, which runs concurrently once per migrated project. `resolveSourceBaseURL` in turn calls `readExtractItems(e, "getServerSettings")` → `structure.ReadExtractData`, which reads and parses the entire getServerSettings extract from disk on every invocation. For a migration with N projects this re-reads/re-parses the same file N times (and concurrently). The server base URL is effectively per-server (usually a single source server), so this is redundant work.
   
   This mirrors the existing issue/hotspot-sync pattern (also once per project), so it is not a regression, but it is worth resolving the base URL once (or memoizing per server_url) before the loop to avoid repeated disk I/O and JSON parsing on large migrations.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqubecloud

Copy link
Copy Markdown

@okorach-sonar
okorach-sonar merged commit 2785624 into release-1-1 Jul 10, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants