Skip to content

feat: add Docker Hub integration#2831

Closed
rakesh0x wants to merge 14 commits intosuperplanehq:mainfrom
rakesh0x:feat/dockerhub-integration
Closed

feat: add Docker Hub integration#2831
rakesh0x wants to merge 14 commits intosuperplanehq:mainfrom
rakesh0x:feat/dockerhub-integration

Conversation

@rakesh0x
Copy link

@rakesh0x rakesh0x commented Feb 3, 2026

Summary

This PR adds a new Docker Hub integration for SuperPlane as described in #2243.

Changes

Base Integration

  • Docker Hub integration with username/access token authentication
  • Validates credentials on sync by testing Docker Hub API access
  • Uses Docker Hub v2 API with JWT token authentication

Trigger: On Image Pushed

  • Receives Docker Hub webhook events when images are pushed
  • Supports optional tag filtering with wildcard patterns
  • Emits dockerhub.imagePushed event with push details

Action: List Tags

  • Lists tags for a specified Docker Hub repository
  • Supports optional pageSize and nameFilter parameters
  • Emits dockerhub.tags event with tag information

Frontend

  • Added UI mappers for trigger and action components
  • Displays repository, tag, and pusher information
Screen.Recording.2026-02-04.at.12.02.00.AM.mp4

Testing

  • Added comprehensive unit tests (27 test cases)
  • All tests passing

Closes #2243

@AleksandarCole
Copy link
Collaborator

@rakesh0x thank you for your submission. Please attach the video recording as specified in the Bounty Details in the issue.

@rakesh0x
Copy link
Author

rakesh0x commented Feb 3, 2026

@rakesh0x thank you for your submission. Please attach the video recording as specified in the Bounty Details in the issue.

Attached a video @AleksandarCole, check it out

@rakesh0x rakesh0x force-pushed the feat/dockerhub-integration branch from 6b406b4 to 7850eb0 Compare February 3, 2026 19:04
@AleksandarCole
Copy link
Collaborator

@rakesh0x the video shows Unit tests passing - which is great. However, we need the video of in-app usage of the integration and components. I tried running this in dev environment and I do not see either DockerHub integration appearing in UI or the components.

@rakesh0x
Copy link
Author

rakesh0x commented Feb 3, 2026

@rakesh0x the video shows Unit tests passing - which is great. However, we need the video of in-app usage of the integration and components. I tried running this in dev environment and I do not see either DockerHub integration appearing in UI or the components.

oh yeah, i haven't imported the dockerhub package anywhere , so the package haven't loaded
fixed this issue, should work now @AleksandarCole :)

@lucaspin
Copy link
Contributor

lucaspin commented Feb 3, 2026

@rakesh0x how is the webhook setup working? Looks like DockerHub webhooks are per repository

@rakesh0x
Copy link
Author

rakesh0x commented Feb 3, 2026

@rakesh0x how is the webhook setup working? Looks like DockerHub webhooks are per repository

Docker Hub doesn't provide an API to programmatically create webhooks, they must be manually configured by users in the Docker Hub UI per repository.

@rakesh0x
Copy link
Author

rakesh0x commented Feb 3, 2026

Screenshot 2026-02-04 at 12 58 19 AM

yup, now the integration appears in the list @AleksandarCole

@rakesh0x
Copy link
Author

rakesh0x commented Feb 3, 2026

@lucaspin The 4 failing E2E tests are unrelated to docker hub integration , these appear to be pre existing flaky tests, could you please confirm , if these tests are known to be flaky

@lucaspin
Copy link
Contributor

lucaspin commented Feb 4, 2026

Docker Hub doesn't provide an API to programmatically create webhooks, they must be manually configured by users in the Docker Hub UI per repository.

@rakesh0x and how are users doing this? Which SuperPlane URL are you using to do that?

@rakesh0x
Copy link
Author

rakesh0x commented Feb 4, 2026

Docker Hub doesn't provide an API to programmatically create webhooks, they must be manually configured by users in the Docker Hub UI per repository.

@rakesh0x and how are users doing this? Which SuperPlane URL are you using to do that?

When users set up the "On Image Pushed" trigger in SuperPlane, the system generates a unique webhook URL (like https://your-superplane.com/webhooks/abc123).

Users then copy this URL and paste it into their Docker Hub repo settings under Webhooks. It's a manual step since Docker Hub doesn't have an API for creating webhooks programmatically.

The URL is shown in the trigger configuration UI after setup.

@rakesh0x
Copy link
Author

rakesh0x commented Feb 5, 2026

hey @lucaspin , can you please review and merge if everything is good

@lucaspin
Copy link
Contributor

lucaspin commented Feb 5, 2026

@rakesh0x I checked out your changes, but I can't even open the application:

Screenshot 2026-02-05 at 09 20 03

Also, CI is failing.

@rakesh0x rakesh0x force-pushed the feat/dockerhub-integration branch from 3e7da08 to 634b7a2 Compare February 5, 2026 12:34
@rakesh0x
Copy link
Author

rakesh0x commented Feb 5, 2026

@rakesh0x I checked out your changes, but I can't even open the application:

Screenshot 2026-02-05 at 09 20 03 Also, [CI is failing](https://superplanehq.semaphoreci.com/workflows/c211a1db-49c7-45f1-92ad-7a4839036f53?pipeline_id=dc878c44-a4b7-4dce-b936-20f86e02d0ba).

i had to keep merging the branch again and again, so it created a merge conflicts
fixed the issue , should work now

@rakesh0x rakesh0x force-pushed the feat/dockerhub-integration branch from ce5ca2d to 2ae3d7f Compare February 5, 2026 13:03
@rakesh0x
Copy link
Author

rakesh0x commented Feb 5, 2026

@lucaspin fixed the ci as well

@lucaspin
Copy link
Contributor

lucaspin commented Feb 5, 2026

@rakesh0x tested and reviewed your PR. It's not there yet, but it has potential. Here's a list of things we need to address before being able to merge it.

Missing icon on integrations page and on component sidebar

Screenshot 2026-02-05 at 11 58 30 Screenshot 2026-02-05 at 11 03 02

Do not authenticate every time

We are calling authenticate() every time, which is not efficient. The JWT we get back from DockerHub has an expiration time. We should store it as an integration secret, reuse for operations, and use ctx.Integration.ScheduleActionCall to schedule a refresh operation before it expires.

listTags component

We have been discussing list operations in this thread. I don't really see a concrete use case for that one, so I'd just remove it for now.

I think a better alternative component here would be a describeImageTag instead. The use case for that would be: since the webhook payload from DockerHub does not include all the information about the image being pushed, you can grab the additional information through describeImageTag.

onImagePushed component

A couple of notes for this one:

  • Rename it to onImagePush. Check this comment for the reason why.
  • The repository configuration field should be an IntegrationResource field type, so user can select from a list. We can use this API endpoint to fetch repositories. We will need to include a namespace configuration for this to work properly too, and use this idea to properly list repositories in the correct namespace.
  • Rename the tagFilter configuration field to just tags, and use AnyPredicateList as the type. That is a common and established pattern for filtering things already in the system already. Check github.onPush for example.
  • On Setup(), verify that repository exists in Docker Hub. Once verified, use an object to store information the repository in the trigger metadata. That makes it easier to extend the trigger with more information in the future, if needed, and also allows us to store more information about the repository, like the URL. Check the semaphore.onPipelineDone for example.
  • There is no way for the user to know which URL to use to create the trigger in DockerHub. Use ctx.Webhook.Setup() instead of ctx.Integration.RequestWebhook(), since the webhook won't be provisioned through the integration, and store the URL returned by it in the metadata. Also, to show that to the user, you will need a CustomFieldRenderer in your mapper. You can check how the webhook trigger does it. If we store the repository URL in the metadata, we can even have the link pointing the user to the proper page in DockerHub to create the webhook.

@rakesh0x rakesh0x force-pushed the feat/dockerhub-integration branch from cf7f5d5 to fb37e9c Compare February 5, 2026 15:39
if payload.Repository.RepoName != expectedRepo {
// Not for this repository, ignore
return http.StatusOK, nil
}
Copy link

Choose a reason for hiding this comment

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

Case-sensitive repository comparison may silently drop webhooks

Medium Severity

The webhook handler compares payload.Repository.RepoName against expectedRepo using case-sensitive string comparison. Docker Hub normalizes repository names to lowercase in webhook payloads, but the expectedRepo is constructed from config.Namespace and config.Repository which come from user input without normalization. If a user configures "MyOrg/MyApp" but Docker Hub sends "myorg/myapp" in the webhook, the comparison fails and events are silently ignored.

Fix in Cursor Fix in Web

}
// Default namespace is "library" for official images
return "library", repoName
}
Copy link

Choose a reason for hiding this comment

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

Unused function splitRepository is dead code

Low Severity

The splitRepository function is defined but never called anywhere in the codebase. This helper function should be removed or used.

Fix in Cursor Fix in Web

export interface OnImagePushedConfiguration {
repository?: string;
tagFilter?: string;
}
Copy link

Choose a reason for hiding this comment

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

Unused legacy types in new integration

Low Severity

OnImagePushedMetadata and OnImagePushedConfiguration are labeled as "Legacy types for backwards compatibility" but this is a brand new integration. These types are never used anywhere in the codebase.

Fix in Cursor Fix in Web

next?: string;
previous?: string;
results?: Tag[];
}
Copy link

Choose a reason for hiding this comment

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

Unused ListTagsResponse TypeScript interface

Low Severity

The ListTagsResponse interface is defined but never imported or used anywhere in the frontend codebase.

Fix in Cursor Fix in Web

@AleksandarCole AleksandarCole added the bounty This issue has a bounty open label Feb 6, 2026
ticket: Superplane 2131

Added Jira integration

<img width="1511" height="807" alt="Screenshot 2026-02-03 at 09 52 49"
src="https://github.com/user-attachments/assets/0ad4a199-f15b-4b32-8b23-0cbb57256759"
/>

<img width="1510" height="808" alt="Screenshot 2026-02-03 at 09 52 57"
src="https://github.com/user-attachments/assets/e05d2024-2adb-4c0e-ba93-f8a756c51103"
/>

<img width="1000" height="801" alt="Screenshot 2026-02-05 at 13 21 38"
src="https://github.com/user-attachments/assets/848ce4a2-ab70-45a1-94ce-6c43412f9423"
/>
<img width="450" height="718" alt="Screenshot 2026-02-05 at 13 21 21"
src="https://github.com/user-attachments/assets/590d85ee-ed5e-4f1b-ac3b-d4d2098d21ad"
/>

---------

Signed-off-by: alabro-bm <atanas.labroski@brightmarbles.io>
Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
rakesh0x and others added 13 commits February 6, 2026 19:18
…t Tags action

This adds a new Docker Hub integration for SuperPlane with:

- Docker Hub integration with username/access token authentication
- Validates credentials on sync by testing Docker Hub API access

- Receives Docker Hub webhook events when images are pushed
- Supports optional tag filtering with wildcard patterns (e.g., 'v*', '*-latest')
- Emits 'dockerhub.imagePushed' event with push details

- Lists tags for a specified Docker Hub repository
- Supports optional pageSize and nameFilter parameters
- Emits 'dockerhub.tags' event with tag information

- Added UI mappers for trigger and action components
- Displays repository, tag, and pusher information

Closes superplanehq#2243

Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
- Fix unreachable code: move contains wildcard check (*beta*) before
  prefix/suffix checks so it's properly matched
- Fix trigger setup: compare config.Repository with metadata.Repository
  to detect configuration changes (previously only checked non-empty)
- Remove unused GetRepository function from client.go
- Remove unused verifyWebhookSignature function and related imports
- Add tests for contains wildcard pattern matching
- Add test for repository configuration change scenario

Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
Add blank import for dockerhub package in server.go to trigger
the init() function that registers the integration with the registry.
This makes the Docker Hub integration appear in the UI.

Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
The HandleRequest function was intercepting webhook requests and only
logging them without routing to triggers. Changed to no-op pattern
(like PagerDuty) so the framework routes webhooks directly to the
OnImagePushed trigger's HandleWebhook method.

Removed unused imports: encoding/json, io, net/http, strings

Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
Moved metadata persistence to AFTER RequestWebhook succeeds. Previously,
if RequestWebhook failed after metadata was saved, retries would see
metadata matches config and skip the webhook request entirely.

Now the order is:
1. Validate config
2. Check if already setup (metadata matches config)
3. Request webhook
4. Only if webhook succeeds, persist metadata

Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
Each subtest now creates its own WaitSteps instance with the subtest's
*testing.T, matching the pattern used in approvals_test.go. This fixes
the 'subtest may have called FailNow on a parent test' error that
occurred when require assertions failed.

Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
Generated via scripts/generate_components_docs.go

Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
- Rename listTags -> describeImageTag (single tag lookup)
- Rename onImagePushed -> onImagePush (consistent naming)
- Add namespace configuration field for repository selection
- Use IntegrationResource with Parameters for repository field
- Use AnyPredicateList for tag filtering (consistent with github.onPush)
- Implement ctx.Webhook.Setup() for manual webhook URL generation
- Verify repository exists on Setup() and store repo info in metadata
- Add CustomFieldRenderer to display webhook URL with instructions
- Update frontend mappers for new component names
- Add dockerhub icon to integration icon maps

Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
continuation of this
superplanehq#2518

---------

Signed-off-by: Aleksandar Radosevic <aradosevicarchon@gmail.com>
Signed-off-by: Igor Šarčević <igor@operately.com>
Co-authored-by: Aleksandar Radosevic <aradosevicarchon@gmail.com>
Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
**Summary**
Adds full SendGrid integration support: base integration, Send Email
action, On Email Event trigger, Create/Update Contact action, UI
mappers, example payloads/docs, and webhook handling updates.

**Key Changes**
- **SendGrid integration (backend)**
- New SendGrid integration with sync/verification, webhook
setup/cleanup, signed webhook support, and tests.
- New Send Email action with text/html/template modes, categories,
failed channel handling, and tests.
- New On Email Event trigger with filtering, signature verification,
defaults, and tests.
- New Create/Update Contact action (Marketing Contacts API) with list
IDs/custom fields, failed channel handling, and tests.
  - Embedded JSON examples for SendGrid outputs/events.
- **API / webhook handling**
- Webhook route accepts `application/json` with optional charset
(utf-8).
- **Frontend/UI**
  - SendGrid icon + integration display name.
- Workflow mapper support for Send Email, On Email Event, Create/Update
Contact with failed state handling and metadata.
  - Building Blocks sidebar updates for SendGrid components/triggers.
- **Docs**
  - Generated SendGrid component docs.
<img width="862" height="728" alt="image"
src="https://github.com/user-attachments/assets/6457989e-f8b3-4d09-85f0-21943966228f"
/>

https://github.com/user-attachments/assets/f271bfca-046a-4120-8701-1dd0e93d4642

---------

Signed-off-by: Pedro F. Leao <pedroforestileao@gmail.com>
Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
## Summary
- Adjusted SendGrid trigger run item title to `email · Event Type` with
time-only subtitle
- Ensured Details tab always includes a timestamp and places errors last
for SendGrid actions
- Hid default trigger metadata (all events, `*` category) and summarized
long event lists
- Displayed SendGrid contact list IDs as specs/badges instead of inline
metadata
<img width="298" height="224" alt="image"
src="https://github.com/user-attachments/assets/c9c48f91-e912-480b-b476-c858b2cd4473"
/>
<img width="678" height="154" alt="image"
src="https://github.com/user-attachments/assets/bbd790af-10d0-4e7b-9a4f-a2299ec6c474"
/>

Signed-off-by: Pedro F. Leao <pedroforestileao@gmail.com>
Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
…assertion, example output)

Signed-off-by: Rakes <niu-24-14815@niu.edu.in>
@rakesh0x rakesh0x force-pushed the feat/dockerhub-integration branch from 0469f2e to 515aec8 Compare February 6, 2026 13:50
@rakesh0x rakesh0x closed this Feb 6, 2026
@rakesh0x rakesh0x deleted the feat/dockerhub-integration branch February 6, 2026 13:55
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 5 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

Default: "*",
Description: "Optional category filter (supports * wildcards)",
},
{},
Copy link

Choose a reason for hiding this comment

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

Empty configuration field in OnEmailEvent Configuration

High Severity

The Configuration() method includes a trailing empty configuration.Field{} in the returned slice. This zero-value struct has no name, label, or type, and will likely render as a broken or blank field in the UI form for the "On Email Event" trigger configuration.

Fix in Cursor Fix in Web

---
title: "SendGrid"
sidebar:
order: 15
Copy link

Choose a reason for hiding this comment

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

Duplicate sidebar order for Semaphore and SendGrid

Low Severity

Both Semaphore.mdx and SendGrid.mdx have order: 15 in their sidebar frontmatter. SendGrid appears after Semaphore alphabetically, so it needs order: 16, and Slack.mdx would need order: 17 to maintain proper ordering.

Additional Locations (1)

Fix in Cursor Fix in Web

return nil, fmt.Errorf(
"invalid private key format (expected PEM/OpenSSH block). Key length: %d, starts with: %q",
len(keyBytes), keyPreview,
)
Copy link

Choose a reason for hiding this comment

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

Private key material leaked in error message

Medium Severity

The getSigner method includes up to 50 characters of the raw private key bytes in its error message via keyPreview. This sensitive credential material could end up in logs, UI error displays, or monitoring systems. The error message already includes the key length, which is sufficient for debugging without exposing actual key content.

Fix in Cursor Fix in Web

command := spec.Command
if spec.WorkingDirectory != "" {
command = fmt.Sprintf("cd %s && %s", spec.WorkingDirectory, command)
}
Copy link

Choose a reason for hiding this comment

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

Working directory not shell-quoted in SSH command

Medium Severity

The WorkingDirectory value is interpolated directly into the shell command without quoting. Paths containing spaces, shell metacharacters, or special characters will cause the cd to fail or behave unexpectedly. The directory path needs to be shell-quoted in the fmt.Sprintf call.

Fix in Cursor Fix in Web


var passphrase []byte
if spec.Authentication.Passphrase.IsSet() {
passphrase, _ = ctx.Secrets.GetKey(spec.Authentication.Passphrase.Secret, spec.Authentication.Passphrase.Key)
Copy link

Choose a reason for hiding this comment

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

Missing error check on passphrase secret lookup

Medium Severity

When the user configures a passphrase secret reference and IsSet() returns true, the error from ctx.Secrets.GetKey is silently discarded with _. If the referenced secret or key doesn't exist, passphrase will be nil, and the SSH client will attempt to parse the encrypted key without a passphrase. This produces a misleading "failed to parse private key" error instead of a clear message about the missing passphrase credential — unlike the private key path directly above, which properly checks and wraps the error.

Fix in Cursor Fix in Web

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

Labels

bounty This issue has a bounty open

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Docker Hub] Base

6 participants