Drone CI’s Go plugin actually works by invoking go build and go test directly within the container, not by managing a separate Go toolchain itself.
Let’s see how this plays out with a simple Go project. Imagine this main.go:
package main
import "fmt"
func main() {
fmt.Println("Hello, Drone!")
}
And a main_test.go:
package main
import "testing"
func TestHello(t *testing.T) {
// This is a placeholder test, we'll make it fail later.
t.Errorf("This test is designed to fail")
}
Here’s a basic .drone.yml to build and test it:
kind: pipeline
type: docker
name: default
steps:
- name: build
image: golang:1.21
commands:
- go build
- go test
When Drone runs this, it pulls the golang:1.21 image. Inside that container, it executes go build and go test using the Go toolchain that’s already installed within the golang Docker image. The image field in your Drone configuration is where the Go environment comes from.
The real magic, if you can call it that, is how Drone orchestrates these commands. It doesn’t have a special "Go plugin" that does the building; it uses a container that has the Go tools. The commands section is just a list of shell commands executed sequentially within that container.
For a more realistic scenario, let’s say you have a project with dependencies.
go.mod:
module mygoapp
go 1.21
require github.com/gin-gonic/gin v1.9.1
main.go:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello from Gin!")
})
r.Run(":8080")
}
main_test.go:
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestHealthz(t *testing.T) {
// Build gin engine
r := gin.Default()
r.GET("/healthz", func(c *gin.Context) {
c.Status(http.StatusOK)
})
// Request to the above route
req, _ := http.NewRequest("GET", "/healthz", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// Assertions
if w.Code != http.StatusOK {
t.Errorf("Expected status code %d, but got %d", http.StatusOK, w.Code)
}
}
Your .drone.yml would look like this:
kind: pipeline
type: docker
name: default
steps:
- name: build_and_test
image: golang:1.21
commands:
- echo "Running go mod tidy..."
- go mod tidy
- echo "Running go build..."
- go build -v
- echo "Running go test..."
- go test -v -race
Here’s what’s happening:
image: golang:1.21: Drone pulls the official Go 1.21 Docker image. This image contains the Go toolchain, includinggo build,go test, andgo mod.go mod tidy: This command ensures yourgo.modfile is up-to-date with your dependencies. It fetches any missing modules and cleans up unused ones.go build -v: This compiles your Go program. The-vflag provides verbose output, showing which packages are being compiled. This step will fail if there are compilation errors.go test -v -race: This runs your tests.-vprovides verbose output, listing each test function being run.-raceenables the Go race detector, which is crucial for finding data races in concurrent code. This is a very common and important step for Go projects.
Drone’s execution model here is straightforward: it spins up a Docker container based on the specified image, mounts your repository code into it, and then executes the commands sequentially. If any command exits with a non-zero status code, the pipeline step (and potentially the entire pipeline) fails, and Drone reports the error.
The most surprising thing about Drone’s Go integration is that it doesn’t need a dedicated "Go plugin" in the sense of a separate, complex piece of software managed by Drone itself. The Go ecosystem’s strength is its self-contained toolchain, and Drone leverages this by simply providing a container with that toolchain ready to go. You could use any Docker image that has go installed, like ubuntu with go manually installed, or a custom image you’ve built. The official golang images are just convenient.
Think about how you’d normally build and test a Go project on your local machine. You’d open your terminal, navigate to your project directory, and run go mod tidy, go build, and go test. Drone is doing precisely that, but within an isolated container environment. The steps are just sequential commands in a shell.
The real power comes when you start adding more complex steps, like linting, code coverage generation, or even building Docker images of your application using the compiled binary. For instance, to generate a coverage report:
kind: pipeline
type: docker
name: default
steps:
- name: build_test_coverage
image: golang:1.21
commands:
- go mod tidy
- go test -v -race -coverprofile=coverage.txt
- go tool cover -html=coverage.txt -o coverage.html
artifacts:
paths:
- coverage.html
In this expanded example, we add a step to generate an HTML coverage report using go tool cover. The artifacts section then tells Drone to upload coverage.html as a build artifact, making it downloadable after the pipeline run. The golang image has go tool available, so this works out of the box.
The one thing most people don’t realize is that the image you specify can be anything that has the Go compiler installed. You don’t have to use the official golang image. If you have a custom base image with specific dependencies or a particular Go version installed, you can use that. This flexibility is key; Drone itself doesn’t need to know about Go; it just needs to run commands in a container that does.
The next hurdle you’ll likely encounter is managing multiple Go versions or dealing with cross-compilation within your pipeline.