Skip to content
daggerPublic

About

A Dagger module for Go - a programming language for building simple, secure, scalable systems.

Resources

Code of conduct

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

This Go module requires Dagger v1.0.0-beta.22 or later for selected collection arguments on checks. Each test receives the filtered package collection and passes it directly to the shared module runner. No self API call or Dang import aliases are needed.

go

Dagger modules for Go tooling, written in the .dang module language.

Go has one toolchain from one supplier, so go is one module and it lives at the root of this repository, where it always has. The linters do not come from that supplier and do not share its release dates, so they are their own modules — over one shared library, in this repository.

github.com/dagger/go
├── go.dang           the main module, at the root: test, build, generate
├── gomod/            the shared library. No checks.
├── golangci-lint/    lint
├── staticcheck/      lint
└── .dagger/          development checks for the root module

Install

go is the root module, so its address is the repository:

dagger install github.com/dagger/go              # test, build, generate
dagger install github.com/dagger/go/golangci-lint
dagger install github.com/dagger/go/staticcheck

gomod is a library, not a tool. You depend on it when you write a module; you do not install it to get checks.

Scoping

go discovers every Go module in the workspace, so a repository with test fixtures or a vendored copy of some source will have those checked too. The linters discover every project — every directory holding their configuration file — which a fixture can have too. Say which roots you mean in dagger.toml:

[modules.golangci-lint.settings]
lint = ["**", "!docs"]

A bare pattern selects, a "!"-prefixed pattern excludes, and an exclude wins whatever the order. docs means docs and every module below it; ** and * mean all of them; ["!**"] means none. Other glob shapes are not interpreted. go spells its two as test and generate rather than lint. Its build setting uses the same rules, but selects main packages, by their directory within each module or by their workspace path.

The modules

go

Function Description
modules Filtered Go module collection; use list to read its items.
module The module containing a workspace path.

On a package: tests, binary, platforms and generate.

On a module: packages, test, generate, skip-test, skip-generate, has-generate-directives, generate-directories, base, include, include-base, include-discovered, source, test-data.

Settings

Setting Meaning
version Go version for every module, instead of each one's go.mod.
base Base image for Go containers.
includeExtraFiles Extra workspace files to mount, as include patterns.
test Module roots to test (see Scoping).
generate Directories to run go generate in (see below).
build Main packages to build, by module-relative or workspace path.
buildFlags Extra flags for go build only.
goflags GOFLAGS in every Go container.
mountPath Where the workspace is mounted in Go containers.

base must carry a Go toolchain and any C/C++ dependencies the modules need. otelgotest is installed into it unless it already has one. It supplies its own toolchain, so version is ignored alongside it and recorded in warnings.

goflags is passed as-is, e.g. -tags=extended,withdeploy. Flags that take a value must use the -flag=value form, as Go requires inside GOFLAGS.

mountPath defaults to /src/<workspace name>, where the name is the last segment of the workspace address — usually the repository name — so tests that expect it in their working directory keep passing. It must be absolute. A workspace made from a directory or git ref with asWorkspace has no name, so it is mounted at /src.

Packages

modules is a collection keyed by module root (go-module). Each module's packages collection is keyed by package directory relative to the workspace root, e.g. sdk/go/cmd/dagger, which adds a go-package dimension. A directory is a package key when it has something to do: tests to run, a main package to build, or go:generate commands to run. Each package's tests are a collection too, keyed by function name, which adds go-test.

$ dagger list go-packages -a
$ dagger check --go --test --go-package=sdk/go
$ dagger check --go --test --go-test=TestConnect
$ dagger check --go --test --go-package=sdk/go --go-test=TestConnect
$ dagger generate --go --go-package=sdk/go/internal/gen
$ dagger check -l --all --go -f=cli     # one line per test, as flags to reuse

--test alone also selects every other module's check named test; --go narrows it to this one. The linters key their collections by project, as --golangci-lint-project and --staticcheck-project. dagger check --help lists the flags in effect.

Packages are found in the Go modules at or below the workspace cwd, plus the enclosing module when the cwd is inside one, so dagger -W ./sdk/go check scopes every tool to that module without a flag. Use --go-module=sdk/go to select a module explicitly. modules retains the include/exclude and skip options. Read the selected modules as an array with modules(...).list. Pass findUp: false to module when the path is already a module root. An explicit module's packages lookup is independent of the workspace cwd. Packages are accessed through their module, for example: go.modules(ws).get(key: "sdk/go").packages(ws). There is no root go.packages API. To operate across modules, select each module's packages explicitly.

Tests

Each selected test has its own check. All checks in a Go module call the same helper, which runs one otelgotest -json command and returns a shared report. The engine deduplicates identical container operations. Separate modules use separate commands and their own toolchains.

The shared run streams its JSON report through an indexer once. Each check reads one bounded result file, so total report size is independent of File.contents limits. Failure messages retain the last 32 KiB of each diagnostic stream and mark truncated output; the full report remains in the shared execution container.

The helper reads the filtered package and test collections. A partial selection uses explicit package arguments and an anchored -run pattern. Packages without a selected test are omitted. With no keys removed, the command runs ./... without -run. A test name in two packages selects both; --go-package narrows that selection.

A failed test fails its own check. A package that fails to build fails its selected tests. Failed TestMain setup or teardown and failures from undiscovered tests also fail that package's selected checks. Failures in packages without selected test checks fail all checks sharing that module's command. Test failure messages include subtest output. Otherwise, passing tests in other packages still pass. Logs and test spans belong to the check that starts the shared command; other checks share its result.

GoTests.test remains a manual package helper, without a check marker. It does not replace individual checks. There is no .batch namespace.

Test names come from searching the package's _test.go files for func TestXxx(t *testing.T), func FuzzXxx(f *testing.F) and func ExampleXxx(), so listing them runs no tests: only the shared source scan and, with a custom base, one go env in it. A file counts when Go's build rules accept it for the engine's platform with the build tags of goflags (and -race's), the Go version and no cgo, the settings the default containers run tests with. With a custom base, they are the toolchain, cgo and GOFLAGS its own go env reports, read once; a base that lets Go switch to a newer toolchain for a module can still run tests constrained to that newer release unlisted. An example counts only with an output comment, as go test runs no other. TestMain, testdata, vendored packages and nested modules are left out. A selected test that go test does not run after all fails, saying so, rather than passing. A module outside the test selection offers no tests.

test on a module is a plain function rather than a check, so that dagger check does not run the same tests twice.

Binaries

binary on a package returns its binary for the engine's platform. Only a main package selected by the build setting has one; any other package fails, naming itself. The GoPackages.binary helper compiles the selected main packages in that module, skips the rest, and returns one directory of binaries. Combine module outputs explicitly when building across modules.

A binary is named as go build names it: after the last element of the package's import path, or the one before when that is a major version such as v2. A module's root package takes its name from the module path in go.mod. Names can collide, as cmd/foo and tools/foo both build foo: the binary helper refuses such a pair and names both packages, while a single package's binary does not mind.

A directory is a main package when one of its own non-test .go files says package main, so listing them runs no container. Files with an ignore build tag, usually go generate scripts, are left out, as are testdata, nested modules and names starting with . or _. Other build constraints are not evaluated: a package whose files are all for another platform is listed, and building it fails.

The build setting selects main packages with the Scoping rules. A pattern matches a package's directory within its module, in every module, or its workspace path: ["cmd/**", "!cmd/internal"] keeps cmd and its subdirectories except cmd/internal in each module, and ["!vendor"] drops every main package in the modules under vendor.

buildFlags are passed to go build as separate arguments, after goflags has applied through GOFLAGS, so a value may contain spaces: ["-trimpath", "-ldflags=-s -w -X main.version=dev"]. Tests and go generate never see them. Every package in every module gets the same flags.

Platforms

A package's platforms are keyed by GOOS/GOARCH, and every pair the pinned Go toolchain supports is a key (gomod's go-platforms). Each platform's binary is the package's binary cross-compiled for it, in the native container with GOOS and GOARCH set; nothing is emulated. Windows binaries end in .exe.

The GoPlatforms.binary helper returns one subdirectory per platform, e.g. linux-amd64/, windows-amd64/:

  • With no platform selected, it builds for the engine's platform only, e.g. linux/arm64.
  • With platforms selected, it builds for exactly those.

The helper tells the two apart by its delta: an unnarrowed collection means no platform was asked for. So selecting every platform explicitly looks the same as selecting none, and builds only the engine's. A single platform's binary has no such default; it builds for its own key.

binary returns artifacts and is not a check. A main package that stops compiling is caught by golangci-lint and staticcheck, which type-check every package.

Tests and go generate always run natively. cgo is off when cross-compiling unless base brings a C cross-toolchain; some ports, such as ios/*, need one. A module whose toolchain predates a port fails with Go's own error.

Generate

generate patterns are relative to the workspace root, whatever the cwd, and use Dagger glob syntax: *, ? and character classes match within a path segment, and ** matches zero or more segments.

  • A literal path selects only that directory, even if it is a module root. . selects the workspace root only.
  • internal/** selects internal and everything below it.
  • A ! prefix excludes, and exclusions always win.
  • An empty list, or one with only exclusions, starts from every directory.
  • The default ["**"] reaches directories in nested modules.

For example: ["sdk/go/engineconn", "internal/**", "!internal/fixtures/**"].

go generate . runs in each selected directory that has a generator command; go:generate:include alone is not one. Each such directory is a package with a generate generator and its stale check.

A package's generate runs against the committed source. Collection-level generate is now an ordinary helper; it does not replace item generators. Calling that helper explicitly runs its selected packages in lexical order, each seeing the preceding output. Modules run independently. Use an explicit coordinating call when generators require an order.

A directory can pick the container its generators run in with //go:generate:container, resolved with the caller's Workspace.resolve — a workspace container by name, such as generate-env, or an image reference such as docker.io/library/golang:1.26.1-alpine. Conflicting values in one directory are errors. Consecutive directories naming the same one share a container; when it changes, workspace files carry over so later commands still see earlier output. Without the directive, the directory uses the module's own base container.

A package's own generate fails when the scan could not read its module, naming the file, while the manual generate helper skips that module (see below).

Tests run with a nested Dagger engine, so a suite that drives Dagger works.

golangci-lint

Function Description
projects Projects discovered from the workspace, as a collection.
project The project containing a workspace path.
module The Go module containing a workspace path.
version The bundled golangci-lint version.

A project is a directory holding .golangci.yml, .yaml, .toml or .json, and it lints everything below that no nested configuration claims: each Go module below it, and the part below it of a module that encloses it. A directory with no configuration above it is not linted, and a configuration with no Go code around it is not a project.

On a project: modules and lint (a @check). A project runs golangci-lint once per Go module it covers, with that module's toolchain, and golangci-lint finds the project's configuration by its own lookup. Projects the lint setting skips are left out of projects unless includeSkipLint (for staticcheck, includeSkipped) is true; project still reaches one, and its lint does nothing. For both linters, artifact discovery runs each project's lint check. projects(...).lint(ws) is an ordinary helper for explicitly linting the selected projects; it does not add another artifact check. For compatibility with older collection engines, call each selected item with projects(...).get(key: path).lint(ws) and evaluate the returned check. Older engines expose the explicit collection helper as .batch.lint(ws) instead of .lint(ws); that helper now returns Void, so it must not be wrapped as a Check. Both linter development suites use the per-project API, shared by both engine versions. Both linters name their check lint, so dagger check --lint runs them together and --golangci-lint --lint runs this one; select projects with --golangci-lint-project=PATH.

version is the linter release, pinned to 2.15.0 by digest, and goVersion is the Go toolchain — chosen independently, because the binary is copied out of its image onto that toolchain. A C/C++ toolchain is present, since cgo dependencies need one during typecheck.

Both linters take base, goVersion, includeExtraFiles and lint settings that work as go's do, except that lint selects project roots. A base must carry a Go toolchain and any C/C++ dependencies; the linter is installed into it unless it already has one, and goVersion is ignored alongside it and recorded in warnings. Each linter's configuration files are mounted with their directory structure, so its normal config lookup applies. Use includeExtraFiles for files the linter reads that the Go patterns miss, such as generated inputs or non-Go embedded assets.

staticcheck

Function Description
projects Projects discovered from the workspace, as a collection.
project The project containing a workspace path.
module The Go module containing a workspace path.

Projects work as golangci-lint's do, rooted at each staticcheck.conf. Staticcheck merges a package's staticcheck.conf files itself, so a nested project still inherits from its parent's. Select it with --staticcheck --lint, and projects with --staticcheck-project=PATH.

version is the Staticcheck release, built once with go install in a pinned Go container, or in the module's own when its Go is newer, and goVersion is the Go toolchain. Test files are analyzed; tests are not executed.

gomod

The shared library. It has no checks of its own and never will — two modules with a check for the same tool would run that tool twice.

Function Description
modules Modules discovered from the workspace's go.mod files.
module The module containing a workspace path.
at A module root taken as given, with no workspace consulted.
setting-patterns Selection patterns, repaired for older beta engines.
go-container A Go toolchain container at a version, with the shared caches.
tool-builder The pinned container tool binaries are built in.
go-platforms Every GOOS/GOARCH the pinned toolchain can target.
host-platform The engine's platform as GOOS/GOARCH.
with-tool Install a tool binary, unless the container has one.
with-warnings Announce ignored settings as container steps.
default-go-version The version used for a module that declares no go directive.
scanned-module-roots The directories the scan accepts as Go modules.
scan The raw workspace scan, as a directory.
with-go-caches A container with the shared Go download and build caches.

On a module:

Function Description
selected Whether selection patterns choose this module root.
include-discovered Patterns found by scanning directives and local replaces.
include / source What this module mounts, as patterns and as a directory.
test-directories Directories holding Go test files.
generate-directories Directories holding a go:generate command.
scan-error Why this module could not be scanned, or null.
include-base The built-in Go source patterns, module-scoped.
go-version The Go version this module's go directive asks for.
subpath / test-data Path and testdata mechanics.

Choices worth knowing

The tool types stay duplicated; the mechanics do not. Each module returns its own GoModule, because that type is what its callers see and each tool has its own fields and verbs to hang on it. Dang also cannot yet name a dependency's non-root type. So each GoModule holds a path and forwards discovery, selection and source policy to gomod.

One source scan for the whole workspace. gomod builds the helpers/go-includes scanner in a pinned Go image that depends on nothing about the caller or the module root, so three tools with three different base images still produce one identical container. The engine builds it once, runs it once, and each module reads its own slice of the output. Before, each of the three modules carried its own copy of that program and ran its own scan.

The Go toolchain comes from the module, not from the tool. With nothing set, each module is checked with the toolchain its own go.mod asks for. Each module runs in its own container anyway, so there is no need to find one version that suits the whole workspace.

go 1.26.1 in a module golang:1.26-alpine — the minor series, always its newest patch
goVersion: "1.26.1" golang:1.26.1-alpine — set explicitly, used as written
no go directive gomod's default-go-version

The series rather than the exact patch, because a go directive is a minimum and a go.mod bumped to a patch with no image yet would otherwise stop the module being checked at all. The root reports null for version/goVersion, since the answer varies per module; ask a module what it resolved to.

A tool is never built in an older module's container. golangci-lint's binary is copied out of its pinned image; Staticcheck and otelgotest are built once in a pinned Go container. A module on an older Go would otherwise fail to build the tool, and the error would be about the tool rather than the module. Staticcheck type-checks the standard library of the toolchain it runs under, and cannot read one newer than the Go it was built with, so a module on a Go newer than the pinned one gets Staticcheck built with its own.

A tool is installed only when the container does not already have it, so a base carrying its own build — an instrumented one, say — keeps it. version therefore says what to install if needed and composes with base.

base brings its own toolchain, so version/goVersion is ignored alongside it rather than refused — no setting needs editing to say what the base already says. Each ignored setting is listed in warnings and announced as a step in the container, since Dang has no diagnostics channel of its own.

A directory with a go.mod is not automatically a module. Discovery reports a module root only when its go.mod carries a module line, and when the module holds Go files that Go itself would look at — not only ones under testdata/ or a ./_ directory. Everything else is left out, because handing it to a tool yields a complaint about the tool rather than about the directory: golangci-lint errors on a module with no packages, and no Go command accepts a go.mod with no module line. module and at still reach one, so a caller that spells out a path gets that path, and an unparseable go.mod still says what is wrong with it. gomod's scanned-module-roots is the scanner's view of the same list, and its suite asserts the two agree.

Listing never runs the scanner. Which roots are modules, and which tests a module has, are answered from the workspace directly: findRoots, a read of each go.mod, globs and Workspace.search. The scanner builds and runs only when something needs a module's sources, so dagger check -l and dagger list pay for neither.

The line is "no Go files", not "no packages after build constraints". A module whose files are all constrained out is still a module, and the toolchain's message about it names the module already.

A module the scan cannot read fails alone. One scan serves every tool in the workspace, so a module with an unreadable go.mod or a Go file whose imports will not parse records the reason against itself and the pass carries on. A syntax error after a file's imports is the compiler's to report, as with go build: the scan parses no further than the imports. Anything that needs that module's sources raises the reason, which names the file; everything else is unaffected. Test checks report their own failures. The manual generate helper can ask each selected module whether it holds generators and receive an answer rather than an error. Failing the whole scan instead would let one bad file stop every check in the repository, including for modules you had excluded.

Discovery errors name the file. go: error reading go.mod: missing module declaration, from whichever container happened to run first, is not something you can act on. The scan catches the same fault and says vendored/go.mod: missing module declaration, and an unresolvable local replace points at the line that declares it.

Scan flags mean what they say. A bare scan follows go:embed and local replace directives. --test also follows go:test:include, and --generate also follows go:generate:include and the modules a go:generate go -C reaches. A lint run asks for neither, because golangci-lint and Staticcheck type check the code rather than run it.

Selection is one syntax, asserted in one place. gomod's suite owns the selection matrix; each tool's suite asserts only that its own settings reach it.

Fixtures are shared where they are the same. testdata/ and fixtures/ at the root serve every module, with a .golangci.yml in the ones golangci-lint lints. A tool keeps its own only when the fixture is specific to it — golangci-lint/testdata/projects fails under any configuration but the one each part is meant to get, and golangci-lint/testdata/go-module-lint-fail does not compile, so neither can live where go's module test helper would find it.

Known problem: only one failure is reported

An explicit collection lint call over three failing projects reports one failure, not three. These helpers aggregate results into a directory and sync it, and the first error stops every later report; you repair one project, run again, and find the next. go reads the shared JSON report separately for each test check, so each failed test has its own result.

The obvious repair for the lint modules — collecting exit codes rather than merging outputs — does report all three, but it loses the parallel run. Dang has no structured concurrency primitive of its own, so this should be repaired once, in Dang, rather than twice here.

Development

The Go changes can be checked with:

dagger check -m .dagger/modules/go-dev

The Go selection checks require the PR 14802 collection API. The linter suites use per-project calls supported by both collection APIs.

The root dagger.toml installs go's suite as the entrypoint and each other suite under a <tool>-checks key, which prefixes its checks. Every module needs v1.0.0-beta.15 or later, the first release with collections and the Workspace.resolve that //go:generate:container uses.

One suite, or one check, at a time:

dagger check -m .dagger/modules/go-dev            # the root go module
dagger check -m gomod/.dagger/modules/e2e         # the shared library
dagger check -m golangci-lint/.dagger/modules/e2e
dagger check -m staticcheck/.dagger/modules/e2e

dagger check -m gomod/.dagger/modules/e2e scan-check   # or one check by name

testdata/ and fixtures/ hold the modules the suites run against. Several of them fail on purpose — a failing test, a lint diagnostic, a module with no packages — and the suites assert those failures, so repairing a fixture would be a hole in the coverage rather than a repair.

Relationship to the standalone modules

github.com/dagger/golangci-lint and github.com/dagger/staticcheck came first and still work. The modules here are the same tools rebuilt on the shared library, so they gain what it gives every module: one notion of a module root, one selection syntax, one source scan shared across every tool in the workspace.

Two differences are worth knowing before you switch:

  • They lint projects, rooted at each configuration file, rather than Go modules. A repository with no configuration file is not linted at all.
  • skipLint no longer takes a workspace. It never read one; selection is a question about a path.
  • A bare scan no longer implies test inputs. The standalone modules were embed-only in practice, so this changes nothing about what they mounted, but the flag now says so.

Filtered-view regression

The selected-* checks in go-dev cover multiple modules and packages, excluded tests, isolated test failures, teardown failures, undiscovered test failures, and build failures in packages with and without test checks. They run as part of the normal Go development suite:

dagger check -m .dagger/modules/go-dev 'selected-*'

Run the suite with Dagger v1.0.0-beta.22 or later. Each check creates its fixture with Dagger directories and invokes the engine-provided, version-matched CLI in an isolated session. This exercises the engine's active selection; direct Dang collection calls do not apply that view. No host Python or Bash harness is needed.

About

A Dagger module for Go - a programming language for building simple, secure, scalable systems.

Resources

Code of conduct

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages