Skip to content

Instantly share code, notes, and snippets.

@shykes
Last active February 13, 2026 17:44
Show Gist options
  • Select an option

  • Save shykes/e4778dc5ec17c9a8bbd3120f5c21ce73 to your computer and use it in GitHub Desktop.

Select an option

Save shykes/e4778dc5ec17c9a8bbd3120f5c21ce73 to your computer and use it in GitHub Desktop.
Dagger Design: Part 1 - Module vs. Workspace

Dagger Design: Part 1 - Workspaces and Modules

Dagger Design: Part 1 - Workspaces and Modules

This proposal is part 1 of an multi-part proposal to simplify the "design knot" - an interconnected set of design and ux problems blocking implementation of a wide range of features and improvements

Table of Contents

Problem

Dagger currently conflates two distinct concepts in a single dagger.json file:

  • Project configuration (what tools to use, how to configure them)
  • Module definition (what code to package, what dependencies it needs)

This causes confusion about what files are accessible, how dependencies work, and what appears in the CLI.

Core Concepts

This proposal establishes workspaces and modules as two fundamentally different things, each with their own configuration file and dependency model.

What is a Workspace?

A workspace is a directory used as context for configuring and using Dagger. Typically it is a git repository, or a subdirectory within a larger repo.

The main way to configure a Dagger workspace is to add modules to it, possibly with custom workspace-level configuration.

What is a Module?

A Dagger module is software packaged for the Dagger platform (engine & SDK). It implements functions and types to extend the capabilities of a Dagger workspace: typically with new ways to build, check, generate, deploy, or publish artifacts within the workspace.

Modules can access Dagger's powerful API to orchestrate system primitives (containers, secrets, etc), as well as interact with the contents of the workspace. This allows deep integration with the project's existing tools by parsing their configuration files and adapting behavior - the best of both worlds between native integration and cross-platform repeatability.

Comparison

Workspace Module
What it is A directory (project context) Packaged software
Configured via .dagger/config.toml dagger.json
Contains Modules added to it Functions and types
Purpose Configure Dagger for this project Extend Dagger's capabilities
Analogy A VS Code workspace A VS Code extension

Modules are added to workspaces. Once added, a module can access and interact with the workspace's contents.

Dependency Model

There are two distinct ways to depend on a module:

Relationship Meaning Configured in Example
Workspace → Module "Use this module in my project" .dagger/config.toml Adding go-toolchain to build your Go code
Module → Module "My code calls this module" dagger.json Importing a helper library in your module's source

These serve different purposes:

  • Workspace → Module is project configuration. The module gains access to your workspace and extends your CLI. This is how you set up your development environment.

  • Module → Module is code dependency. One module's implementation calls another module's functions. The dependency is internal - it doesn't affect the workspace or CLI.

A module added to a workspace is not the same as a module dependency. They live in different config files, are installed with different commands, and have different effects.

Configure a Workspace

How Dagger Loads Configuration

Configuration loading happens in the engine, not the CLI. When the CLI connects to the engine, the engine detects the workspace and loads modules before the CLI issues any commands.

Workspace Detection (always runs)

  1. Find workspace: Walk up from the client's working directory looking for a .dagger/ directory. The directory containing .dagger/ is the workspace root.

    • If not found: check for dagger.json with legacy triggers → fail with migration error (see No Runtime Compat Mode). Otherwise, fall back to .git directory or current directory as workspace root.
  2. Parse config: Read .dagger/config.toml if it exists. If .dagger/ exists but has no config.toml, the workspace is valid but empty. Resolve all paths relative to the .dagger/ directory.

Workspace detection always runs, regardless of other flags. The engine always knows the workspace root and config. This is important for the future workspace API (Part 2), where modules can access workspace context.

Module Loading

After workspace detection, the engine loads modules based on connect-time parameters sent by the client as metadata headers. These parameters control what gets loaded into the schema before any queries are served.

Connect-Time Parameters
Parameter Type Default Effect
ExtraModules []ExtraModule [] Additional modules to load. Each entry has a Ref (module source reference), optional Name override, and Alias flag (if true, the module's functions are promoted to the Query root as auto-aliases).
SkipWorkspaceModules bool false When true, skip loading modules from .dagger/config.toml. Workspace detection still runs.
IncludeCoreModule bool false When true, include core API functions (container, directory, git, etc.) at the Query root. When false (default), only module constructors and auto-aliased functions are available at the Query root.
RemoteWorkdir string "" A git ref (e.g. github.com/foo/bar@v1.0) to use as the workspace source. When set, the engine clones this repo and performs workspace detection within it instead of using the client's local filesystem. See Remote Workspaces.

These are the primitives. The CLI maps its flags onto them:

  • No flags: ExtraModules=[], SkipWorkspaceModules=false, IncludeCoreModule=false — workspace modules load from config, core API is not exposed at the Query root.
  • -m <ref>: ExtraModules=[{Ref: "<ref>", Alias: true}], SkipWorkspaceModules=true, IncludeCoreModule=false — workspace modules skipped, explicit module loaded with its functions as top-level commands, core API not exposed.
  • -C <git-ref>: RemoteWorkdir="<git-ref>" — workspace detection runs against the remote repo instead of the local filesystem. All other parameters apply normally.

Other clients (MCP servers, SDKs, custom tooling) can use these parameters directly to control module loading without going through CLI flags. For example, an MCP server could set IncludeCoreModule=true to expose both module functions and core API primitives to an AI agent.

Loading Flow

Workspace modules (when SkipWorkspaceModules is false):

  1. Load workspace modules: For each entry in [modules], resolve the source to a module and install its constructor as a top-level function in the GraphQL schema. If alias = true, the module's functions are also installed as top-level aliases (see Auto-Aliases).
  2. Serve: The CLI dispatches dagger call <name> <function> by looking up <name> in the schema — either a module constructor or an aliased function.

Extra modules (when ExtraModules is non-empty):

  1. Load extra modules: For each entry, resolve the ref to a module. If Alias is true, promote its functions to the Query root. If Name is set, override the module's name.
  2. Serve: The CLI dispatches dagger call <function> against the module's functions at the Query root.

The CLI has a single initialization path — it never calls Serve() or manages module state. It simply reads whatever the engine served and builds commands from the schema's type definitions.

Design Rationale for -m

The -m flag provides:

  • Backwards compatibility: Existing CI scripts using dagger call -m ./ci test continue to work. The module loads in isolation, functions are top-level.
  • Ad-hoc module usage: Run any module by reference without installing it in the workspace: dagger call -m github.com/foo/bar build.
  • No duplicate code paths: Both modes use the same engine-side module loading pipeline. The CLI has a single initialization path — it never calls Serve() or manages module state.
  • Workspace is always available: Even with -m, the workspace has been detected. Today this is unused, but it enables a future where -m modules can access workspace context (e.g., reading workspace config, accessing workspace files toolchain-style).

The CLI always operates in workspace context. There is no "module context" at the CLI level — -m simply changes which modules the engine loads.

Adding Modules

# Add a module to the workspace
dagger install github.com/dagger/go-toolchain

The module is registered under its name from dagger.json. Override with --name:

dagger install github.com/dagger/go-toolchain --name=go

This updates .dagger/config.toml. If no workspace exists yet:

  • If in a git repository, creates .dagger/config.toml at the repo root
  • Otherwise, creates in the current directory

Once installed, module functions are available:

dagger call go build
dagger call go test

Configuration Reference

Config file: .dagger/config.toml (human-editable)

# Paths to ignore during workspace operations (extends .gitignore)
# ignore = ["docs/**", "marketing/**"]

# Modules added to this workspace
[modules.ci]
source = "modules/ci"

[modules.node]
source = "github.com/dagger/node-toolchain@v1.0"

[modules.go]
source = "github.com/dagger/go-toolchain@v1.0"
config.goVersion = "1.22"
config.lintStrict = true

Each module entry is a table with a required source and optional config.* keys. The table key (e.g., go) is the module's local name in this workspace — it determines the CLI namespace (dagger call go build) and is used by aliases and other references.

Paths are relative to the .dagger/ directory.

Module Configuration

The config.* keys set default values for the module's constructor arguments:

[modules.go]
source = "github.com/dagger/go-toolchain@v1.0"
config.goVersion = "1.22"
config.lintStrict = true
config.tags = ["integration", "unit"]

The config keys map directly to constructor argument names. Supported types: strings, booleans, numbers, arrays.

This replaces customizations in dagger.json. Only constructor arguments can be configured - this keeps the config surface simple and encourages module authors to expose important settings as constructor parameters.

Config values are evaluated by the engine the same way as .env defaults — variable expansion (${SYSTEM_VAR}), env:// references for secrets, and file/directory path resolution all work:

[modules.go]
source = "github.com/dagger/go-toolchain@v1.0"
config.goVersion = "1.22"
config.cacheDir = "${HOME}/.cache/go"
config.apiKey = "env://GO_API_KEY"

This replaces .env-based user defaults for constructor arguments. The .env mechanism for setting constructor arg defaults is deprecated — use config.* in workspace config instead. The engine provides the same evaluation behavior regardless of whether values come from .env or config.toml.

.env files remain supported for non-constructor function argument defaults, which cannot be expressed in workspace config.

Auto-Aliases

A module with alias = true has all of its functions promoted to top-level commands:

[modules.ci]
source = "modules/ci"
alias = true

With this config, if the ci module has functions build, test, and lint, all of the following work:

dagger call ci build     # explicit: module constructor + function
dagger call build        # auto-alias: function promoted to top level
dagger call test         # auto-alias
dagger call lint         # auto-alias

Auto-aliases are the primary mechanism for backwards compatibility during migration. When a legacy "project module" is migrated to a workspace module, setting alias = true preserves existing dagger call <function> commands without requiring an [aliases] section or per-function configuration.

This strikes a balance between facilitating migration and avoiding the baggage of a "main module" concept. The module is still explicit in the config — it just has a shorthand for its functions. Users can always use the fully qualified dagger call ci build form, and can remove alias = true when they're ready to drop the shortcuts.

Environments

Module config and workspace settings can be overridden per environment (e.g., CI, staging, production). Environments are selected explicitly via --env:

dagger check --env=ci

See Part N: Environments (coming soon) for the full design.

Workspace Ignore

Top-level ignore defines paths to skip during all workspace operations (glob, search, file access). This extends .gitignore for tracked-but-irrelevant parts of the repo:

ignore = ["docs/**", "marketing/**", "data-science/**"]

The engine already respects .gitignore by default. The ignore key covers paths that are tracked in git but irrelevant to Dagger - useful in large monorepos to speed up module discovery.

For scoping which artifacts to operate on (rather than which files to see), use artifact path filtering. See Part 3: Artifacts:

dagger check --path='./myapp'

Lock file: .dagger/lock (machine-managed)

[["version", "1"]]
["modules", "resolve", ["github.com/dagger/go-toolchain@v1.0"], "abc123..."]
["core", "git.ref", ["https://github.com/dagger/go-toolchain", "v1.0"], "abc123..."]

The lock file pins module versions to exact commits and caches runtime lookups (git refs, container digests, HTTP content). Modules can store their own lookups under their namespace. See PR #11156 for the lockfile design.

Develop a Module

Most projects eventually need custom logic that doesn't fit existing modules - custom build steps, project-specific CI/CD, glue code combining multiple tools. This is when you create a module.

Create a Module

# From anywhere in the workspace
dagger module init --sdk=go ci

This:

  1. Creates .dagger/modules/ci/ with module source
  2. Auto-installs in .dagger/config.toml:
    [modules.ci]
    source = "modules/ci"
  3. Module is immediately callable: dagger call ci <function>

The --sdk flag is required. Options: go, python, typescript, php, or a custom SDK module reference.

Directory structure:

repo/
├── .dagger/
│   ├── config.toml
│   └── modules/
│       └── ci/
│           ├── dagger.json
│           └── main.go
└── src/

Write Module Code

Your module code lives in .dagger/modules/<name>/. Edit the generated source files to add functions:

// .dagger/modules/ci/main.go
func (m *Ci) Build(ctx context.Context) *dagger.Container {
    // ...
}

Changes take effect immediately - just run dagger call ci build.

Share a Module

To make a module installable by other projects:

Option 1: Promote an existing module

Move it from .dagger/modules/foo/ to a git-accessible location (dedicated repo, monorepo subdirectory, etc.).

Option 2: Start standalone

Run dagger module init outside any workspace:

mkdir my-module && cd my-module
dagger module init --sdk=go my-module

When no workspace is found, the module is created in the current directory.

Either way, others can then install it:

dagger install github.com/you/my-module

To test a standalone module during development, create a workspace:

dagger install .
dagger call my-module <function>

Remote Workspaces

The --workdir / -C flag accepts remote git references in addition to local paths. This allows running Dagger against a remote project's workspace without cloning it locally.

Usage

# Run a function from a remote project's workspace
dagger -C github.com/dagger/dagger@main call ci build

# Same thing, long form
dagger --workdir github.com/dagger/dagger@main call ci build

# Subdirectory within a repo
dagger -C github.com/dagger/dagger/subproject@v1.0 call build

# Local paths still work (existing behavior)
dagger -C /path/to/project call build
dagger -C ../sibling-project call build

The flag name -C follows the git -C convention for changing the working directory.

How It Works

When the engine receives a RemoteWorkdir connect-time parameter:

  1. Parse the git ref: Extract the repo URL, version (tag/branch/commit), and optional subdirectory.
  2. Clone the repo: Use the engine's existing git pipeline to fetch the repo at the specified version.
  3. Detect workspace: Look for .dagger/config.toml within the cloned tree (starting from the subdirectory if specified).
  4. Load modules: For each module entry in the config:
    • Local sources (e.g. source = "modules/ci") are resolved as paths within the cloned repo. The engine constructs a full git ref (e.g. github.com/dagger/dagger/.dagger/modules/ci@main) and loads the module through the standard git module pipeline.
    • Remote sources (e.g. source = "github.com/other/module@v1.0") are loaded as-is.

This reuses the same git cloning and module loading infrastructure that -m uses for remote modules. No new abstractions are needed — the engine already knows how to clone repos and load modules from git refs.

Limitations

  • Read-only: Mutating operations (dagger install, dagger module init) are not supported with a remote workdir. These commands require writing to the host filesystem.
  • No local host access: When using a remote workdir, modules cannot access the local filesystem via host.directory() etc. The workspace context is the cloned git tree, not the local machine.
  • Config values: config.* entries that reference local environment variables (${VAR}) or secrets (env://KEY) resolve against the local environment where Dagger runs, not the remote repo's environment.

Migration

This section covers the transition from the current model. The design principle is: no runtime compat mode. The engine has exactly one loading path (workspace config). All backwards compatibility is handled by dagger migrate, which transforms legacy configurations into the new format.

No Runtime Compat Mode

When the engine encounters a dagger.json with legacy triggers and no .dagger/config.toml, it fails with a clear message:

Error: this project uses a legacy module format.
Run 'dagger migrate' to update your project.

The engine never interprets legacy dagger.json at runtime. This keeps the loading path clean — one format, one behavior, no branching.

Detection Triggers

Two independent signals in dagger.json indicate a legacy project needing migration. They are not mutually exclusive — a single file can match both:

Trigger What it signals Example
source != "." Legacy "project module" — a module that doubles as project config "source": ".dagger"
Has toolchains Toolchains that should become workspace modules "toolchains": [...]

Modules that match neither trigger (pure modules with source == "." or absent, no toolchains) are not legacy. They get a clean break: call them via dagger call -m . or install them in a workspace with dagger install ..

dagger migrate

The dagger migrate command detects legacy triggers and transforms the project. It handles two cases: project module migration and toolchain migration.

Implementation

dagger migrate is implemented as a standalone Dagger module. It receives the project source via +defaultPath, performs the migration logic using Dagger's own APIs (module introspection for enumerating constructor args and functions, user defaults discovery, etc.), and returns a Changeset. The CLI's built-in Changeset handling shows the user a diff preview and prompts to apply.

# Run migration (shows diff, prompts to apply)
dagger call -m github.com/dagger/migrate migrate

# Or install as a toolchain first
dagger toolchain install github.com/dagger/migrate
dagger call migrate migrate

This keeps migration logic out of the engine, makes it independently testable, and dogfoods the module and Changeset APIs.

Project Module Migration

Triggered by: source != "." in dagger.json.

A "project module" is one where dagger.json sits at the project root with source code in a subdirectory (e.g., source: ".dagger"). In the new model, modules are self-contained packages — dagger.json lives alongside the source, and source is always ..

Steps:

  1. Move module source to .dagger/modules/<name>/:

    .dagger/*                →  .dagger/modules/<name>/
    dagger.json (project root)  →  .dagger/modules/<name>/dagger.json
    
  2. Update the moved dagger.json:

    • Remove source field (now always .)
    • Remove toolchains field (migrated separately, see below)
    • Rewrite dependencies[].source paths relative to new location
    • Rewrite include paths relative to new location
  3. Create .dagger/config.toml:

    • Add module under [modules] with alias = true to preserve backwards compat with dagger call <function>
    • Enumerate the module's constructor arguments and write each as a commented-out config.* entry (with type-appropriate example values)

Example — migrating dagger/dagger (name: dagger-dev, source: .dagger):

.dagger/config.toml (generated):

[modules.dagger-dev]
source = "modules/dagger-dev"
alias = true  # preserves 'dagger call <function>' backwards compat
# Constructor arguments (uncomment to configure):
# config.someArg = "value"

.dagger/modules/dagger-dev/dagger.json (moved and updated):

{
  "name": "dagger-dev",
  "sdk": {"source": "go"},
  "dependencies": [
    {"name": "changelog", "source": "../../toolchains/changelog"},
    {"name": "docs", "source": "../../toolchains/docs-dev"},
    ...
  ]
}

Toolchain Migration

Triggered by: toolchains field present in dagger.json.

Each toolchain becomes a workspace module. Toolchain source directories are left in place — only the configuration moves.

Steps:

For each toolchain entry:

  1. Add to [modules] in .dagger/config.toml, with path relative to .dagger/:

    [modules.go]
    source = "../toolchains/go"
    
    [modules.ci]
    source = "../toolchains/ci"
  2. Migrate customizations (if any):

    Customization type Action
    Default value for constructor arg Migrate to [modules.<name>.config]
    ignore, defaultPath, or other non-value customization for constructor arg Add warning comment with original value
    Customization targeting a non-constructor function Add warning comment with original value (cannot be migrated)

    Example — go toolchain with constructor-level ignore customization:

    [modules.go]
    source = "../toolchains/go"
    # WARNING: constructor arg 'source' had 'ignore' customization that cannot
    # be expressed as a config value. Original:
    # {"argument":"source","ignore":["bin",".git","**/node_modules",...]}

    Example — security toolchain with function-level customization:

    [modules.security]
    source = "../toolchains/security"
    # WARNING: customization for function 'scanSource' could not be migrated
    # (non-constructor). Original:
    # {"function":["scanSource"],"argument":"source","ignore":["bin",".git","docs",...]}
  3. Remove toolchains field from the migrated dagger.json.

User Defaults Migration

For each module (project module and toolchains), dagger migrate uses the Dagger API's existing user defaults introspection to discover .env-based defaults.

For each discovered default:

Default type Action
Constructor arg with simple value Migrate to config.* in config.toml
Constructor arg with variable expansion Migrate as-is — engine evaluates ${VAR} the same way in config.toml
Constructor arg with env:// reference Migrate as-is — engine handles env:// the same way in config.toml
Non-constructor function arg Add warning comment (cannot be expressed in workspace config)

Example — .env before migration:

GO_VERSION=1.22
GO_CGO=true
GO_CACHE_DIR=${HOME}/.cache/go
SECURITY_API_KEY=env://SECURITY_KEY

After migration in .dagger/config.toml:

[modules.go]
source = "../toolchains/go"
config.version = "1.22"
config.cgo = true
config.cacheDir = "${HOME}/.cache/go"

[modules.security]
source = "../toolchains/security"
config.apiKey = "env://SECURITY_KEY"

The migrated .env entries should be removed or commented out to avoid duplicate defaults. Note: the Dagger API's user defaults introspection may not track the source file path of each default — if so, dagger migrate should print which .env entries to remove manually.

Command Changes

Command Current New
dagger init Creates a module Deprecated; use dagger module init
dagger call in module dirs Loads module from dagger.json Only reads workspace config (.dagger/config.toml)
dagger call -m Specifies current module Loads explicit module instead of workspace modules. Workspace is still detected (not loaded). Functions appear as top-level commands.
dagger -C <ref> (new) Change workspace directory. Accepts local paths or remote git refs. Alias: --workdir.
dagger install Adds code dependency to module Adds module to workspace
dagger module dependency add (new) Adds code dependency to a module's dagger.json
dagger migrate (new) Migrates legacy project to workspace format

Real-World Examples

dagger/dagger.io

Before — workspace ancestor pattern (no sdk, no source, toolchains only):

{"name": "dagger.io", "engineVersion": "v0.19.8",
 "toolchains": [
   {"name": "api", "source": "api"},
   {"name": "dagger-cloud", "source": "cloud"}
 ]}

After dagger migrate:

.dagger/config.toml:

[modules.api]
source = "../api"

[modules.dagger-cloud]
source = "../cloud"

The dagger.json is removed (it had no sdk, no source — it was purely config).

dagger/dagger

Before — both triggers (source != ".", has toolchains):

{"name": "dagger-dev", "sdk": {"source": "go"}, "source": ".dagger",
 "toolchains": [
   {"name": "go", "source": "toolchains/go", "customizations": [...]},
   {"name": "security", "source": "toolchains/security", "customizations": [...]},
   ... (17 more)
 ],
 "dependencies": [...]}

After dagger migrate:

.dagger/config.toml:

[modules.dagger-dev]
source = "modules/dagger-dev"
alias = true  # preserves 'dagger call <function>' backwards compat

[modules.changelog]
source = "../toolchains/changelog"

[modules.ci]
source = "../toolchains/ci"

[modules.cli]
source = "../toolchains/cli-dev"

[modules.docs]
source = "../toolchains/docs-dev"

# ... (15 more toolchains)

[modules.go]
source = "../toolchains/go"
# WARNING: constructor arg 'source' had 'ignore' customization that cannot
# be expressed as a config value. Original:
# {"argument":"source","ignore":["bin",".git","**/node_modules",...]}

[modules.security]
source = "../toolchains/security"
# WARNING: customization for function 'scanSource' could not be migrated
# (non-constructor). Original:
# {"function":["scanSource"],"argument":"source","ignore":["bin",".git","docs",...]}

.dagger/modules/dagger-dev/dagger.json:

{
  "name": "dagger-dev",
  "sdk": {"source": "go"},
  "dependencies": [
    {"name": "changelog", "source": "../../toolchains/changelog"},
    {"name": "docs", "source": "../../toolchains/docs-dev"},
    {"name": "helm", "source": "../../toolchains/helm-dev"},
    {"name": "sdks", "source": "../../toolchains/all-sdks"},
    {"name": "engine-dev", "source": "../../toolchains/engine-dev"},
    {"name": "cli", "source": "../../toolchains/cli-dev"}
  ]
}

Status

Prototype in progress: PR #11812


Next: Part 2: Workspace API

@shykes
Copy link
Author

shykes commented Feb 5, 2026

Changelog

  • Removed [fs] workspace filters section
  • Scoping operations to part of the workspace belongs in the artifact layer (path-based filtering via dagger check --path), not filesystem-level hiding
  • Added cross-reference to Part 3 for path filtering

@shykes
Copy link
Author

shykes commented Feb 5, 2026

Changelog

  • Added [workspace] ignore for monorepo performance optimization
    • Extends .gitignore for tracked-but-irrelevant paths
    • Applied to all workspace operations (glob, search, file access)
    • Distinct from artifact path filtering (scoping operations vs hiding files)

@shykes
Copy link
Author

shykes commented Feb 5, 2026

Update: fleshing out migration and backwards compatibility. Getting ready to prototype.

@shykes
Copy link
Author

shykes commented Feb 5, 2026

Changelog

Migration section: complete rewrite

  • No runtime compat mode: engine has one loading path. Legacy dagger.json with triggers and no config.toml → hard fail with "run dagger migrate" message. No fallback, no branching.
  • Detection triggers: two independent signals — source != "." (legacy project module) and toolchains present. Not mutually exclusive.
  • dagger migrate detailed spec:
    • Project module migration: move source to .dagger/modules/<name>/, move and update dagger.json, rewrite dependency and include paths
    • Toolchain migration: convert to workspace modules in config.toml, migrate constructor-arg customizations to [modules.<name>.config], warn on unmigrateable customizations (function-level, ignore patterns)
    • Auto-generate [aliases] entries for top-level functions to preserve dagger call <function> backwards compat
  • Real-world examples: full before/after for dagger/dagger.io (simple case) and dagger/dagger (complex case with both triggers)

New feature: [aliases] in config.toml

  • Permanent feature (not migration-only) added to Configuration Reference
  • Array-encoded paths: deploy = ["k8s", "deploy"]
  • Supports deeper chains: scan = ["security", "source", "scan"]
  • Used by migration to preserve backwards compat, useful ongoing for project-level shortcuts

@shykes
Copy link
Author

shykes commented Feb 6, 2026

Changelog

Module config schema: always-table, no flex schema

  • Every module entry is now a TOML table with required source key: [modules.go] / source = "..."
  • No more flex schema (string vs table). Every module looks the same.
  • Constructor arg defaults use inline config.* keys: config.version = "1.22"
  • The table key (go, ci, etc.) is the module's local name in the workspace — determines CLI namespace and is referenced by aliases
  • dagger install auto-populates the name from the module's dagger.json name field
  • All examples updated throughout (config reference, migration examples, real-world examples)

@shykes
Copy link
Author

shykes commented Feb 6, 2026

Changelog

User defaults migration + .env deprecation

  • .env deprecated for constructor args: config.* in workspace config replaces .env-based user defaults for constructor arguments. Engine evaluates values the same way — variable expansion (${VAR}), env:// references, and path resolution all work in config.toml.
  • New migration step: dagger migrate uses Dagger API's existing user defaults introspection to discover .env defaults and migrate constructor-arg values to config.* entries.
  • Non-constructor defaults: treated like non-constructor customizations — warning comment, cannot be expressed in workspace config.
  • .env not fully removed: remains supported for non-constructor function argument defaults, which have no config.toml equivalent.

@shykes
Copy link
Author

shykes commented Feb 6, 2026

Changelog

Expanded "How Dagger Loads Configuration" section

Fleshed out the 3-step loading description into a 6-step precise spec:

  1. Find workspace (walk up for .dagger/config.toml, fail on legacy dagger.json)
  2. Parse config (paths relative to .dagger/)
  3. Load modules (resolve source, apply config.* as constructor defaults)
  4. Register aliases (array-encoded paths as top-level function aliases)
  5. Build schema (module constructors mounted by workspace name, aliases add entry points)
  6. Serve (CLI dispatches via schema lookup)

This should be detailed enough to prototype against.

@shykes
Copy link
Author

shykes commented Feb 10, 2026

Implementation Gaps (vs. PR #11812)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment