Home About Me

A Custom Drone UI Starts in the Go Binary

Drone is an open-source CI/CD system that leans on containers for a powerful plugin model. It splits responsibilities between a server, which accepts requests and dispatches work, and agents, which run the jobs. That architecture is straightforward to operate, but it has an unusual consequence for the web interface: if you want a custom UI, you usually have to rebuild the server binary too.

The server is written in Go, while the frontend lives in the drone/drone-ui repository and is built with Vue.js; older versions used React. In a conventional separated frontend/backend deployment, a web server such as NGINX would sit in front, serve the static pages, and proxy API calls to the backend. Drone does something different. Its Go server serves both the API and the UI. To keep the Go server a single distributable file, the project uses bradrydzewski/togo to compile static assets into Go source. The generated output is essentially a hash table mapping routes to file contents. That generated Go file is committed to the repository and loaded by the server as a module, after which the server can look up a route and return the corresponding asset.

This is convenient for people who install Drone: the UI is inside the binary. It is less convenient for anyone changing the UI, because the static assets are not picked up from disk at runtime. A custom interface therefore means rebuilding the frontend assets, regenerating the embedded Go file, and then rebuilding the server executable.

Building the frontend module

Start by forking drone/drone-ui and making the desired changes in the fork. The repository’s README covers local development. If the changes are small, a practical approach is to keep the fork close to upstream and merge official releases through pull requests rather than letting the divergence grow. Running npm run build produces the final static assets under dist/files.

Next, the assets need to be embedded into Go code with bradrydzewski/togo. Go must be installed, and its global bin directory must be on PATH; otherwise the build will fail because the togo command cannot be found.

go get github.com/bradrydzewski/togo
cd dist
go generate dist.go

go generate is a way to run a command from a comment. In this case it executes the instruction in dist.go: togo http -package dist -output dist_gen.go.

Commit the generated dist_gen.go file to the fork. That completes the frontend module. The remaining work is to rebuild the server executable against this fork.

Rebuilding the server executable

The server source is in drone/drone. Find the files that import github.com/drone/drone-ui and point them at the fork, for example xxx.com/xxx/drone-ui. In practice, three files need changing: ./handler/web/{logout,pages,web}.go.

go get -v -insecure xxx.com/xxx/drone-ui
sed -i '' 's/github.com\/drone\/drone-ui/xxx.com\/xxx\/drone-ui/' ./handler/web/{logout,pages,web}.go

Go modules support a replace directive for swapping one module for another, but in this case the replacement did not work, so the sed route was used instead:

go mod edit -replace=github.com/drone/drone-ui=xxx.com/xxx/drone-ui

After the import paths are updated, go build can produce the server. No source changes are needed in the server repository itself; the only change is to its frontend dependency. That suggests a repeatable process: whenever the UI fork changes, run a CI pipeline that clones the server repository, applies the dependency substitution, builds a container image, and pushes it to a registry.

Using Drone to build Drone

The CI system for this job can be Drone itself, which is a fitting arrangement: Drone building Drone. By default, Drone clones the repository that contains the pipeline. That is not what is needed here, because the UI fork is only a dependency. The pipeline needs the main drone/drone repository instead.

clone:
  disable: true

steps:
- name: clone
  image: alpine/git
  commands:
  - git clone https://github.com/drone/drone.git .
  - git checkout ${DRONE_TAG}

trigger:
  event:
  -tag

With the default clone disabled, a dedicated step clones drone/drone into the working directory and checks out the tag that triggered the build. Matching the server source to the same tag keeps the resulting image aligned with the upstream version.

- name: build
  image: golang:1.14.4
  commands:
  - go get -v -insecure xxx.com/xxx/drone-ui
  - sed -i '' 's/github.com\\/drone\\/drone-ui/xxx.com\\/xxx\\/drone-ui/' ./handler/web/{logout,pages,web}.go
  - sh scripts/build.sh
  environment:
    GOARCH: amd64
    GOOS: linux

Apart from the substitution of the frontend module, these build commands come from the upstream server repository. Upstream also builds ARM and ARM64 variants; if those architectures are not needed, leaving them out keeps the pipeline shorter. The final steps mirror the official build: create a Docker image, push it to a registry, and use that image when running Drone.

A note on the landing page

The hosted Drone service at https://cloud.drone.io shows a login page to visitors who are not signed in. The server decides this in pages.go: when the incoming domain is cloud.drone.io, it serves the static page at handler/web/landingpage/index.html. If a custom deployment needs a portal or a branded entry page, that is another place to adjust the embedded UI files before rebuilding.