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. """