Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
00b87ff
feat: Add Buildkite integration
immz4 Feb 9, 2026
6571c99
Merge branch 'main' into feat/buildkite-integration
immz4 Feb 9, 2026
49cd1cc
fix: subscription routing error handling
immz4 Feb 10, 2026
42e9126
fix: frontend trigger metadata
immz4 Feb 10, 2026
b16fc43
fix: error propagation in polling function
immz4 Feb 10, 2026
3f5ce7a
fix: fix Trigger Build description
immz4 Feb 10, 2026
f089cf8
Merge remote-tracking branch 'upstream' into feat/buildkite-integration
immz4 Feb 10, 2026
e7bcc8d
fix: improved integration setup UX
immz4 Feb 10, 2026
bd4810c
fix: simplified org url check
immz4 Feb 10, 2026
a776857
fix: metadata and env handling
immz4 Feb 11, 2026
f31361f
fix: buildkite logo
immz4 Feb 11, 2026
dedb70a
fix: use fail state instead of error
immz4 Feb 11, 2026
e43bf66
fix: change detail info
immz4 Feb 11, 2026
e62da38
fix: added link for new api tokens to instructions
immz4 Feb 11, 2026
f557eb8
fix: update Buildkite.mdx
immz4 Feb 11, 2026
0f17905
Merge remote-tracking branch 'upstream/main' into feat/buildkite-inte…
immz4 Feb 11, 2026
accfc2e
fix: removed organisation from params
immz4 Feb 11, 2026
b6aa311
chore: updated docs, code cleanup
immz4 Feb 11, 2026
c81937d
fix: verify webhook before checking for event type
immz4 Feb 11, 2026
9a4ca91
fix: remove unused logic
immz4 Feb 13, 2026
e2340a4
chore: remove unused api call
immz4 Feb 13, 2026
e5326d8
fix: add event type to the BuildkiteSubscriptionConfiguration
immz4 Feb 15, 2026
e0900f0
Merge branch 'main' into feat/buildkite-integration
lucaspin Feb 17, 2026
05e9f7e
feat: webhook setup per trigger
immz4 Feb 18, 2026
0f5d0a5
chore: moved copy button to section title
immz4 Feb 18, 2026
a67afa7
chore: remove organization from ListResources
immz4 Feb 18, 2026
5208996
feat: improve webhook creation UX
immz4 Feb 18, 2026
3eb9902
chore: removed unused OnIntegrationMessage
immz4 Feb 19, 2026
64d6c49
fix: added token verification request
immz4 Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions docs/components/Buildkite.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
title: "Buildkite"
---

Trigger and react to your Buildkite builds

## Triggers

<CardGrid>
<LinkCard title="On Build Finished" href="#on-build-finished" description="Listen to Buildkite build completion events" />
</CardGrid>

import { CardGrid, LinkCard } from "@astrojs/starlight/components";

## Actions

<CardGrid>
<LinkCard title="Trigger Build" href="#trigger-build" description="Trigger a Buildkite build and wait for completion." />
</CardGrid>

## Instructions

To create new Buildkite API key, open [Personal Settings > API Access Tokens](https://buildkite.com/user/api-access-tokens/new).

<a id="on-build-finished"></a>

## On Build Finished

The On Build Finished trigger starts a workflow execution when a Buildkite build completes.

### Use Cases

- **CI/CD pipeline chaining**: Trigger downstream workflows when builds complete
- **Build monitoring**: Monitor build results and trigger alerts or notifications
- **Deployment orchestration**: Start deployment workflows only after successful builds
- **Build result processing**: Process build artifacts or results based on build outcome

### Configuration

- **Pipeline**: Select the Buildkite pipeline to monitor
- **Branch** (optional): Filter to specific branch (exact match)

### Event Data

Each build finished event includes:
- **build**: Build information including ID, state, result, commit, branch
- **pipeline**: Pipeline information including ID and name
- **organization**: Organization information
- **sender**: User who triggered the build

### Webhook Setup

This trigger requires setting up a webhook in Buildkite to receive build events:

1. When you configure this trigger, SuperPlane generates a unique webhook URL and token
2. A browser action will guide you to the Buildkite webhook settings page
3. In Buildkite, create a new webhook with:
- **Webhook URL**: The URL provided by SuperPlane
- **Webhook Token**: The token provided by SuperPlane
- **Events**: Select "build.finished"
- **Pipelines**: Select the specific pipeline(s) you want to monitor

### Event Processing

SuperPlane automatically:
1. Receives webhook events at the trigger-specific webhook URL
2. Authenticates requests using the webhook token
3. Filters events by pipeline and branch (if configured)
4. Emits buildkite.build.finished events to start workflow executions

### Example Data

```json
{
"build": {
"blocked": false,
"branch": "main",
"commit": "a1b2c3d4e5f6789012345678901234567890abcd",
"id": "12345678-1234-1234-1234-123456789012",
"message": "Fix: Update dependencies",
"number": 123,
"state": "passed",
"web_url": "https://buildkite.com/example-org/example-pipeline/builds/123"
},
"event": "build.finished",
"organization": {
"id": "example-org",
"name": "Example Organization"
},
"pipeline": {
"id": "example-pipeline",
"name": "Example Pipeline"
},
"sender": {
"email": "john@example.com",
"id": "user-123",
"name": "John Doe"
}
}
```

<a id="trigger-build"></a>

## Trigger Build

Trigger a Buildkite build and wait for completion using polling mechanism.

### How It Works

1. Triggers a build via Buildkite API
2. Monitors build status via polling
3. Emits to success/failure channels when build finishes

### Configuration

- **Pipeline**: Select pipeline to trigger
- **Branch**: Git branch to build
- **Commit**: Git commit SHA (optional, defaults to HEAD)
- **Message**: Optional build message
- **Environment Variables**: Optional env vars for the build
- **Metadata**: Optional metadata for the build

### Output Channels

- **Passed**: Build completed successfully
- **Failed**: Build failed, was cancelled, or was blocked

### Example Output

```json
{
"build": {
"blocked": false,
"branch": "main",
"commit": "a1b2c3d4e5f678901234567890abcd",
"id": "12345678-1234-1234-123456789012",
"message": "Triggered by SuperPlane",
"number": 123,
"state": "passed",
"web_url": "https://buildkite.com/example-org/example-pipeline/builds/123"
},
"organization": {
"id": "example-org"
},
"pipeline": {
"id": "example-pipeline"
}
}
```

212 changes: 212 additions & 0 deletions pkg/integrations/buildkite/buildkite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// Package buildkite implements the Buildkite integration for SuperPlane.
package buildkite

import (
"fmt"
"net/http"
"regexp"
"strings"

"github.com/mitchellh/mapstructure"
"github.com/superplanehq/superplane/pkg/configuration"
"github.com/superplanehq/superplane/pkg/core"
"github.com/superplanehq/superplane/pkg/registry"
)

func init() {
registry.RegisterIntegration("buildkite", &Buildkite{})
}

type Buildkite struct{}

type Configuration struct {
Organization string `json:"organization"`
APIToken string `json:"apiToken"`
}

type Metadata struct {
Organizations []string `json:"organizations"`
SetupComplete bool `json:"setupComplete"`
OrgSlug string `json:"orgSlug"`
}

func extractOrgSlug(orgInput string) (string, error) {
if orgInput == "" {
return "", fmt.Errorf("organization input is empty")
}

urlPattern := regexp.MustCompile(`(?:https?://)?(?:www\.)?buildkite\.com/(?:organizations/)?([^/]+)`)
if matches := urlPattern.FindStringSubmatch(strings.TrimSpace(orgInput)); len(matches) > 1 {
return matches[1], nil
Copy link

Choose a reason for hiding this comment

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

URL regex captures query params and fragments in slug

Medium Severity

The URL regex capture group ([^/]+) in extractOrgSlug matches everything after buildkite.com/ except forward slashes, so if a user pastes a URL with query parameters or a fragment (e.g., https://buildkite.com/my-org?tab=pipelines), the extracted slug becomes my-org?tab=pipelines. This causes all subsequent API calls and generated setup-action URLs to use an invalid org slug, producing confusing "not found" errors during integration setup. The capture group needs to also exclude ? and #.

Fix in Cursor Fix in Web

}

// Just the slug (validate it looks like a valid org slug)
slugPattern := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]$`)
if slugPattern.MatchString(strings.TrimSpace(orgInput)) {
return strings.TrimSpace(orgInput), nil
}
Copy link

Choose a reason for hiding this comment

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

Slug regex rejects valid single-character org slugs

Low Severity

The slugPattern regex ^[a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]$ requires at least two characters because it demands both a start and end character class match. A valid single-character org slug (e.g., "a") would be rejected with an "invalid organization format" error, even though Buildkite permits it.

Copy link

Choose a reason for hiding this comment

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

Single-character org slugs rejected as invalid

Low Severity

The slug validation regex ^[a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]$ requires a minimum of two characters (one anchored at each end with zero-or-more in between), so any single-character org slug fails validation and returns an error. This is also inconsistent: the same single-character slug succeeds if the user provides it as a full URL (e.g., https://buildkite.com/a) because the URL pattern matches first, bypassing the slug pattern entirely.

Fix in Cursor Fix in Web


return "", fmt.Errorf("invalid organization format: %s. Expected format: https://buildkite.com/my-org or just 'my-org'", orgInput)
}

func (b *Buildkite) createTokenSetupAction(ctx core.SyncContext) {
ctx.Integration.NewBrowserAction(core.BrowserAction{
Description: "Generate API token for triggering builds. Required permissions: `read_organizations`, `read_user`, `read_pipelines`, `read_builds`, `write_builds`.",
URL: "https://buildkite.com/user/api-access-tokens",
Method: "GET",
})
}

func (b *Buildkite) Name() string {
return "buildkite"
}

func (b *Buildkite) Label() string {
return "Buildkite"
}

func (b *Buildkite) Icon() string {
return "workflow"
}

func (b *Buildkite) Description() string {
return "Trigger and react to your Buildkite builds"
}

func (b *Buildkite) Instructions() string {
return "To create new Buildkite API key, open [Personal Settings > API Access Tokens](https://buildkite.com/user/api-access-tokens/new)."
}

func (b *Buildkite) Configuration() []configuration.Field {
return []configuration.Field{
{
Name: "organization",
Label: "Organization URL",
Type: configuration.FieldTypeString,
Description: "Buildkite organization URL (e.g. https://buildkite.com/my-org or just my-org)",
Placeholder: "e.g. https://buildkite.com/my-org or my-org",
Required: true,
},
{
Name: "apiToken",
Label: "API Token",
Type: configuration.FieldTypeString,
Sensitive: true,
Description: "Buildkite API token with permissions: read_organizations, read_user, read_pipelines, read_builds, write_builds",
Placeholder: "e.g. bkua_...",
Required: true,
},
}
}

func (b *Buildkite) Cleanup(ctx core.IntegrationCleanupContext) error {
return nil
}

func (b *Buildkite) Sync(ctx core.SyncContext) error {
config := Configuration{}
err := mapstructure.Decode(ctx.Configuration, &config)
if err != nil {
return fmt.Errorf("Failed to decode configuration: %v", err)
}

if config.Organization == "" {
return fmt.Errorf("Organization is required")
}

orgSlug, err := extractOrgSlug(config.Organization)
if err != nil {
return fmt.Errorf("Invalid organization format: %v", err)
}

// Prompt user to create API token
if config.APIToken == "" {
b.createTokenSetupAction(ctx)
return nil
}

// Validate the API token
client, err := NewClient(ctx.HTTP, ctx.Integration)
if err != nil {
return fmt.Errorf("failed to create client: %v", err)
}
_, err = client.ValidateToken()
if err != nil {
return fmt.Errorf("Invalid API token: %v", err)
}

// Update metadata to track setup completion
metadata := Metadata{}
if err := mapstructure.Decode(ctx.Integration.GetMetadata(), &metadata); err != nil {
metadata = Metadata{}
}

metadata.OrgSlug = orgSlug
metadata.SetupComplete = true
ctx.Integration.SetMetadata(metadata)

ctx.Integration.Ready()
return nil
}

func (b *Buildkite) ListResources(resourceType string, ctx core.ListResourcesContext) ([]core.IntegrationResource, error) {
client, err := NewClient(ctx.HTTP, ctx.Integration)
if err != nil {
return nil, fmt.Errorf("error creating client: %v", err)
}

switch resourceType {

case "pipeline":
orgConfig, err := ctx.Integration.GetConfig("organization")
if err != nil {
return nil, fmt.Errorf("failed to get organization from integration config: %w", err)
}
orgSlug, err := extractOrgSlug(string(orgConfig))
if err != nil {
return nil, fmt.Errorf("failed to extract organization slug: %w", err)
}

pipelines, err := client.ListPipelines(orgSlug)
if err != nil {
return nil, fmt.Errorf("error listing pipelines: %v", err)
}

resources := make([]core.IntegrationResource, len(pipelines))
for i, pipeline := range pipelines {
resources[i] = core.IntegrationResource{
Type: "pipeline",
ID: pipeline.Slug,
Name: pipeline.Name,
}
}
return resources, nil

default:
return nil, fmt.Errorf("unsupported resource type: %s", resourceType)
}
}

func (b *Buildkite) Actions() []core.Action {
return []core.Action{}
}

func (b *Buildkite) HandleAction(ctx core.IntegrationActionContext) error {
return nil
}

func (b *Buildkite) HandleRequest(ctx core.HTTPRequestContext) {
ctx.Response.WriteHeader(http.StatusNotFound)
}

func (b *Buildkite) Components() []core.Component {
return []core.Component{
&TriggerBuild{},
}
}

func (b *Buildkite) Triggers() []core.Trigger {
return []core.Trigger{
&OnBuildFinished{},
}
}
Loading