From 9781f84bec0719e962fea6f9da08cab08604f6b5 Mon Sep 17 00:00:00 2001 From: Tom Chauveau Date: Thu, 13 Aug 2026 14:28:23 +0200 Subject: [PATCH] feat(checks): cover workspace configs in a git subdirectory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every workspace the checks built put dagger.toml at the git root, so nothing covered the monorepo layout where several project workspaces sit in subdirectories of one repository. That is the shape dagger/dagger#13889 is about: the workspace root is the git root, so the caller's cwd sits below it and module paths cross the engine <-> SDK boundary root-relative while the selected config reads them relative to itself. Add a `monorepo` group building a third workspace: dagger.toml in `common/`, the SDK vendored under it, every command driven from `common/`. - installs-from-subdir-config: `sdk install` registers the SDK in common/dagger.toml. - scaffolds-module-from-subdir-config: `module init` writes common/.dagger/modules/subdir-mod/dagger-module.toml. - keeps-module-under-subdir-config: `module init` writes nothing outside the module directory. - subdir-module-loads: the scaffolded module generates and serves its API. - explicit-path-keeps-module-whole: `module init --path` writes the module config at the requested path and nothing outside it. The --path check asserts co-location, not installation. The engine gates the [modules.] entry on `usingDefaultPath`, so an explicit path authors a module without installing it — the same at a git-root workspace, so it is not a monorepo defect. What the issue reports there is a split: the engine's module config and the SDK's files landing in two different directories. Two of the five pass. The other three are one engine defect and stay red until it is fixed: initModuleChanges computes relPath workspace-root-relative, writes the module's dagger-module.toml there, then records `source = relPath` in a config whose sources resolve relative to its own directory. They assert the contract, not a convention, so they hold whichever side is reconciled. sdk-sdk stages files without the cwd-scoped polyfill fork, so it shows the failure the issue does not: `module init` raises nothing and writes the whole module — config and sources — at the git root, while common/dagger.toml registers it at common/.dagger/modules/subdir-mod. Every later command that loads the workspace fails. No SDK-side fix reaches this; prefixing the SDK's changeset with ws.cwd moves only its half and splits the module, leaving the engine's dagger-module.toml stranded at the git root. Signed-off-by: Tom Chauveau --- README.md | 20 ++++++ checks-monorepo.dang | 104 +++++++++++++++++++++++++++ sdk-run.dang | 16 +++++ sdk-sdk.dang | 9 ++- sdk-target.dang | 166 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 checks-monorepo.dang diff --git a/README.md b/README.md index 9f3233c..e656e7c 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,25 @@ A second workspace exercises local path dependencies — `chain-a` depends on depend on is generated, so an SDK that widens generation to the whole workspace fails here. +A third workspace exercises the monorepo layout — `dagger.toml` in a `common/` +subdirectory of the git root, with everything driven from that subdirectory +([dagger#13889](https://github.com/dagger/dagger/issues/13889)): + +- `dagger sdk install` registers the SDK in `common/dagger.toml`. +- `dagger module init` scaffolds a module under `common/.dagger/modules/`, + writes nothing outside it, and the module loads afterwards. The workspace + root is the git root, so the default module path the engine hands the SDK is + not under the caller's cwd: an SDK that reads it cwd-relative refuses it, and + one that reads it root-relative writes the module a directory above where + `common/dagger.toml` registers it. +- `dagger module init --path` keeps the whole module at that path. This is the + quiet half: an explicit path is workspace-root-relative, so a path naming the + config directory is also under the caller's cwd read the other way, and + nothing refuses it — the engine's module config and the SDK's files just land + in two different directories. An explicit path deliberately skips the + `[modules.]` entry that the default layout writes, here as at the + workspace root, so there is nothing to load by name. + Function-level contract checks additionally call the SDK's `initModule` directly (always with an explicit `--path`, as the engine does) and inspect the returned changesets: `initModule` must seed at least one file, must not @@ -66,6 +85,7 @@ behavior they cover and reported as `:`: | `generation` | `checks-generate.dang` | `dagger generate` and the SDK's `@generate` hook | | `module` | `checks-module.dang` | the scaffolded module loading and serving its API | | `chain` | `checks-chain.dang` | local path dependencies and cwd-anchored generation | +| `monorepo` | `checks-monorepo.dang` | a workspace config in a subdirectory of the git root | | `contract` | `checks-contract.dang` | function-level `initModule` changeset behavior | | `template` | `template.dang` | sdk-sdk's own scaffolding template | diff --git a/checks-monorepo.dang b/checks-monorepo.dang new file mode 100644 index 0000000..e76ef74 --- /dev/null +++ b/checks-monorepo.dang @@ -0,0 +1,104 @@ +""" +Checks for workspaces whose dagger.toml lives in a subdirectory of the git +root, the layout of a monorepo where several project workspaces share one +repository. + +The workspace root is the git root, so the caller's cwd sits below the root +instead of at it. Module paths cross the engine <-> SDK boundary +workspace-root-relative, so an SDK that reads them relative to the caller's cwd +either refuses the path outright or lands its files in a different directory +than the module config the engine writes. + +See https://github.com/dagger/dagger/issues/13889. +""" +type MonorepoChecks { + let harness: SdkHarness! + + """ + `dagger sdk install` should register the SDK in a subdirectory config. + """ + pub installsFromSubdirConfig(ws: Workspace!): Void @check { + let testTarget = harness.sdkTarget(ws) + let run = testTarget.installInSubdir + run.assertSuccess + + if (run.workspaceFile(testTarget.subdirWorkspaceConfigPath).contains(testTarget.sdkInstallName) == false) { + raise "sdk install should register the SDK in " + testTarget.subdirWorkspaceConfigPath + } + } + + """ + `dagger module init` should scaffold a module from a subdirectory config. + + This is the reported failure: the engine hands the SDK the default module + path workspace-root-relative, and a cwd-scoped SDK rejects it as being + outside its changeset root. + """ + pub scaffoldsModuleFromSubdirConfig(ws: Workspace!): Void @check { + let testTarget = harness.sdkTarget(ws) + let run = testTarget.initSubdirModule + run.assertSuccess + + if (run.workspaceHasFile(testTarget.subdirModuleConfigPath) == false) { + raise "module init from " + testTarget.subdirWorkspaceDir + " should write " + testTarget.subdirModuleConfigPath + } + } + + """ + `dagger module init` should keep the whole module under the config directory. + + The engine's config changeset is applied workspace-root-relative while the + SDK's file changeset is applied relative to the caller's cwd, so the two + halves of a new module can land in different directories. + """ + pub keepsModuleUnderSubdirConfig(ws: Workspace!): Void @check { + let testTarget = harness.sdkTarget(ws) + let run = testTarget.initSubdirModule + run.assertSuccess + + let leaked = run.workspaceChangesOutsideModule(testTarget.subdirModulePath, testTarget.subdirWorkspaceDir) + if (leaked != "") { + raise "module init from " + testTarget.subdirWorkspaceDir + " should write nothing outside " + testTarget.subdirModulePath + "\nchanged:\n" + leaked + } + } + + """ + A module scaffolded from a subdirectory config should load and serve its API. + + A module split across two directories still registers in dagger.toml, so + loading it is what proves both halves ended up together. + """ + pub subdirModuleLoads(ws: Workspace!): Void @check { + let testTarget = harness.sdkTarget(ws) + testTarget.generateSubdirWorkspace.assertSuccess + testTarget.runInSubdir(["api", "functions", testTarget.subdirModuleName]).assertSuccess + } + + """ + `dagger module init --path` from a subdirectory config should keep the whole + module at that path. + + This is the half of the layout that fails quietly. An explicit path is + workspace-root-relative, so a path naming the config directory is also under + the caller's cwd read the other way: nothing refuses it, and the engine's + module config and the SDK's files land in two different directories. + + An explicit path deliberately skips the `[modules.]` entry that the + default layout writes, here as at the workspace root, so the module is + authored rather than installed and there is nothing to load by name. + """ + pub explicitPathKeepsModuleWhole(ws: Workspace!): Void @check { + let testTarget = harness.sdkTarget(ws) + let run = testTarget.initSubdirModuleAtPath + run.assertSuccess + + if (run.workspaceHasFile(testTarget.subdirPathModuleConfigPath) == false) { + raise "module init --path " + testTarget.subdirPathArg + " should write " + testTarget.subdirPathModuleConfigPath + } + + let leaked = run.workspaceChangesOutsideModule(testTarget.subdirPathArg, testTarget.subdirWorkspaceDir) + if (leaked != "") { + raise "module init --path " + testTarget.subdirPathArg + " should write nothing outside it\nchanged:\n" + leaked + } + } +} diff --git a/sdk-run.dang b/sdk-run.dang index 9d53dbd..af8eeda 100644 --- a/sdk-run.dang +++ b/sdk-run.dang @@ -93,6 +93,22 @@ type SdkRun { .stdout } + """ + Return the git status of everything a `module init` run wrote outside the new + module's own directory, ignoring the workspace config and lock file it is + meant to update. Empty when the module's files landed with its config. + """ + pub workspaceChangesOutsideModule(modulePath: String!, configDir: String!): String! { + container + .withExec([ + "git", "-C", "/work", "status", "--porcelain", "--untracked-files=all", "--", ".", + ":(exclude)" + modulePath, + ":(exclude,glob)" + configDir + "/*.toml", + ":(exclude,glob)" + configDir + "/*.lock", + ]) + .stdout + } + """ The first failed pipeline command, as recorded by the run wrapper. """ diff --git a/sdk-sdk.dang b/sdk-sdk.dang index b46d2bf..97be225 100644 --- a/sdk-sdk.dang +++ b/sdk-sdk.dang @@ -15,7 +15,7 @@ and `@generate` hooks directly and inspect the returned changesets. Checks are grouped by the behavior they cover and reported as `:`: `install`, `init`, `generation`, `module`, `chain`, -`contract`, and `template`. +`monorepo`, `contract`, and `template`. Under CLI 1.0 the engine owns module bookkeeping (dagger-module.toml, workspace config, dependency and engine-version edits). An SDK module implements only @@ -112,6 +112,13 @@ type SdkSdk { ChainChecks(harness: harness) } + """ + Checks for workspaces whose config lives in a subdirectory of the git root. + """ + pub monorepo: MonorepoChecks! { + MonorepoChecks(harness: harness) + } + """ Function-level checks of the SDK's `initModule` contract. """ diff --git a/sdk-target.dang b/sdk-target.dang index eb15a94..ea32629 100644 --- a/sdk-target.dang +++ b/sdk-target.dang @@ -41,6 +41,23 @@ type SdkTarget { """ pub chainLeafName: String! = "chain-c" + """ + Directory holding the workspace config in the monorepo layout, a + subdirectory of the git root rather than the root itself. + """ + pub subdirWorkspaceDir: String! = "common" + + """ + Name of the module scaffolded from the subdirectory workspace config. + """ + pub subdirModuleName: String! = "subdir-mod" + + """ + Name of the module scaffolded from the subdirectory workspace config with an + explicit `--path`. + """ + pub subdirPathModuleName: String! = "subdir-path-mod" + """ Return a copy of this target with a different command timeout. """ @@ -95,6 +112,45 @@ type SdkTarget { ".dagger/modules/" + chainLeafName } + """ + Workspace-root-relative path of the subdirectory workspace config. + """ + pub subdirWorkspaceConfigPath: String! { + subdirWorkspaceDir + "/dagger.toml" + } + + """ + Workspace-root-relative path of the module scaffolded from the subdirectory + workspace config. + """ + pub subdirModulePath: String! { + subdirWorkspaceDir + "/.dagger/modules/" + subdirModuleName + } + + """ + Workspace-root-relative path of the subdirectory module's config. + """ + pub subdirModuleConfigPath: String! { + subdirModulePath + "/dagger-module.toml" + } + + """ + Path passed to `dagger module init --path` from the subdirectory workspace, + the one from the issue report: a path that is under the caller's cwd only + when read workspace-root-relative. + """ + pub subdirPathArg: String! { + subdirWorkspaceDir + "/" + subdirPathModuleName + } + + """ + Workspace-root-relative path of the config for the module scaffolded with an + explicit `--path`. + """ + pub subdirPathModuleConfigPath: String! { + subdirPathArg + "/dagger-module.toml" + } + """ Run `dagger sdk install` for the SDK under test and capture the result. """ @@ -141,6 +197,38 @@ type SdkTarget { SdkRun(container: chainGeneratedFromLeafState, args: generateArgs) } + """ + Run `dagger sdk install` from a workspace config that lives in a + subdirectory of the git root, and capture the result. + """ + pub installInSubdir: SdkRun! { + SdkRun(container: subdirInstalledState, args: installArgs) + } + + """ + Run `dagger module init` from a subdirectory workspace config and capture the + result. + """ + pub initSubdirModule: SdkRun! { + SdkRun(container: subdirInitializedState, args: subdirInitArgs) + } + + """ + Run `dagger module init --path` from a subdirectory workspace config and + capture the result. + """ + pub initSubdirModuleAtPath: SdkRun! { + SdkRun(container: subdirPathInitializedState, args: subdirPathInitArgs) + } + + """ + Run `dagger generate` from a subdirectory workspace config and capture the + result. + """ + pub generateSubdirWorkspace: SdkRun! { + SdkRun(container: subdirGeneratedState, args: generateArgs) + } + """ Run a dagger command from the workspace root after the SDK is installed. """ @@ -164,6 +252,14 @@ type SdkTarget { runOn(chainGeneratedFromLeafState, "/work", args) } + """ + Run a dagger command from a subdirectory workspace config after its module is + scaffolded and generated. + """ + pub runInSubdir(args: [String!]!): SdkRun! { + runOn(subdirGeneratedState, subdirWorkdir, args) + } + """ Run a dagger command from the workspace root after a module is scaffolded and generated. @@ -289,6 +385,68 @@ type SdkTarget { pipe(chainInitializedState, "/work/" + chainLeafPath, generateArgs) } + """ + Scratch workspace whose dagger.toml sits in a subdirectory of the git root, + the layout of a monorepo holding several project workspaces. + + The SDK is vendored under that subdirectory, so the config location is the + only thing that differs from the root workspace. Everything is committed + before the first command, so later git status calls see only what the + commands wrote. + """ + let subdirWorkspace: Container! { + runner + .withDirectory(subdirWorkdir + "/" + vendorRoot, workspaceView, exclude: [".git", "**/.git"]) + .withNewFile("/work/" + subdirWorkspaceConfigPath, "# Dagger workspace configuration\n") + .withWorkdir("/work") + .withExec(["git", "init", "-q"]) + .withExec(["git", "add", "-A"]) + .withExec([ + "git", + "-c", "user.email=sdk-sdk@dagger.io", + "-c", "user.name=sdk-sdk", + "commit", "-q", "--allow-empty", "-m", "sdk-sdk subdirectory workspace", + ]) + } + + """ + Subdirectory workspace state after `dagger sdk install`. + """ + let subdirInstalledState: Container! { + pipe(subdirWorkspace, subdirWorkdir, installArgs) + } + + """ + Subdirectory workspace state after `dagger module init`. + """ + let subdirInitializedState: Container! { + pipe(subdirInstalledState, subdirWorkdir, subdirInitArgs) + } + + """ + Subdirectory workspace state after `dagger generate`. + """ + let subdirGeneratedState: Container! { + pipe(subdirInitializedState, subdirWorkdir, generateArgs) + } + + """ + Subdirectory workspace state after `dagger module init --path`. + + It branches off the installed state rather than the initialized one, so the + default-path init failing does not mask what an explicit path does. + """ + let subdirPathInitializedState: Container! { + pipe(subdirInstalledState, subdirWorkdir, subdirPathInitArgs) + } + + """ + Container path of the subdirectory holding the workspace config. + """ + let subdirWorkdir: String! { + "/work/" + subdirWorkspaceDir + } + """ Workspace-relative path of the vendored SDK module source. """ @@ -308,6 +466,14 @@ type SdkTarget { ["generate"] } + let subdirInitArgs: [String!]! { + ["module", "init", sdkInstallName, subdirModuleName] + } + + let subdirPathInitArgs: [String!]! { + ["module", "init", sdkInstallName, subdirPathModuleName, "--path", subdirPathArg] + } + """ Run a dagger command on a workspace state and capture the result. """