Skip to content

Commit 055f474

Browse files
committed
feat: use toolhive v0.2.4 with Dockerfile generation and add SBOM, provenance, and multi-arch support
- Updated toolhive to v0.2.4 to use new --dry-run functionality - Modified main.go to always generate Dockerfiles instead of building - Updated GitHub workflow to use Docker Buildx for building - Added SBOM generation for supply chain transparency - Added provenance attestation for build verification - Added multi-architecture support (linux/amd64, linux/arm64) - Improved build caching with GitHub Actions cache
1 parent 5c214bc commit 055f474

4 files changed

Lines changed: 94 additions & 33 deletions

File tree

.github/workflows/build-containers.yml

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ jobs:
105105
permissions:
106106
contents: read
107107
packages: write
108+
id-token: write # Needed for provenance attestation
108109

109110
steps:
110111
- name: Checkout repository
@@ -115,6 +116,9 @@ jobs:
115116
with:
116117
go-version: '1.24'
117118

119+
- name: Set up QEMU
120+
uses: docker/setup-qemu-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
121+
118122
- name: Set up Docker Buildx
119123
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
120124

@@ -139,37 +143,77 @@ jobs:
139143
# Extract server name from filename (without .yaml extension)
140144
server_name=$(basename "$config_file" .yaml)
141145
echo "server_name=$server_name" >> $GITHUB_OUTPUT
146+
147+
# Generate image name
148+
image_name="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${protocol}/${server_name}"
149+
echo "image_name=$image_name" >> $GITHUB_OUTPUT
142150
143-
- name: Build and capture container info
144-
id: build
151+
- name: Generate Dockerfile
152+
id: dockerfile
145153
run: |
146-
echo "Building container for ${{ steps.meta.outputs.config_file }}"
154+
echo "Generating Dockerfile for ${{ steps.meta.outputs.config_file }}"
147155
148-
# Capture the output which includes the built image name
149-
output=$(go run main.go -config "${{ steps.meta.outputs.config_file }}" 2>&1)
150-
echo "$output"
156+
# Create a temporary directory for the Dockerfile
157+
dockerfile_dir=$(mktemp -d)
158+
dockerfile_path="${dockerfile_dir}/Dockerfile"
151159
152-
# Extract the image name from the last line of output
153-
image_name=$(echo "$output" | grep "Successfully built container image:" | sed 's/Successfully built container image: //')
154-
echo "image_name=$image_name" >> $GITHUB_OUTPUT
155-
echo "Captured built image: $image_name"
160+
# Generate the Dockerfile using our tool
161+
go run main.go -config "${{ steps.meta.outputs.config_file }}" -output "${dockerfile_path}"
162+
163+
echo "dockerfile_dir=$dockerfile_dir" >> $GITHUB_OUTPUT
164+
echo "dockerfile_path=$dockerfile_path" >> $GITHUB_OUTPUT
165+
166+
# Display the generated Dockerfile for debugging
167+
echo "Generated Dockerfile:"
168+
cat "${dockerfile_path}"
156169
157-
- name: Push container image
158-
if: github.event_name != 'pull_request' && steps.build.outputs.image_name != ''
159-
run: |
160-
image_name="${{ steps.build.outputs.image_name }}"
161-
echo "Pushing image: $image_name"
162-
docker push "$image_name"
170+
- name: Extract metadata for Docker
171+
id: docker-meta
172+
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5
173+
with:
174+
images: ${{ steps.meta.outputs.image_name }}
175+
tags: |
176+
type=ref,event=branch
177+
type=ref,event=pr
178+
type=semver,pattern={{version}}
179+
type=semver,pattern={{major}}.{{minor}}
180+
type=sha,prefix={{branch}}-
181+
type=raw,value=latest,enable={{is_default_branch}}
182+
183+
- name: Build and push Docker image
184+
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6
185+
with:
186+
context: ${{ steps.dockerfile.outputs.dockerfile_dir }}
187+
file: ${{ steps.dockerfile.outputs.dockerfile_path }}
188+
platforms: linux/amd64,linux/arm64
189+
push: ${{ github.event_name != 'pull_request' }}
190+
tags: ${{ steps.docker-meta.outputs.tags }}
191+
labels: ${{ steps.docker-meta.outputs.labels }}
192+
cache-from: type=gha
193+
cache-to: type=gha,mode=max
194+
sbom: true
195+
provenance: true
196+
annotations: |
197+
org.opencontainers.image.title=${{ steps.meta.outputs.server_name }}
198+
org.opencontainers.image.description=MCP server for ${{ steps.meta.outputs.server_name }}
199+
org.opencontainers.image.vendor=Stacklok
200+
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
201+
org.opencontainers.image.revision=${{ github.sha }}
163202
164203
- name: Generate image summary
165204
run: |
166205
echo "## Container Build Summary" >> $GITHUB_STEP_SUMMARY
167206
echo "- **Config**: ${{ steps.meta.outputs.config_file }}" >> $GITHUB_STEP_SUMMARY
168207
echo "- **Protocol**: ${{ steps.meta.outputs.protocol }}" >> $GITHUB_STEP_SUMMARY
169208
echo "- **Server**: ${{ steps.meta.outputs.server_name }}" >> $GITHUB_STEP_SUMMARY
170-
echo "- **Image**: ${{ steps.build.outputs.image_name }}" >> $GITHUB_STEP_SUMMARY
209+
echo "- **Image**: ${{ steps.meta.outputs.image_name }}" >> $GITHUB_STEP_SUMMARY
210+
echo "- **Platforms**: linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY
211+
echo "- **SBOM**: ✅ Included" >> $GITHUB_STEP_SUMMARY
212+
echo "- **Provenance**: ✅ Attested" >> $GITHUB_STEP_SUMMARY
171213
if [ "${{ github.event_name }}" != "pull_request" ]; then
172214
echo "- **Status**: ✅ Built and pushed" >> $GITHUB_STEP_SUMMARY
215+
echo "- **Tags**:" >> $GITHUB_STEP_SUMMARY
216+
echo "${{ steps.docker-meta.outputs.tags }}" | sed 's/^/ - /' >> $GITHUB_STEP_SUMMARY
173217
else
174218
echo "- **Status**: ✅ Built (not pushed - PR)" >> $GITHUB_STEP_SUMMARY
175219
fi
@@ -189,6 +233,11 @@ jobs:
189233
190234
if [ "${{ needs.build-containers.result }}" == "success" ]; then
191235
echo "- **Build Status**: ✅ All changed containers built successfully" >> $GITHUB_STEP_SUMMARY
236+
echo "- **Features**:" >> $GITHUB_STEP_SUMMARY
237+
echo " - 🏗️ Multi-architecture support (amd64, arm64)" >> $GITHUB_STEP_SUMMARY
238+
echo " - 📦 SBOM (Software Bill of Materials) included" >> $GITHUB_STEP_SUMMARY
239+
echo " - 🔐 Provenance attestation for supply chain security" >> $GITHUB_STEP_SUMMARY
240+
echo " - 🚀 GitHub Actions cache for faster builds" >> $GITHUB_STEP_SUMMARY
192241
elif [ "${{ needs.build-containers.result }}" == "failure" ]; then
193242
echo "- **Build Status**: ❌ Some containers failed to build" >> $GITHUB_STEP_SUMMARY
194243
elif [ "${{ needs.build-containers.result }}" == "skipped" ]; then

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.24.1
55
toolchain go1.24.6
66

77
require (
8-
github.com/stacklok/toolhive v0.2.3
8+
github.com/stacklok/toolhive v0.2.4
99
gopkg.in/yaml.v3 v3.0.1
1010
)
1111

@@ -18,7 +18,7 @@ require (
1818
github.com/bahlo/generic-list-go v0.2.0 // indirect
1919
github.com/beorn7/perks v1.0.1 // indirect
2020
github.com/buger/jsonparser v1.1.1 // indirect
21-
github.com/cedar-policy/cedar-go v1.2.5 // indirect
21+
github.com/cedar-policy/cedar-go v1.2.6 // indirect
2222
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
2323
github.com/cespare/xxhash/v2 v2.3.0 // indirect
2424
github.com/containerd/errdefs v1.0.0 // indirect

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
1616
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
1717
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
1818
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
19-
github.com/cedar-policy/cedar-go v1.2.5 h1:JDyW1DrXR2f27bRIaS1mPWYULnZsCzi/Jjh397vxvoU=
20-
github.com/cedar-policy/cedar-go v1.2.5/go.mod h1:h5+3CVW1oI5LXVskJG+my9TFCYI5yjh/+Ul3EJie6MI=
19+
github.com/cedar-policy/cedar-go v1.2.6 h1:q6f1sRxhoBG7lnK/fH6oBG33ruf2yIpcfcPXNExANa0=
20+
github.com/cedar-policy/cedar-go v1.2.6/go.mod h1:h5+3CVW1oI5LXVskJG+my9TFCYI5yjh/+Ul3EJie6MI=
2121
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
2222
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
2323
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -217,8 +217,8 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
217217
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
218218
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
219219
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
220-
github.com/stacklok/toolhive v0.2.3 h1:Nt0i1130kw02RXN2wlgz86eDNZXjDkKVctFmM+V9CbY=
221-
github.com/stacklok/toolhive v0.2.3/go.mod h1:WXyTC0mvg39DAbTdvT5FSsflq7MydcEZwpphxJD7e/A=
220+
github.com/stacklok/toolhive v0.2.4 h1:09q5WpXBkUo5HqBwul2GbI9A34bw0GC0MwxtOktGtC0=
221+
github.com/stacklok/toolhive v0.2.4/go.mod h1:eKmvhxewyIJ/BLR3ZbNnTsosqAVmSAY7l+PVcrJlnbM=
222222
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
223223
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
224224
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=

main.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func main() {
5959
var (
6060
configFile = flag.String("config", "", "Path to the YAML configuration file")
6161
outputTag = flag.String("tag", "", "Custom container image tag (optional)")
62+
output = flag.String("output", "", "Output file for Dockerfile (optional, defaults to stdout)")
6263
)
6364
flag.Parse()
6465

@@ -72,14 +73,24 @@ func main() {
7273
log.Fatalf("Failed to load configuration: %v", err)
7374
}
7475

75-
// Build the container image
76+
// Generate Dockerfile
7677
ctx := context.Background()
77-
imageName, err := buildMCPServerContainer(ctx, spec, *outputTag)
78+
dockerfile, err := generateDockerfile(ctx, spec, *outputTag)
7879
if err != nil {
79-
log.Fatalf("Failed to build container: %v", err)
80+
log.Fatalf("Failed to generate Dockerfile: %v", err)
8081
}
8182

82-
fmt.Printf("Successfully built container image: %s\n", imageName)
83+
// Output Dockerfile
84+
if *output != "" {
85+
// Write to file
86+
if err := os.WriteFile(*output, []byte(dockerfile), 0644); err != nil {
87+
log.Fatalf("Failed to write Dockerfile to %s: %v", *output, err)
88+
}
89+
fmt.Printf("Dockerfile written to: %s\n", *output)
90+
} else {
91+
// Output to stdout
92+
fmt.Print(dockerfile)
93+
}
8394
}
8495

8596
// validateConfigPath ensures the config path is safe and within expected directories
@@ -148,8 +159,8 @@ func loadMCPServerSpec(configPath string) (*MCPServerSpec, error) {
148159
return &spec, nil
149160
}
150161

151-
// buildMCPServerContainer builds a container image using toolhive's library
152-
func buildMCPServerContainer(ctx context.Context, spec *MCPServerSpec, customTag string) (string, error) {
162+
// generateDockerfile generates a Dockerfile using toolhive's library
163+
func generateDockerfile(ctx context.Context, spec *MCPServerSpec, customTag string) (string, error) {
153164
// Create the protocol scheme string
154165
packageRef := spec.Spec.Package
155166
if spec.Spec.Version != "" {
@@ -166,19 +177,20 @@ func buildMCPServerContainer(ctx context.Context, spec *MCPServerSpec, customTag
166177
// Create image manager
167178
imageManager := images.NewImageManager(ctx)
168179

169-
// Build the image using toolhive's BuildFromProtocolSchemeWithName function
170-
imageName, err := runner.BuildFromProtocolSchemeWithName(
180+
// Generate Dockerfile using toolhive's BuildFromProtocolSchemeWithName function with dryRun=true
181+
dockerfile, err := runner.BuildFromProtocolSchemeWithName(
171182
ctx,
172183
imageManager,
173184
protocolScheme,
174185
"", // caCertPath - empty for now
175186
imageTag,
187+
true, // always dryRun to generate Dockerfile
176188
)
177189
if err != nil {
178-
return "", fmt.Errorf("failed to build from protocol scheme %s: %w", protocolScheme, err)
190+
return "", fmt.Errorf("failed to generate Dockerfile for protocol scheme %s: %w", protocolScheme, err)
179191
}
180192

181-
return imageName, nil
193+
return dockerfile, nil
182194
}
183195

184196
// generateImageTag creates a container image tag based on the repository structure

0 commit comments

Comments
 (0)