Skip to main content
The three binaries in this repository have had a service.yaml beside them since the last step, inert, waiting for something to read them:
cmd/server/service.yaml (abridged)
That is a convention — “a package with a service.yaml is a service” — and conventions are what nobody wants to hand-write BUILD files for. Only name and port are read by anything; the rest of the file, including a resources: block this snippet leaves out, is there to look like a real deployment descriptor. Gazelle already generates go_library and go_binary by reading Go sources. This step teaches it one more rule.

Run it

The generated targets are already committed at this step, so throw them away first and ask for them back:
Look at what came back in cmd/server/BUILD.bazel:
Four targets, from two keys. port: 8080 became exposed_ports; name: server became the image label and the push repository.

How it works

Open tools/gazelle/service_image.axl. An extension has two halves. prepare says what to read.
Matchers and yq expressions. Packages without a service.yaml cost nothing. The engine running this is called Orion — that is the name in the last line of the file, and it is why the repository can use a prebuilt Gazelle with no Go toolchain. declare says what targets belong in one package. Two of the four it adds are plumbing — go_cross_binary and tar exist to get a Linux binary into a layer, and neither comes from the YAML. The two your service.yaml actually shapes are image and push.
It is handed the answers for a package and adds targets. If there is no service.yaml, it calls ctx.targets.remove(...) instead — which is how a generated target disappears when its YAML does. After declare there is one gazelle_rule_kind registration per rule this extension writes. Each names three things:
  • From — where the rule comes from, a .bzl label or a bare module name like @tar.bzl. This is what writes the load statements you saw at the top of the generated BUILD file.
  • MergeableAttrs — attributes the generator owns, re-asserted on every run.
  • NonEmptyAttrs — the attribute whose absence means the rule is dead. It is what lets ctx.targets.remove(...) actually delete a target rather than leave an empty one behind.

How Gazelle finds it

A task under .aspect/ needs no registration. A Gazelle extension does — it is named by the Gazelle binary that hosts it, which step-6 also added:
tools/gazelle/BUILD.bazel
That is the binary aspect gazelle drives, and it is the prebuilt one because only that binary hosts Orion. The comments in the file cover repo_config, which is required whenever go is in languages.

It is idempotent

Nothing to do, because the generated targets already match what the YAML says. Now change the YAML and run it again:
exposed_ports follows. That is MergeableAttrs doing its job: the generator owns that attribute, so a re-run re-asserts it.

Then ship it

The generated oci_push targets are real. aspect delivery runs them:
Crane logs every layer as it uploads, so expect a wall of pushed blob: lines before the table. Three images, live on ttl.sh — an anonymous registry that needs no account and expires an image an hour after it is pushed.
ttl.sh has one global namespace, so everyone in a workshop pushes to the same three repositories. The builds are reproducible, so everyone pushes identical bytes and the tag expires within the hour — but if you would rather have your own, change _REGISTRY in the extension to ttl.sh/<something-unique> and re-run aspect gazelle.
Nothing here was hand-written: a service.yaml became BUILD targets, and the BUILD targets became images. config.axl is what told delivery where to look:
Every oci_push in this repository was generated from a service.yaml, so that query means “all of them”. Next: hand all of this to an agent.