From cea219fddec4fab8b28264a1774c1c65102a56ba Mon Sep 17 00:00:00 2001 From: kpenfound Date: Mon, 27 Jul 2026 16:31:32 -0400 Subject: [PATCH 1/6] feat: add sdk-test module for black-box SDK contract checks Add ./sdk-test, a CLI 1.0 module that black-box tests SDK modules (go-sdk, dang-sdk, typescript-sdk, python-sdk) through a release Dagger CLI, the way a user drives them: dagger -m github.com/dagger/sdk-sdk/sdk-test -W check The checks vendor the SDK into a scratch git workspace inside a runner container and exercise the full lifecycle: `dagger sdk install` marks the SDK as-sdk, `dagger module init` scaffolds and registers a module and records the authoring SDK, `dagger generate` succeeds, the generated module serves functions, `dagger sdk module-options` introspects initModule, and `dagger module engine`/`deps` verbs work from the module directory. When run inside this repository the checks redirect to a minimal fixture SDK under .dagger/modules/sdk-test-e2e, keeping the harness self-testing. Verified green against the fixture and dang-sdk on a v1.0.0-beta.7 CLI/engine. Co-Authored-By: Claude Fable 5 Signed-off-by: kpenfound --- .../fixtures/sdk-under-test/dagger.json | 7 + .../fixtures/sdk-under-test/main.dang | 98 ++++ README.md | 8 + sdk-test/README.md | 51 ++ sdk-test/dagger-module.toml | 9 + sdk-test/sdk-test.dang | 481 ++++++++++++++++++ 6 files changed, 654 insertions(+) create mode 100644 .dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/dagger.json create mode 100644 .dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/main.dang create mode 100644 sdk-test/README.md create mode 100644 sdk-test/dagger-module.toml create mode 100644 sdk-test/sdk-test.dang diff --git a/.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/dagger.json b/.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/dagger.json new file mode 100644 index 0000000..944850a --- /dev/null +++ b/.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/dagger.json @@ -0,0 +1,7 @@ +{ + "name": "sdk-under-test", + "engineVersion": "v0.20.8", + "sdk": { + "source": "dang" + } +} diff --git a/.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/main.dang b/.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/main.dang new file mode 100644 index 0000000..4a71f75 --- /dev/null +++ b/.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/main.dang @@ -0,0 +1,98 @@ +""" +Minimal SDK module fixture used to run sdk-test's black-box checks end-to-end. + +Implements the CLI 1.0 SDK contract: `targetRuntime`, `initModule`, and `mod`. +Scaffolded modules run on the built-in Dang runtime. The starter source always +declares a `TestMod` root type, so the fixture only supports initializing a +module named "test-mod" — the name sdk-test uses. +""" +type SdkUnderTest { + """ + Engine runtime recorded for modules created with this SDK. + """ + targetRuntime: String! { + "dang" + } + + """ + Scaffold a new module for `dagger module init sdk-under-test `. + """ + initModule( + ws: Workspace!, + name: String!, + path: String!, + template: String! = "", + ): Changeset! { + let modPath = if (path == "" or path == ".") { "." } else { path.trimSuffix("/") } + + directory + .withNewFile(modPath + "/main.dang", starterSource) + .changes(directory) + } + + """ + Return the SDK module at a workspace path. + """ + mod(ws: Workspace!, path: String! = ".", findUp: Boolean! = true): SdkUnderTestMod! { + SdkUnderTestMod(modulePath: path) + } + + """ + Starter source for a module named "test-mod". + """ + let starterSource: String! { + "\"\"\"\nStarter module generated by the sdk-under-test fixture.\n\"\"\"\ntype TestMod {\n \"\"\"\n Return a greeting from the fixture starter module.\n \"\"\"\n pub hello: String! {\n \"hello from the fixture SDK\"\n }\n}\n" + } +} + +""" +Minimal module handle for the fixture SDK. +""" +type SdkUnderTestMod { + let modulePath: String! + + """ + Module path relative to the current workspace. + """ + pub path: String! { + modulePath + } + + """ + Dependency manager. + """ + pub deps: SdkUnderTestDeps! { + SdkUnderTestDeps() + } + + """ + Engine manager. + """ + pub engine: SdkUnderTestEngine! { + SdkUnderTestEngine() + } +} + +""" +Minimal dependency manager for the fixture SDK. +""" +type SdkUnderTestDeps { + """ + Return configured dependency names. + """ + pub list: [String!]! { + [] + } +} + +""" +Minimal engine manager for the fixture SDK. +""" +type SdkUnderTestEngine { + """ + Return the configured engine version. + """ + pub required: String! { + "v1.0.0-beta.7" + } +} diff --git a/README.md b/README.md index 0186cbe..77a20b5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,14 @@ Shared contract checks for official SDK helper modules. +For black-box lifecycle checks that drive an SDK module through the real CLI +(`dagger sdk install`, `dagger module init`, `dagger generate`), see +[`sdk-test`](./sdk-test): + +```sh +dagger -m github.com/dagger/sdk-sdk/sdk-test -W check +``` + Start a new Dang SDK helper module: ```sh diff --git a/sdk-test/README.md b/sdk-test/README.md new file mode 100644 index 0000000..b931d81 --- /dev/null +++ b/sdk-test/README.md @@ -0,0 +1,51 @@ +# sdk-test + +Black-box contract checks for Dagger SDK modules, such as +`github.com/dagger/go-sdk`, `github.com/dagger/dang-sdk`, +`github.com/dagger/typescript-sdk`, and `github.com/dagger/python-sdk`. + +Where `mod-test` calls one module's functions, `sdk-test` exercises an SDK the +way a user does across its whole lifecycle. The checks vendor the SDK module +into a scratch git workspace inside a runner container, install a release +Dagger CLI, then drive the SDK through real CLI commands: + +- `dagger sdk install ./` registers the SDK and marks it `as-sdk` in + `dagger.toml`. +- `dagger module init test-mod` scaffolds a new module, writes its + `dagger-module.toml`, installs it in `dagger.toml`, and records the SDK as + the module's authoring SDK. +- `dagger generate` succeeds on the fresh scaffold. +- After generation the scaffolded module serves functions: + `dagger api functions test-mod`. +- `dagger sdk module-options ` introspects the SDK's `initModule` + capability. +- `dagger module engine required` and `dagger module deps list` work from the + scaffolded module directory. + +Run the checks against an SDK repository: + +```sh +dagger -m github.com/dagger/sdk-sdk/sdk-test -W check +``` + +For example: + +```sh +dagger -m . -W https://github.com/dagger/go-sdk check +``` + +When run inside the sdk-sdk repository itself, the checks redirect to the +fixture SDK at `.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test`, so +`dagger -m ./sdk-test -W . check` self-tests the harness. + +Configure the CLI release with the top-level `dagger-cli-version` setting; the +default is `1.0.0-beta.7`. Individual targets accept `with-timeout` for slow +SDKs (the default command timeout is `10m`). + +Custom checks can reuse the harness through `target`: + +```dang +let testTarget = sdkTest.target(module.workspaceView, module.sourceRootPath) +testTarget.install.assertSuccess +testTarget.runInModule(["module", "deps", "list"]).assertSuccess +``` diff --git a/sdk-test/dagger-module.toml b/sdk-test/dagger-module.toml new file mode 100644 index 0000000..27cf17e --- /dev/null +++ b/sdk-test/dagger-module.toml @@ -0,0 +1,9 @@ +name = "sdk-test" +engineVersion = "v1.0.0-0" + +[runtime] + source = "dang" + +[[dependencies]] + name = "polyfill" + source = "../polyfill" diff --git a/sdk-test/sdk-test.dang b/sdk-test/sdk-test.dang new file mode 100644 index 0000000..43949c2 --- /dev/null +++ b/sdk-test/sdk-test.dang @@ -0,0 +1,481 @@ +""" +Black-box contract checks for Dagger SDK modules. + +Run against an SDK repository with: + + dagger -m github.com/dagger/sdk-sdk/sdk-test -W check + +The checks vendor the SDK module into a scratch git workspace, then drive it +the way a user would through a release Dagger CLI: `dagger sdk install`, +`dagger module init`, and the `dagger module` authoring verbs. +""" +type SdkTest { + """ + Dagger CLI release version used by black-box tests. + """ + pub daggerCliVersion: String! = "1.0.0-beta.7" + + """ + Return a black-box test target for an SDK module directory view. + """ + pub target(workspaceView: Directory!, sourceRootPath: String!): SdkTestTarget! { + SdkTestTarget( + workspaceView: workspaceView, + sourceRootPath: sourceRootPath, + daggerCliVersion: daggerCliVersion, + timeout: "10m", + ) + } + + """ + `dagger sdk install` should accept the SDK module. + """ + pub installRegistersSdk(ws: Workspace!): Void @check { + sdkTarget(ws).install.assertSuccess + } + + """ + `dagger sdk install` should mark the SDK with an as-sdk marker in dagger.toml. + """ + pub installMarksAsSdk(ws: Workspace!): Void @check { + let run = sdkTarget(ws).install + run.assertSuccess + if (run.workspaceFile("dagger.toml").contains("as-sdk") == false) { + raise "sdk install should record an as-sdk marker in dagger.toml" + } + } + + """ + `dagger module init ` should scaffold a new module. + """ + pub initScaffoldsModule(ws: Workspace!): Void @check { + sdkTarget(ws).initModule.assertSuccess + } + + """ + `dagger module init` should write the new module's dagger-module.toml. + """ + pub initWritesModuleConfig(ws: Workspace!): Void @check { + let testTarget = sdkTarget(ws) + let run = testTarget.initModule + run.assertSuccess + if (run.workspaceHasFile(testTarget.moduleConfigPath) == false) { + raise "module init should write " + testTarget.moduleConfigPath + } + } + + """ + `dagger module init` should install the new module in dagger.toml. + """ + pub initRegistersModule(ws: Workspace!): Void @check { + let testTarget = sdkTarget(ws) + let run = testTarget.initModule + run.assertSuccess + if (run.workspaceFile("dagger.toml").contains("[modules." + testTarget.moduleName + "]") == false) { + raise "module init should install the new module in dagger.toml" + } + } + + """ + `dagger module init` should record the SDK as the new module's authoring SDK. + """ + pub initRecordsAuthoringSdk(ws: Workspace!): Void @check { + let testTarget = sdkTarget(ws) + let run = testTarget.initModule + run.assertSuccess + if (run.workspaceFile("dagger.toml").contains(".as-sdk.modules]]") == false) { + raise "module init should record an as-sdk.modules authoring entry in dagger.toml" + } + } + + """ + `dagger generate` should succeed on a freshly scaffolded module. + """ + pub generateSucceeds(ws: Workspace!): Void @check { + sdkTarget(ws).generate.assertSuccess + } + + """ + A scaffolded module should serve at least one function after `dagger generate`. + """ + pub scaffoldedModuleServesFunctions(ws: Workspace!): Void @check { + let testTarget = sdkTarget(ws) + let run = testTarget.run(["api", "functions", testTarget.moduleName]) + run.assertSuccess + if (run.stdout.trimSuffix("\n") == "") { + raise "a scaffolded module should expose at least one function" + } + } + + """ + `dagger sdk module-options` should introspect the SDK's initModule capability. + """ + pub sdkReportsModuleOptions(ws: Workspace!): Void @check { + let testTarget = sdkTarget(ws) + testTarget.runInstalled(["sdk", "module-options", testTarget.sdkInstallName]).assertSuccess + } + + """ + `dagger module engine required` should report a version for a scaffolded module. + """ + pub engineRequiredReportsVersion(ws: Workspace!): Void @check { + let run = sdkTarget(ws).runInModule(["module", "engine", "required"]) + run.assertSuccess + if (run.stdout.trimSuffix("\n") == "") { + raise "module engine required should report a version" + } + } + + """ + `dagger module deps list` should succeed for a scaffolded module. + """ + pub depsListSucceeds(ws: Workspace!): Void @check { + sdkTarget(ws).runInModule(["module", "deps", "list"]).assertSuccess + } + + """ + Return the SDK module under test prepared by the workspace polyfill. + + When run inside the sdk-sdk repository itself, redirect to the checked-in + fixture SDK so the checks stay self-testing. + """ + let sdkTarget(ws: Workspace!): SdkTestTarget! { + let workspace = polyfill.workspace(ws) + let module = workspace.moduleSource(".") + + if (module.configExists == false) { + raise noSdkModuleMessage + } + + let resolved = if (selfModules.filter { name => name == module.config.name }.length > 0) { + workspace.moduleSource(selfFixturePath) + } else { + module + } + + target(resolved.workspaceView, resolved.sourceRootPath) + } + + let selfModules: [String!]! = ["sdk-sdk", "sdk-test"] + let selfFixturePath: String! = ".dagger/modules/sdk-test-e2e/fixtures/sdk-under-test" + let noSdkModuleMessage: String! = "no SDK module detected. Run from an SDK module workspace, or pass one with -W " +} + +""" +A Dagger SDK module under black-box test. + +The target vendors the SDK into a scratch git workspace inside a runner +container, installs a release Dagger CLI, then exercises the SDK through the +same commands a user would run. +""" +type SdkTestTarget { + let workspaceView: Directory! + let sourceRootPath: String! + let daggerCliVersion: String! + let timeout: String! + + """ + Workspace install name assigned to the SDK under test. + """ + pub sdkInstallName: String! = "sdk-under-test" + + """ + Name of the module scaffolded from the SDK under test. + """ + pub moduleName: String! = "test-mod" + + """ + Return a copy of this target with a different command timeout. + """ + pub withTimeout(timeout: String!): SdkTestTarget! { + SdkTestTarget( + workspaceView: workspaceView, + sourceRootPath: sourceRootPath, + daggerCliVersion: daggerCliVersion, + timeout: timeout, + ) + } + + """ + Workspace-relative path of the scaffolded module. + """ + pub modulePath: String! { + ".dagger/modules/" + moduleName + } + + """ + Workspace-relative path of the scaffolded module's config. + """ + pub moduleConfigPath: String! { + modulePath + "/dagger-module.toml" + } + + """ + Run `dagger sdk install` for the SDK under test and capture the result. + """ + pub install: SdkTestRun! { + runOn(workspace, "/work", installArgs) + } + + """ + Run `dagger module init` with the SDK under test and capture the result. + """ + pub initModule: SdkTestRun! { + runOn(installedState, "/work", initArgs) + } + + """ + Run `dagger generate` on the scaffolded module and capture the result. + """ + pub generate: SdkTestRun! { + runOn(initializedState, "/work", generateArgs) + } + + """ + Run a dagger command from the workspace root after the SDK is installed. + """ + pub runInstalled(args: [String!]!): SdkTestRun! { + runOn(installedState, "/work", args) + } + + """ + Run a dagger command from the workspace root after a module is scaffolded + and generated. + """ + pub run(args: [String!]!): SdkTestRun! { + runOn(generatedState, "/work", args) + } + + """ + Run a dagger command from the scaffolded module directory after generation. + """ + pub runInModule(args: [String!]!): SdkTestRun! { + runOn(generatedState, "/work/" + modulePath, args) + } + + """ + Scratch git workspace with the SDK under test vendored in. + """ + let workspace: Container! { + runner + .withDirectory("/work/" + vendorRoot, workspaceView, exclude: [".git", "**/.git"]) + .withWorkdir("/work") + .withExec(["git", "init", "-q"]) + .withExec(["git", "add", "-A"]) + .withExec([ + "git", + "-c", "user.email=sdk-test@dagger.io", + "-c", "user.name=sdk-test", + "commit", "-q", "--allow-empty", "-m", "sdk-test workspace", + ]) + } + + """ + Workspace state after `dagger sdk install`. + """ + let installedState: Container! { + step(workspace, installArgs) + } + + """ + Workspace state after `dagger module init`. + """ + let initializedState: Container! { + step(installedState, initArgs) + } + + """ + Workspace state after `dagger generate`. + """ + let generatedState: Container! { + step(initializedState, generateArgs) + } + + """ + Workspace-relative path of the vendored SDK module source. + """ + let sdkPath: String! { + if (sourceRootPath == ".") { vendorRoot } else { vendorRoot + "/" + sourceRootPath } + } + + let installArgs: [String!]! { + ["sdk", "install", "--name", sdkInstallName, "./" + sdkPath] + } + + let initArgs: [String!]! { + ["module", "init", sdkInstallName, moduleName] + } + + let generateArgs: [String!]! { + ["generate"] + } + + """ + Run a required dagger setup step, failing the pipeline on error. + """ + let step(state: Container!, args: [String!]!): Container! { + state.withExec( + [ + "sh", + "-c", + "timeout_arg=$1; shift 1; exec timeout \"$timeout_arg\" /usr/local/bin/dagger --progress plain -y \"$@\"", + "sdk-test-step", + timeout, + ] + args, + experimentalPrivilegedNesting: true, + ) + } + + """ + Run a dagger command and capture stdout, stderr, and exit code. + """ + let runOn(state: Container!, workdir: String!, args: [String!]!): SdkTestRun! { + SdkTestRun( + container: state.withWorkdir(workdir).withExec( + [ + "sh", + "-c", + "mkdir -p /tmp/sdk-test; timeout_arg=$1; shift 1; timeout \"$timeout_arg\" /usr/local/bin/dagger --progress plain -y \"$@\" > /tmp/sdk-test/stdout 2> /tmp/sdk-test/stderr; code=$?; printf '%s' \"$code\" > /tmp/sdk-test/exit-code; exit 0", + "sdk-test-run", + timeout, + ] + args, + experimentalPrivilegedNesting: true, + ), + args: args, + ) + } + + """ + Container with a release Dagger CLI and git installed. + """ + let runner: Container! { + container(platform: runnerPlatform) + .from("alpine:3.22") + .withoutEntrypoint + .withExec(["apk", "add", "--no-cache", "git"]) + .withFile("/tmp/" + daggerCliArchiveName, daggerCliArchive) + .withFile("/tmp/checksums.txt", daggerCliChecksums) + .withExec([ + "sh", + "-c", + "archive=$1; cd /tmp; line=$(awk -v f=\"$archive\" '$2 == f { print }' checksums.txt); test -n \"$line\"; printf '%s\n' \"$line\" | sha256sum -c -; tar -xzf \"$archive\" -C /usr/local/bin dagger; chmod +x /usr/local/bin/dagger", + "install-dagger-cli", + daggerCliArchiveName, + ]) + } + + let vendorRoot: String! = "vendor/sdk-workspace" + + """ + Resolved Dagger CLI release version without a leading "v". + """ + let daggerCliReleaseVersion: String! { + if (daggerCliVersion == "latest") { + http(url: "https://dl.dagger.io/dagger/versions/latest").contents.trimSuffix("\n").trimPrefix("v") + } else { + daggerCliVersion.trimPrefix("v") + } + } + + """ + Dagger CLI release archive name for the runner platform. + """ + let daggerCliArchiveName: String! { + "dagger_v" + daggerCliReleaseVersion + "_" + runnerOS + "_" + runnerArch + ".tar.gz" + } + + """ + Dagger CLI release archive. + """ + let daggerCliArchive: File! { + http(url: daggerCliReleaseURL + "/" + daggerCliArchiveName) + } + + """ + Dagger CLI release checksum file. + """ + let daggerCliChecksums: File! { + http(url: daggerCliReleaseURL + "/checksums.txt") + } + + """ + Dagger CLI release URL. + """ + let daggerCliReleaseURL: String! { + "https://dl.dagger.io/dagger/releases/" + daggerCliReleaseVersion + } + + let runnerPlatform: Platform! = "linux/amd64" + let runnerOS: String! = "linux" + let runnerArch: String! = "amd64" +} + +""" +Result of a black-box dagger CLI command against an SDK under test. +""" +type SdkTestRun { + let container: Container! + let args: [String!]! + + """ + The command stdout. + """ + pub stdout: String! { + container.file("/tmp/sdk-test/stdout").contents + } + + """ + The command stderr. + """ + pub stderr: String! { + container.file("/tmp/sdk-test/stderr").contents + } + + """ + The command exit code as decimal text. + """ + pub exitCode: String! { + container.file("/tmp/sdk-test/exit-code").contents + } + + """ + Whether the command exited successfully. + """ + pub succeeded: Boolean! { + exitCode == "0" + } + + """ + Fail when the command did not exit successfully. + """ + pub assertSuccess: Void { + if (succeeded == false) { + raise "dagger command failed: " + toJSON(args) + "\nstderr:\n" + stderr + } + null + } + + """ + Fail when the command exited successfully. + """ + pub assertFailure: Void { + if (succeeded) { + raise "dagger command unexpectedly succeeded: " + toJSON(args) + } + null + } + + """ + Read a workspace file after the command ran. + """ + pub workspaceFile(path: String!): String! { + container.file("/work/" + path).contents + } + + """ + Return true when a workspace file exists after the command ran. + """ + pub workspaceHasFile(path: String!): Boolean! { + container.directory("/work").exists(path) + } +} From 7898cd91a2b8607b5b83a8cee832e1f3925e99d7 Mon Sep 17 00:00:00 2001 From: kpenfound Date: Tue, 28 Jul 2026 13:29:30 -0400 Subject: [PATCH 2/6] feat!: replace root module with the black-box SDK test harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root sdk-sdk module now hosts the black-box lifecycle checks that previously lived in ./sdk-test, alongside the function-level contract checks and the Dang SDK helper scaffolding carried forward from the previous root module: dagger -m github.com/dagger/sdk-sdk -W check sdk-sdk now fulfills the CLI 1.0 SDK contract itself (targetRuntime, initModule, a @generate hook), so `dagger check` in this repository exercises every check against sdk-sdk as the SDK under test — no fixtures needed. The contract checks were aligned with how the engine actually drives SDKs: initModule is always called with an explicit --path, and the @generate hook is detected via `dagger generate -l` rather than by function name. The root config is now dagger-module.toml (engineVersion v1.0.0-0) with a dagger.toml workspace config, ./polyfill is replaced by the canonical github.com/dagger/polyfill, and the polyfill-e2e and fixture modules are removed. Verified green: 21/21 checks in-repo and 18/18 against a dang-sdk clone on a v1.0.0-beta.7 CLI/engine. Co-Authored-By: Claude Fable 5 Signed-off-by: kpenfound --- .dagger/modules/mod-test-e2e/dagger.json | 2 +- .dagger/modules/polyfill-e2e/dagger.json | 13 - .../fixtures/config/app/dagger.json | 17 - .../fixtures/config/dep-named/dagger.json | 7 - .../config/dep-source-only/dagger.json | 7 - .../fixtures/config/dep-string/dagger.json | 7 - .../polyfill-e2e/fixtures/fork/dir/old.txt | 1 - .../polyfill-e2e/fixtures/fork/existing.txt | 1 - .../fixtures/generate/app/dagger.json | 7 - .../fixtures/generate/app/main.go | 7 - .../fixtures/update/app/dagger.json | 14 - .../fixtures/update/dep-local/dagger.json | 7 - .dagger/modules/polyfill-e2e/main.dang | 297 --------- .../fixtures/sdk-helper/dagger.json | 7 - .../sdk-sdk-e2e/fixtures/sdk-helper/main.dang | 39 -- .../fixtures/sdk-under-test/dagger.json | 7 - .../fixtures/sdk-under-test/main.dang | 98 --- README.md | 73 ++- dagger-module.toml | 13 + dagger.json | 17 - dagger.toml | 9 + polyfill/dagger.json | 7 - .../module-config-update-dependencies/go.mod | 25 - .../module-config-update-dependencies/go.sum | 55 -- .../module-config-update-dependencies/main.go | 215 ------- .../main_test.go | 100 --- .../helpers/workspace-module-generate/go.mod | 28 - .../helpers/workspace-module-generate/go.sum | 57 -- .../helpers/workspace-module-generate/main.go | 549 ---------------- .../workspace-module-generate/main_test.go | 141 ---- polyfill/helpers/workspace-snapshot/go.mod | 25 - polyfill/helpers/workspace-snapshot/go.sum | 55 -- polyfill/helpers/workspace-snapshot/main.go | 173 ----- .../helpers/workspace-snapshot/main_test.go | 23 - polyfill/module-config.dang | 374 ----------- polyfill/module-source.dang | 222 ------- polyfill/polyfill.dang | 14 - polyfill/workspace-fork.dang | 247 ------- polyfill/workspace.dang | 76 --- sdk-sdk.dang | 602 ++++++++++++++++-- sdk-test/README.md | 51 -- sdk-test/dagger-module.toml | 9 - sdk-test/sdk-test.dang | 481 -------------- 43 files changed, 611 insertions(+), 3568 deletions(-) delete mode 100644 .dagger/modules/polyfill-e2e/dagger.json delete mode 100644 .dagger/modules/polyfill-e2e/fixtures/config/app/dagger.json delete mode 100644 .dagger/modules/polyfill-e2e/fixtures/config/dep-named/dagger.json delete mode 100644 .dagger/modules/polyfill-e2e/fixtures/config/dep-source-only/dagger.json delete mode 100644 .dagger/modules/polyfill-e2e/fixtures/config/dep-string/dagger.json delete mode 100644 .dagger/modules/polyfill-e2e/fixtures/fork/dir/old.txt delete mode 100644 .dagger/modules/polyfill-e2e/fixtures/fork/existing.txt delete mode 100644 .dagger/modules/polyfill-e2e/fixtures/generate/app/dagger.json delete mode 100644 .dagger/modules/polyfill-e2e/fixtures/generate/app/main.go delete mode 100644 .dagger/modules/polyfill-e2e/fixtures/update/app/dagger.json delete mode 100644 .dagger/modules/polyfill-e2e/fixtures/update/dep-local/dagger.json delete mode 100644 .dagger/modules/polyfill-e2e/main.dang delete mode 100644 .dagger/modules/sdk-sdk-e2e/fixtures/sdk-helper/dagger.json delete mode 100644 .dagger/modules/sdk-sdk-e2e/fixtures/sdk-helper/main.dang delete mode 100644 .dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/dagger.json delete mode 100644 .dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/main.dang create mode 100644 dagger-module.toml delete mode 100644 dagger.json create mode 100644 dagger.toml delete mode 100644 polyfill/dagger.json delete mode 100644 polyfill/helpers/module-config-update-dependencies/go.mod delete mode 100644 polyfill/helpers/module-config-update-dependencies/go.sum delete mode 100644 polyfill/helpers/module-config-update-dependencies/main.go delete mode 100644 polyfill/helpers/module-config-update-dependencies/main_test.go delete mode 100644 polyfill/helpers/workspace-module-generate/go.mod delete mode 100644 polyfill/helpers/workspace-module-generate/go.sum delete mode 100644 polyfill/helpers/workspace-module-generate/main.go delete mode 100644 polyfill/helpers/workspace-module-generate/main_test.go delete mode 100644 polyfill/helpers/workspace-snapshot/go.mod delete mode 100644 polyfill/helpers/workspace-snapshot/go.sum delete mode 100644 polyfill/helpers/workspace-snapshot/main.go delete mode 100644 polyfill/helpers/workspace-snapshot/main_test.go delete mode 100644 polyfill/module-config.dang delete mode 100644 polyfill/module-source.dang delete mode 100644 polyfill/polyfill.dang delete mode 100644 polyfill/workspace-fork.dang delete mode 100644 polyfill/workspace.dang delete mode 100644 sdk-test/README.md delete mode 100644 sdk-test/dagger-module.toml delete mode 100644 sdk-test/sdk-test.dang diff --git a/.dagger/modules/mod-test-e2e/dagger.json b/.dagger/modules/mod-test-e2e/dagger.json index 813c717..cb6bb57 100644 --- a/.dagger/modules/mod-test-e2e/dagger.json +++ b/.dagger/modules/mod-test-e2e/dagger.json @@ -11,7 +11,7 @@ }, { "name": "polyfill", - "source": "../../../polyfill" + "source": "github.com/dagger/polyfill@main" } ] } diff --git a/.dagger/modules/polyfill-e2e/dagger.json b/.dagger/modules/polyfill-e2e/dagger.json deleted file mode 100644 index 914d67b..0000000 --- a/.dagger/modules/polyfill-e2e/dagger.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "polyfill-e2e", - "engineVersion": "v0.20.8", - "sdk": { - "source": "dang" - }, - "dependencies": [ - { - "name": "polyfill", - "source": "../../../polyfill" - } - ] -} diff --git a/.dagger/modules/polyfill-e2e/fixtures/config/app/dagger.json b/.dagger/modules/polyfill-e2e/fixtures/config/app/dagger.json deleted file mode 100644 index bf2b575..0000000 --- a/.dagger/modules/polyfill-e2e/fixtures/config/app/dagger.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "config-app", - "engineVersion": "v0.20.8", - "sdk": { - "source": "dang" - }, - "dependencies": [ - "../dep-string", - { - "name": "dep-named", - "source": "../dep-named" - }, - { - "source": "../dep-source-only" - } - ] -} diff --git a/.dagger/modules/polyfill-e2e/fixtures/config/dep-named/dagger.json b/.dagger/modules/polyfill-e2e/fixtures/config/dep-named/dagger.json deleted file mode 100644 index 0315352..0000000 --- a/.dagger/modules/polyfill-e2e/fixtures/config/dep-named/dagger.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "dep-named", - "engineVersion": "v0.20.8", - "sdk": { - "source": "dang" - } -} diff --git a/.dagger/modules/polyfill-e2e/fixtures/config/dep-source-only/dagger.json b/.dagger/modules/polyfill-e2e/fixtures/config/dep-source-only/dagger.json deleted file mode 100644 index b9acb6d..0000000 --- a/.dagger/modules/polyfill-e2e/fixtures/config/dep-source-only/dagger.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "dep-source-only", - "engineVersion": "v0.20.8", - "sdk": { - "source": "dang" - } -} diff --git a/.dagger/modules/polyfill-e2e/fixtures/config/dep-string/dagger.json b/.dagger/modules/polyfill-e2e/fixtures/config/dep-string/dagger.json deleted file mode 100644 index 99dd064..0000000 --- a/.dagger/modules/polyfill-e2e/fixtures/config/dep-string/dagger.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "dep-string", - "engineVersion": "v0.20.8", - "sdk": { - "source": "dang" - } -} diff --git a/.dagger/modules/polyfill-e2e/fixtures/fork/dir/old.txt b/.dagger/modules/polyfill-e2e/fixtures/fork/dir/old.txt deleted file mode 100644 index 3367afd..0000000 --- a/.dagger/modules/polyfill-e2e/fixtures/fork/dir/old.txt +++ /dev/null @@ -1 +0,0 @@ -old diff --git a/.dagger/modules/polyfill-e2e/fixtures/fork/existing.txt b/.dagger/modules/polyfill-e2e/fixtures/fork/existing.txt deleted file mode 100644 index cbaf024..0000000 --- a/.dagger/modules/polyfill-e2e/fixtures/fork/existing.txt +++ /dev/null @@ -1 +0,0 @@ -existing diff --git a/.dagger/modules/polyfill-e2e/fixtures/generate/app/dagger.json b/.dagger/modules/polyfill-e2e/fixtures/generate/app/dagger.json deleted file mode 100644 index 09ca387..0000000 --- a/.dagger/modules/polyfill-e2e/fixtures/generate/app/dagger.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "generate-app", - "engineVersion": "v0.20.8", - "sdk": { - "source": "go" - } -} diff --git a/.dagger/modules/polyfill-e2e/fixtures/generate/app/main.go b/.dagger/modules/polyfill-e2e/fixtures/generate/app/main.go deleted file mode 100644 index 16213d2..0000000 --- a/.dagger/modules/polyfill-e2e/fixtures/generate/app/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -type GenerateApp struct{} - -func (m *GenerateApp) Hello() string { - return "hello" -} diff --git a/.dagger/modules/polyfill-e2e/fixtures/update/app/dagger.json b/.dagger/modules/polyfill-e2e/fixtures/update/app/dagger.json deleted file mode 100644 index 80a607b..0000000 --- a/.dagger/modules/polyfill-e2e/fixtures/update/app/dagger.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "update-app", - "engineVersion": "v0.20.8", - "sdk": { - "source": "dang" - }, - "dependencies": [ - "../dep-local", - { - "name": "hello", - "source": "github.com/shykes/daggerverse/hello" - } - ] -} diff --git a/.dagger/modules/polyfill-e2e/fixtures/update/dep-local/dagger.json b/.dagger/modules/polyfill-e2e/fixtures/update/dep-local/dagger.json deleted file mode 100644 index e86ce98..0000000 --- a/.dagger/modules/polyfill-e2e/fixtures/update/dep-local/dagger.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "dep-local", - "engineVersion": "v0.20.8", - "sdk": { - "source": "dang" - } -} diff --git a/.dagger/modules/polyfill-e2e/main.dang b/.dagger/modules/polyfill-e2e/main.dang deleted file mode 100644 index b27b0ec..0000000 --- a/.dagger/modules/polyfill-e2e/main.dang +++ /dev/null @@ -1,297 +0,0 @@ -""" -End-to-end checks for the polyfill module. -""" -type PolyfillE2e { - let fixtureRoot: String! = ".dagger/modules/polyfill-e2e/fixtures" - let outputRoot: String! = ".dagger/modules/polyfill-e2e/out" - - let configModulePath: String! = fixtureRoot + "/config/app" - let forkFilePath: String! = fixtureRoot + "/fork/existing.txt" - let forkDirPath: String! = fixtureRoot + "/fork/dir" - let generateModulePath: String! = fixtureRoot + "/generate/app" - let updateModulePath: String! = fixtureRoot + "/update/app" - - """ - Fail the current check when a condition is false. - """ - let assert(condition: Boolean!, message: String!): Void { - if (condition == false) { - raise message - } - null - } - - """ - Return true when a list contains the exact string. - """ - let contains(values: [String!]!, want: String!): Boolean! { - values - .filter { value => value == want } - .length > 0 - } - - """ - Assert that a changeset added a path. - """ - let assertAdded(changes: Changeset!, path: String!): Void { - assert(contains(changes.addedPaths, path), "expected added path: " + path) - } - - """ - Assert that a changeset modified a path. - """ - let assertModified(changes: Changeset!, path: String!): Void { - assert(contains(changes.modifiedPaths, path), "expected modified path: " + path) - } - - """ - Assert that a changeset removed a path. - """ - let assertRemoved(changes: Changeset!, path: String!): Void { - assert(contains(changes.removedPaths, path), "expected removed path: " + path) - } - - """ - Assert that a string contains a substring. - """ - let assertContains(value: String!, want: String!, message: String!): Void { - assert(value.contains(want), message) - } - - """ - Assert that a string does not contain a substring. - """ - let assertNotContains(value: String!, want: String!, message: String!): Void { - assert(value.contains(want) == false, message) - } - - """ - Assert that a config edit modified only the given dagger.json. - """ - let assertOnlyModuleConfigChanged(changes: Changeset!, path: String!): Void { - assertModified(changes, path + "/dagger.json") - assert(changes.modifiedPaths.length == 1, "config edit modified more than dagger.json") - assert(changes.addedPaths.length == 0, "config edit added paths") - assert(changes.removedPaths.length == 0, "config edit removed paths") - } - - """ - Assert that a config edit modified only the default config fixture dagger.json. - """ - let assertOnlyConfigChanged(changes: Changeset!): Void { - assertOnlyModuleConfigChanged(changes, configModulePath) - } - - """ - Explicit workspace projections should load the module config. - """ - pub workspaceProjectionCheck(ws: Workspace!): Void @check { - let explicit = polyfill.workspace(ws).moduleSource(configModulePath).config.requiredEngineVersion - - assert(explicit == "0.20.8", "explicit workspace projection read the wrong engine version") - - null - } - - """ - Workspace forks should classify new, existing file, and existing directory edits. - """ - pub workspaceForkEditCheck(ws: Workspace!): Void @check { - let salt = cloud.traceURL - let pws = polyfill.workspace(ws) - let addedPath = outputRoot + "/fork/new.txt" - let added = pws - .fork - .withNewFile(addedPath, "new\n" + salt) - .changes - - assertAdded(added, addedPath) - assert(added.modifiedPaths.length == 0, "new file edit unexpectedly modified paths") - assert(added.layer.file(addedPath).contents.contains(salt), "new file edit wrote the wrong contents") - - let replaced = pws - .fork - .withNewFile(forkFilePath, "replaced\n" + salt) - .changes - - assertModified(replaced, forkFilePath) - assert(replaced.addedPaths.length == 0, "existing file edit should not add paths") - assertContains(replaced.layer.file(forkFilePath).contents, "replaced", "existing file edit wrote the wrong contents") - - let replacementFile = directory - .withNewFile("replacement.txt", "file\n" + salt) - .file("replacement.txt") - let replacedFile = pws - .fork - .withFile(forkFilePath, replacementFile) - .changes - - assertModified(replacedFile, forkFilePath) - assert(replacedFile.addedPaths.length == 0, "existing file source edit should not add paths") - assertContains(replacedFile.layer.file(forkFilePath).contents, "file", "existing file source edit wrote the wrong contents") - - let replacementDir = directory - .withNewFile("old.txt", "updated\n" + salt) - .withNewFile("new.txt", "created\n" + salt) - let replacedDir = pws - .fork - .withDirectory(forkDirPath, replacementDir) - .changes - - assertModified(replacedDir, forkDirPath + "/old.txt") - assertAdded(replacedDir, forkDirPath + "/new.txt") - - let diffPath = outputRoot + "/fork/diff" - let before = directory.withNewFile("old.txt", "old\n" + salt) - let after = directory.withNewFile("new.txt", "new\n" + salt) - let diff = pws - .fork - .withDirectoryDiff(diffPath, before, after) - .changes - - assertAdded(diff, diffPath + "/new.txt") - assertRemoved(diff, diffPath + "/old.txt") - - let merged = pws - .fork - .withNewFile(outputRoot + "/fork/one.txt", "one\n" + salt) - .merge(pws.fork.withNewFile(outputRoot + "/fork/two.txt", "two\n" + salt)) - .changes - - assertAdded(merged, outputRoot + "/fork/one.txt") - assertAdded(merged, outputRoot + "/fork/two.txt") - - null - } - - """ - Module config helpers should list and edit dagger.json dependencies. - """ - pub moduleConfigDependencyCheck(ws: Workspace!): Void @check { - let pws = polyfill.workspace(ws) - let deps = pws.moduleSource(configModulePath).config.dependencies - - assert(contains(deps, "../dep-string"), "string dependency source was not listed") - assert(contains(deps, "dep-named"), "named dependency was not listed") - assert(contains(deps, "../dep-source-only"), "source-only dependency was not listed") - - let added = pws - .moduleSource(configModulePath) - .config - .withDependency("../dep-new") - .fork - .changes - assertOnlyConfigChanged(added) - assertContains(added.after.file(configModulePath + "/dagger.json").contents, "\"../dep-new\"", "unnamed dependency was not added") - - let named = pws - .moduleSource(configModulePath) - .config - .withDependency("../dep-new", name: "dep-alias") - .fork - .changes - assertOnlyConfigChanged(named) - assertContains(named.after.file(configModulePath + "/dagger.json").contents, "\"name\": \"dep-alias\"", "named dependency was not added") - assertContains(named.after.file(configModulePath + "/dagger.json").contents, "\"source\": \"../dep-new\"", "named dependency source was not kept") - - let removed = pws - .moduleSource(configModulePath) - .config - .withoutDependency("dep-named") - .fork - .changes - assertOnlyConfigChanged(removed) - assertNotContains(removed.after.file(configModulePath + "/dagger.json").contents, "\"name\": \"dep-named\"", "named dependency was not removed") - - null - } - - """ - Updating local-only dependencies should return an empty config fork. - """ - pub moduleConfigUpdateCheck(ws: Workspace!): Void @check { - let changes = polyfill.workspace(ws) - .moduleSource(configModulePath) - .config - .withUpdatedDependencies - .fork - .changes - - assert(changes.isEmpty, "updating local-only dependencies should not edit dagger.json") - - null - } - - """ - Updating remote dependencies should rewrite only the remote entries. - """ - pub moduleConfigRemoteUpdateCheck(ws: Workspace!): Void @check { - let changes = polyfill.workspace(ws) - .moduleSource(updateModulePath) - .config - .withUpdatedDependencies("hello") - .fork - .changes - - assertOnlyModuleConfigChanged(changes, updateModulePath) - let updated = changes.after.file(updateModulePath + "/dagger.json").contents - assertContains(updated, "\"../dep-local\"", "local dependency was not preserved") - assertContains(updated, "\"name\": \"hello\"", "remote dependency name was not preserved") - assertContains(updated, "\"source\": \"github.com/shykes/daggerverse/hello@main\"", "remote dependency source was not normalized") - assertContains(updated, "\"pin\": \"", "remote dependency was not pinned") - - null - } - - """ - Module config engine helpers should read and normalize engine versions. - """ - pub moduleConfigEngineCheck(ws: Workspace!): Void @check { - let config = polyfill.workspace(ws).moduleSource(configModulePath).config - let fixed = config.withRequiredEngineVersion("0.21.0").fork.changes - let latest = config.withLatestEngineVersion.fork.changes - - assert(config.requiredEngineVersion == "0.20.8", "required engine version should trim a leading v") - assertOnlyConfigChanged(fixed) - assertContains(fixed.after.file(configModulePath + "/dagger.json").contents, "\"engineVersion\": \"v0.21.0\"", "fixed engine version was not normalized") - assertOnlyConfigChanged(latest) - assertContains(latest.after.file(configModulePath + "/dagger.json").contents, "\"engineVersion\": \"latest\"", "latest engine version was not written") - - null - } - - """ - Config forks should remain composable with extra workspace edits. - """ - pub moduleConfigForkChainingCheck(ws: Workspace!): Void @check { - let changes = polyfill.workspace(ws) - .moduleSource(configModulePath) - .config - .withRequiredEngineVersion("0.21.0") - .fork - .withNewFile(forkFilePath, "chained\n" + cloud.traceURL) - .changes - - assertModified(changes, configModulePath + "/dagger.json") - assertModified(changes, forkFilePath) - assert(changes.addedPaths.length == 0, "chained config fork should not add existing files") - - null - } - - """ - Module source generation should return generated context changes. - """ - pub moduleSourceGenerateCheck(ws: Workspace!): Void @check { - let changes = polyfill.workspace(ws) - .moduleSource(generateModulePath) - .generate - .changes - - assertAdded(changes, generateModulePath + "/dagger.gen.go") - assertAdded(changes, generateModulePath + "/go.mod") - assertContains(changes.layer.file(generateModulePath + "/dagger.gen.go").contents, "Code generated by dagger.", "generated context did not include dagger.gen.go") - - null - } -} diff --git a/.dagger/modules/sdk-sdk-e2e/fixtures/sdk-helper/dagger.json b/.dagger/modules/sdk-sdk-e2e/fixtures/sdk-helper/dagger.json deleted file mode 100644 index 20ef5ea..0000000 --- a/.dagger/modules/sdk-sdk-e2e/fixtures/sdk-helper/dagger.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "sdk-helper-fixture", - "engineVersion": "v0.20.8", - "sdk": { - "source": "dang" - } -} diff --git a/.dagger/modules/sdk-sdk-e2e/fixtures/sdk-helper/main.dang b/.dagger/modules/sdk-sdk-e2e/fixtures/sdk-helper/main.dang deleted file mode 100644 index 20a0b89..0000000 --- a/.dagger/modules/sdk-sdk-e2e/fixtures/sdk-helper/main.dang +++ /dev/null @@ -1,39 +0,0 @@ -""" -Minimal SDK helper fixture used to run sdk-sdk's black-box checks end-to-end. - -Implements the CLI-1.0 SDK authoring contract: `initModule` plus a `@generate` -hook. Client generation (`initClient`) is intentionally absent. -""" -type SdkHelperFixture { - """ - Stage SDK-specific seed files for a new module at `path`. - - Returns only this SDK's files; the engine owns dagger-module.toml and - workspace config. - """ - pub initModule(ws: Workspace!, name: String!, path: String! = ""): Changeset! { - let modulePath = if (path == "") { - ".dagger/modules/" + name - } else { - path - } - let seedPath = if (modulePath == "." or modulePath == "") { - "main.dang" - } else { - modulePath.trimSuffix("/") + "/main.dang" - } - - directory - .withNewFile(seedPath, "\"\"\"\n" + name + " (fixture seed)\n\"\"\"\ntype Main {\n}\n") - .changes(directory) - } - - """ - Regenerate the modules this SDK manages. - - Placeholder no-op: returns an empty Changeset. - """ - pub generate: Changeset! @generate { - directory.changes(directory) - } -} diff --git a/.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/dagger.json b/.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/dagger.json deleted file mode 100644 index 944850a..0000000 --- a/.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/dagger.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "sdk-under-test", - "engineVersion": "v0.20.8", - "sdk": { - "source": "dang" - } -} diff --git a/.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/main.dang b/.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/main.dang deleted file mode 100644 index 4a71f75..0000000 --- a/.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test/main.dang +++ /dev/null @@ -1,98 +0,0 @@ -""" -Minimal SDK module fixture used to run sdk-test's black-box checks end-to-end. - -Implements the CLI 1.0 SDK contract: `targetRuntime`, `initModule`, and `mod`. -Scaffolded modules run on the built-in Dang runtime. The starter source always -declares a `TestMod` root type, so the fixture only supports initializing a -module named "test-mod" — the name sdk-test uses. -""" -type SdkUnderTest { - """ - Engine runtime recorded for modules created with this SDK. - """ - targetRuntime: String! { - "dang" - } - - """ - Scaffold a new module for `dagger module init sdk-under-test `. - """ - initModule( - ws: Workspace!, - name: String!, - path: String!, - template: String! = "", - ): Changeset! { - let modPath = if (path == "" or path == ".") { "." } else { path.trimSuffix("/") } - - directory - .withNewFile(modPath + "/main.dang", starterSource) - .changes(directory) - } - - """ - Return the SDK module at a workspace path. - """ - mod(ws: Workspace!, path: String! = ".", findUp: Boolean! = true): SdkUnderTestMod! { - SdkUnderTestMod(modulePath: path) - } - - """ - Starter source for a module named "test-mod". - """ - let starterSource: String! { - "\"\"\"\nStarter module generated by the sdk-under-test fixture.\n\"\"\"\ntype TestMod {\n \"\"\"\n Return a greeting from the fixture starter module.\n \"\"\"\n pub hello: String! {\n \"hello from the fixture SDK\"\n }\n}\n" - } -} - -""" -Minimal module handle for the fixture SDK. -""" -type SdkUnderTestMod { - let modulePath: String! - - """ - Module path relative to the current workspace. - """ - pub path: String! { - modulePath - } - - """ - Dependency manager. - """ - pub deps: SdkUnderTestDeps! { - SdkUnderTestDeps() - } - - """ - Engine manager. - """ - pub engine: SdkUnderTestEngine! { - SdkUnderTestEngine() - } -} - -""" -Minimal dependency manager for the fixture SDK. -""" -type SdkUnderTestDeps { - """ - Return configured dependency names. - """ - pub list: [String!]! { - [] - } -} - -""" -Minimal engine manager for the fixture SDK. -""" -type SdkUnderTestEngine { - """ - Return the configured engine version. - """ - pub required: String! { - "v1.0.0-beta.7" - } -} diff --git a/README.md b/README.md index 9326e32..95cc2ef 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,57 @@ # sdk-sdk -Shared contract checks for official SDK helper modules. +Black-box contract checks for Dagger SDK modules — such as +`github.com/dagger/go-sdk`, `github.com/dagger/dang-sdk`, +`github.com/dagger/typescript-sdk`, and `github.com/dagger/python-sdk` — plus +tooling to start a new SDK helper module. -For black-box lifecycle checks that drive an SDK module through the real CLI -(`dagger sdk install`, `dagger module init`, `dagger generate`), see -[`sdk-test`](./sdk-test): +## Checking an SDK + +Run the checks against an SDK repository: ```sh -dagger -m github.com/dagger/sdk-sdk/sdk-test -W check +dagger -m github.com/dagger/sdk-sdk -W check ``` -Start a new Dang SDK helper module: +The checks vendor the SDK module into a scratch git workspace inside a runner +container, install a release Dagger CLI, then drive the SDK through real CLI +commands the way a user would: -```sh -dagger module init sdk-sdk my-sdk -``` +- `dagger sdk install ./` registers the SDK and marks it `as-sdk` in + `dagger.toml`. +- `dagger module init test-mod` scaffolds a new module, writes its + `dagger-module.toml`, installs it in `dagger.toml`, and records the SDK as + the module's authoring SDK. +- `dagger generate` succeeds on the fresh scaffold. +- After generation the scaffolded module serves functions: + `dagger api functions test-mod`. +- `dagger sdk module-options ` introspects the SDK's `initModule` + capability. +- `dagger module engine required` and `dagger module deps list` work from the + scaffolded module directory. -The module name is the Dagger module name. The generated Dang root type is -derived from it, for example `my-sdk` becomes `MySdk`. +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 +write engine-owned config (`dagger.json` / `dagger-module.toml`), and must not +remove existing files. The SDK must also list a `@generate` hook in +`dagger generate -l`. -Run the checks from an SDK helper module workspace: +Configure the CLI release with the top-level `dagger-cli-version` setting; the +default is `1.0.0-beta.7`. Individual targets accept `with-timeout` for slow +SDKs (the default command timeout is `10m`). Custom checks can reuse the +harness through `target`: -```sh -cd ./my/sdk/repo -dagger -m github.com/dagger/sdk-sdk check +```dang +let testTarget = sdkSdk.target(module.workspaceView, module.sourceRootPath) +testTarget.install.assertSuccess +testTarget.runInModule(["module", "deps", "list"]).assertSuccess ``` -The checks receive the current `Workspace`, serve the SDK helper module from the -workspace, and exercise its user-facing behavior without applying the returned -changesets. +## The SDK contract Under CLI 1.0 the engine owns module bookkeeping — `dagger-module.toml`, -workspace config, and dependency and engine-version edits. An SDK helper module +workspace config, and dependency and engine-version edits. An SDK module implements only what is genuinely language-specific: - `initModule(ws, name, path): Changeset!` — seed the SDK's own files for a new @@ -45,3 +65,18 @@ is not required or exercised here. Changeset paths are workspace-root-relative. For example, `initModule` for a module named `my-sdk` is expected to seed files under `.dagger/modules/my-sdk/`. + +## Starting a new SDK + +sdk-sdk is itself an SDK for authoring Dang SDK helper modules: + +```sh +dagger sdk install github.com/dagger/sdk-sdk +dagger module init sdk-sdk my-sdk +``` + +The module name is the Dagger module name. The generated Dang root type is +derived from it, for example `my-sdk` becomes `MySdk`. + +Because sdk-sdk fulfills its own contract, running `dagger check` inside this +repository exercises every check against sdk-sdk itself. diff --git a/dagger-module.toml b/dagger-module.toml new file mode 100644 index 0000000..ab7a40c --- /dev/null +++ b/dagger-module.toml @@ -0,0 +1,13 @@ +name = "sdk-sdk" +engineVersion = "v1.0.0-0" + +[runtime] + source = "dang" + +[[dependencies]] + name = "polyfill" + source = "github.com/dagger/polyfill@main" + +[[dependencies]] + name = "mod-test" + source = "./mod-test" diff --git a/dagger.json b/dagger.json deleted file mode 100644 index bfb6e68..0000000 --- a/dagger.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "sdk-sdk", - "engineVersion": "v0.20.8", - "sdk": { - "source": "dang" - }, - "dependencies": [ - { - "name": "mod-test", - "source": "./mod-test" - }, - { - "name": "polyfill", - "source": "./polyfill" - } - ] -} diff --git a/dagger.toml b/dagger.toml new file mode 100644 index 0000000..edd4a9f --- /dev/null +++ b/dagger.toml @@ -0,0 +1,9 @@ +# Dagger workspace configuration + +[modules.sdk-sdk] +source = "." + +[modules.sdk-sdk.as-sdk] + +[modules.mod-test-e2e] +source = ".dagger/modules/mod-test-e2e" diff --git a/polyfill/dagger.json b/polyfill/dagger.json deleted file mode 100644 index 5a531dd..0000000 --- a/polyfill/dagger.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "polyfill", - "engineVersion": "v0.20.8", - "sdk": { - "source": "dang" - } -} diff --git a/polyfill/helpers/module-config-update-dependencies/go.mod b/polyfill/helpers/module-config-update-dependencies/go.mod deleted file mode 100644 index a4089d9..0000000 --- a/polyfill/helpers/module-config-update-dependencies/go.mod +++ /dev/null @@ -1,25 +0,0 @@ -module module-config-update-dependencies - -go 1.26.1 - -require dagger.io/dagger v0.21.3 - -require ( - github.com/99designs/gqlgen v0.17.89 // indirect - github.com/Khan/genqlient v0.8.1 // indirect - github.com/adrg/xdg v0.5.3 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/sosodev/duration v1.4.0 // indirect - github.com/vektah/gqlparser/v2 v2.5.32 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect -) diff --git a/polyfill/helpers/module-config-update-dependencies/go.sum b/polyfill/helpers/module-config-update-dependencies/go.sum deleted file mode 100644 index 398eaa6..0000000 --- a/polyfill/helpers/module-config-update-dependencies/go.sum +++ /dev/null @@ -1,55 +0,0 @@ -dagger.io/dagger v0.20.6 h1:Uac8tdqieoq+psVe5f+rhNk1sk2W8YK5t4OzpGyHuTI= -dagger.io/dagger v0.20.6/go.mod h1:ZXg8+pQZaZUC8rAw4V/gPP8aKvKARIJZ+pfcV+RC1es= -dagger.io/dagger v0.21.3 h1:Y18i1txUYNeILqqw2wZNeMRdKIK1/pYVnIEcttYvHAo= -dagger.io/dagger v0.21.3/go.mod h1:8hztpM9rKNjSmOa2nzd8ot6aw1RZFDWCawMfc5V9tXA= -github.com/99designs/gqlgen v0.17.89 h1:KzEcxPiMgQoMw3m/E85atUEHyZyt0PbAflMia5Kw8z8= -github.com/99designs/gqlgen v0.17.89/go.mod h1:GFqruTVGB7ZTdrf1uzOagpXbY7DrEt1pIxnTdhIbWvQ= -github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs= -github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU= -github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= -github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= -github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= -github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59 h1:g6vfdGRyz6fAjfHz5FyYPZgHy8qcQ31fHrBl1iCOzxw= -github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59/go.mod h1:jsdUJeYzcbyK1j/EqMGPrQgNYxl/Zfg06vvM9C/xXxs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= -github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= -github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/polyfill/helpers/module-config-update-dependencies/main.go b/polyfill/helpers/module-config-update-dependencies/main.go deleted file mode 100644 index 4c786ec..0000000 --- a/polyfill/helpers/module-config-update-dependencies/main.go +++ /dev/null @@ -1,215 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "os" - "path" - "path/filepath" - "strings" - - "dagger.io/dagger" -) - -const ( - workspaceIDEnv = "WORKSPACE_ID" - configContentsEnv = "MODULE_CONFIG_CONTENTS" - updatesJSONEnv = "MODULE_CONFIG_UPDATES_JSON" - defaultGitHeadValue = "ref: refs/heads/main\n" - mockRoot = "/mock" -) - -type configDependency struct { - Name string `json:"name"` - Source string `json:"source"` - Pin string `json:"pin,omitempty"` -} - -func main() { - if err := run(context.Background(), os.Args[1:]); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func run(ctx context.Context, args []string) error { - fs := flag.NewFlagSet("module-config-update-dependencies", flag.ContinueOnError) - fs.SetOutput(os.Stderr) - modulePath := fs.String("path", "", "workspace-root-relative module path") - outPath := fs.String("out", "/dependencies.json", "updated dependencies JSON output file") - if err := fs.Parse(args); err != nil { - return err - } - if fs.NArg() != 0 { - return fmt.Errorf("unexpected arguments: %v", fs.Args()) - } - if *modulePath == "" { - return fmt.Errorf("--path is required") - } - - workspaceID, err := envString(workspaceIDEnv) - if err != nil { - return err - } - contents, err := envString(configContentsEnv) - if err != nil { - return err - } - updates, err := updatesFromEnv() - if err != nil { - return err - } - - client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr)) - if err != nil { - return err - } - defer client.Close() - - workspace := dagger.Ref[*dagger.Workspace](client, dagger.ID(workspaceID)) - dependencies, err := updatedRemoteDependencies(ctx, client, workspace, *modulePath, contents, updates) - if err != nil { - return err - } - - encoded, err := json.MarshalIndent(dependencies, "", " ") - if err != nil { - return fmt.Errorf("marshal dependencies: %w", err) - } - if err := os.MkdirAll(filepath.Dir(*outPath), 0o755); err != nil { - return err - } - return os.WriteFile(*outPath, append(encoded, '\n'), 0o644) -} - -func updatedRemoteDependencies( - ctx context.Context, - client *dagger.Client, - workspace *dagger.Workspace, - modulePath string, - contents string, - updates []string, -) ([]configDependency, error) { - modulePath, err := cleanModulePath(modulePath) - if err != nil { - return nil, err - } - - mock := workspace. - Directory("/", dagger.WorkspaceDirectoryOpts{Include: []string{"**/dagger.json"}}). - WithNewFile(daggerJSONPath(modulePath), contents). - WithNewFile(".git/HEAD", defaultGitHeadValue) - if _, err := mock.Export(ctx, mockRoot); err != nil { - return nil, fmt.Errorf("export mock workspace: %w", err) - } - - dependencies, err := workspaceModuleSource(client, modulePath).WithUpdateDependencies(updates).Dependencies(ctx) - if err != nil { - return nil, err - } - - updated := []configDependency{} - for _, dependency := range dependencies { - fragment, ok, err := remoteDependency(ctx, &dependency) - if err != nil { - return nil, err - } - if ok { - updated = append(updated, fragment) - } - } - return updated, nil -} - -func workspaceModuleSource(client *dagger.Client, modulePath string) *dagger.ModuleSource { - return client.ModuleSource(mockSourcePath(modulePath), dagger.ModuleSourceOpts{ - DisableFindUp: true, - RequireKind: dagger.ModuleSourceKindLocalSource, - }) -} - -func remoteDependency(ctx context.Context, dependency *dagger.ModuleSource) (configDependency, bool, error) { - kind, err := dependency.Kind(ctx) - if err != nil { - return configDependency{}, false, err - } - - switch kind { - case dagger.ModuleSourceKindLocalSource: - return configDependency{}, false, nil - case dagger.ModuleSourceKindGitSource: - default: - return configDependency{}, false, fmt.Errorf("unsupported dependency kind in update response: %s", kind) - } - - name, err := dependency.ModuleName(ctx) - if err != nil { - return configDependency{}, false, err - } - source, err := dependency.AsString(ctx) - if err != nil { - return configDependency{}, false, err - } - pin, err := dependency.Pin(ctx) - if err != nil { - return configDependency{}, false, err - } - return configDependency{Name: name, Source: source, Pin: pin}, true, nil -} - -func updatesFromEnv() ([]string, error) { - raw := os.Getenv(updatesJSONEnv) - if raw == "" { - return nil, nil - } - - var updates []string - if err := json.Unmarshal([]byte(raw), &updates); err != nil { - return nil, fmt.Errorf("decode %s: %w", updatesJSONEnv, err) - } - return updates, nil -} - -func envString(name string) (string, error) { - raw := os.Getenv(name) - if raw == "" { - return "", fmt.Errorf("%s is not set", name) - } - - var decoded string - if err := json.Unmarshal([]byte(raw), &decoded); err == nil { - return decoded, nil - } - return raw, nil -} - -func cleanModulePath(p string) (string, error) { - if strings.HasPrefix(p, "/") { - return "", fmt.Errorf("module path must be relative: %s", p) - } - - p = path.Clean(p) - if p == "" { - return ".", nil - } - if p == ".." || strings.HasPrefix(p, "../") { - return "", fmt.Errorf("module path escapes workspace: %s", p) - } - return p, nil -} - -func daggerJSONPath(modulePath string) string { - if modulePath == "." { - return "dagger.json" - } - return path.Join(modulePath, "dagger.json") -} - -func mockSourcePath(modulePath string) string { - if modulePath == "." { - return mockRoot - } - return path.Join(mockRoot, modulePath) -} diff --git a/polyfill/helpers/module-config-update-dependencies/main_test.go b/polyfill/helpers/module-config-update-dependencies/main_test.go deleted file mode 100644 index 80674f5..0000000 --- a/polyfill/helpers/module-config-update-dependencies/main_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package main - -import ( - "os" - "reflect" - "testing" -) - -func TestCleanModulePath(t *testing.T) { - tests := []struct { - name string - in string - want string - wantErr bool - }{ - {name: "root", in: ".", want: "."}, - {name: "nested", in: "polyfill/.", want: "polyfill"}, - {name: "absolute", in: "/polyfill", wantErr: true}, - {name: "escape", in: "../polyfill", wantErr: true}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got, err := cleanModulePath(test.in) - if test.wantErr { - if err == nil { - t.Fatal("expected error") - } - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != test.want { - t.Fatalf("got %q, want %q", got, test.want) - } - }) - } -} - -func TestDaggerJSONPath(t *testing.T) { - tests := map[string]string{ - ".": "dagger.json", - "polyfill": "polyfill/dagger.json", - } - - for in, want := range tests { - if got := daggerJSONPath(in); got != want { - t.Fatalf("daggerJSONPath(%q) = %q, want %q", in, got, want) - } - } -} - -func TestMockSourcePath(t *testing.T) { - tests := map[string]string{ - ".": "/mock", - "polyfill": "/mock/polyfill", - } - - for in, want := range tests { - if got := mockSourcePath(in); got != want { - t.Fatalf("mockSourcePath(%q) = %q, want %q", in, got, want) - } - } -} - -func TestUpdatesFromEnv(t *testing.T) { - t.Setenv(updatesJSONEnv, `["one","two"]`) - - got, err := updatesFromEnv() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - want := []string{"one", "two"} - if !reflect.DeepEqual(got, want) { - t.Fatalf("got %#v, want %#v", got, want) - } -} - -func TestEnvString(t *testing.T) { - t.Setenv("TEST_STRING", `"decoded"`) - got, err := envString("TEST_STRING") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != "decoded" { - t.Fatalf("got %q, want decoded", got) - } - - if err := os.Setenv("TEST_STRING", `{"json":"object"}`); err != nil { - t.Fatal(err) - } - got, err = envString("TEST_STRING") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != `{"json":"object"}` { - t.Fatalf("got %q, want raw JSON object", got) - } -} diff --git a/polyfill/helpers/workspace-module-generate/go.mod b/polyfill/helpers/workspace-module-generate/go.mod deleted file mode 100644 index 2ac9b4c..0000000 --- a/polyfill/helpers/workspace-module-generate/go.mod +++ /dev/null @@ -1,28 +0,0 @@ -module workspace-module-generate - -go 1.26.1 - -require ( - dagger.io/dagger v0.21.3 - github.com/pelletier/go-toml v1.9.5 -) - -require ( - github.com/99designs/gqlgen v0.17.89 // indirect - github.com/Khan/genqlient v0.8.1 // indirect - github.com/adrg/xdg v0.5.3 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/sosodev/duration v1.4.0 // indirect - github.com/vektah/gqlparser/v2 v2.5.32 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect -) diff --git a/polyfill/helpers/workspace-module-generate/go.sum b/polyfill/helpers/workspace-module-generate/go.sum deleted file mode 100644 index 7c8ed19..0000000 --- a/polyfill/helpers/workspace-module-generate/go.sum +++ /dev/null @@ -1,57 +0,0 @@ -dagger.io/dagger v0.20.6 h1:Uac8tdqieoq+psVe5f+rhNk1sk2W8YK5t4OzpGyHuTI= -dagger.io/dagger v0.20.6/go.mod h1:ZXg8+pQZaZUC8rAw4V/gPP8aKvKARIJZ+pfcV+RC1es= -dagger.io/dagger v0.21.3 h1:Y18i1txUYNeILqqw2wZNeMRdKIK1/pYVnIEcttYvHAo= -dagger.io/dagger v0.21.3/go.mod h1:8hztpM9rKNjSmOa2nzd8ot6aw1RZFDWCawMfc5V9tXA= -github.com/99designs/gqlgen v0.17.89 h1:KzEcxPiMgQoMw3m/E85atUEHyZyt0PbAflMia5Kw8z8= -github.com/99designs/gqlgen v0.17.89/go.mod h1:GFqruTVGB7ZTdrf1uzOagpXbY7DrEt1pIxnTdhIbWvQ= -github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs= -github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU= -github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= -github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= -github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= -github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59 h1:g6vfdGRyz6fAjfHz5FyYPZgHy8qcQ31fHrBl1iCOzxw= -github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59/go.mod h1:jsdUJeYzcbyK1j/EqMGPrQgNYxl/Zfg06vvM9C/xXxs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= -github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= -github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/polyfill/helpers/workspace-module-generate/main.go b/polyfill/helpers/workspace-module-generate/main.go deleted file mode 100644 index e5ea372..0000000 --- a/polyfill/helpers/workspace-module-generate/main.go +++ /dev/null @@ -1,549 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path" - "path/filepath" - "sort" - "strings" - - "dagger.io/dagger" - toml "github.com/pelletier/go-toml" -) - -const workspaceIDEnv = "WORKSPACE_ID" - -type moduleSourceOptions struct { - ref string - cwd string - local bool - name string - root string - before string - after string - idOut string - viewOut string -} - -func main() { - if err := run(context.Background()); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func run(ctx context.Context) error { - opts, err := parseModuleSourceOptions(os.Args[1:], 1) - if err != nil { - return err - } - - client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr)) - if err != nil { - return err - } - defer client.Close() - - workspaceID, err := envString(workspaceIDEnv) - if err != nil { - return err - } - workspace := dagger.Ref[*dagger.Workspace](client, dagger.ID(workspaceID)) - - if opts.viewOut != "" { - view, err := moduleSourceWorkspaceView(ctx, workspace, opts) - if err != nil { - return err - } - if _, err := view.Export(ctx, opts.viewOut); err != nil { - return fmt.Errorf("export module source workspace view: %w", err) - } - return nil - } - - src, err := moduleSource(ctx, client, workspace, opts) - if err != nil { - return err - } - if opts.name != "" { - src = src.WithName(opts.name) - } - if opts.idOut != "" { - id, err := src.ID(ctx) - if err != nil { - return fmt.Errorf("resolve module source id: %w", err) - } - if err := os.MkdirAll(filepath.Dir(opts.idOut), 0o755); err != nil { - return err - } - return os.WriteFile(opts.idOut, []byte(id), 0o644) - } - - changes := src.GeneratedContextChangeset() - if _, err := changes.Before().Directory(opts.root).Export(ctx, opts.before); err != nil { - return fmt.Errorf("export generated context before directory: %w", err) - } - if _, err := changes.After().Directory(opts.root).Export(ctx, opts.after); err != nil { - return fmt.Errorf("export generated context after directory: %w", err) - } - return nil -} - -func parseModuleSourceOptions(args []string, wantPositionals int) (moduleSourceOptions, error) { - opts, rest, err := parseOptions(args) - if err != nil { - return opts, err - } - if len(rest) != wantPositionals { - return opts, fmt.Errorf("usage: workspace-module-generate REF [--cwd CWD] [--local] [--name NAME] [--root ROOT] [--before PATH] [--after PATH] [--id-out PATH] [--view-out PATH]") - } - opts.ref = rest[0] - if opts.root == "" { - opts.root = "." - } - if opts.before == "" { - opts.before = "/before" - } - if opts.after == "" { - opts.after = "/after" - } - return opts, nil -} - -func parseOptions(args []string) (moduleSourceOptions, []string, error) { - var opts moduleSourceOptions - var rest []string - for i := 0; i < len(args); i++ { - arg := args[i] - switch { - case arg == "--local": - opts.local = true - case arg == "--cwd": - i++ - if i >= len(args) { - return opts, nil, fmt.Errorf("--cwd requires a value") - } - opts.cwd = args[i] - case strings.HasPrefix(arg, "--cwd="): - opts.cwd = strings.TrimPrefix(arg, "--cwd=") - case arg == "--name": - i++ - if i >= len(args) { - return opts, nil, fmt.Errorf("--name requires a value") - } - opts.name = args[i] - case strings.HasPrefix(arg, "--name="): - opts.name = strings.TrimPrefix(arg, "--name=") - case arg == "--root": - i++ - if i >= len(args) { - return opts, nil, fmt.Errorf("--root requires a value") - } - opts.root = args[i] - case strings.HasPrefix(arg, "--root="): - opts.root = strings.TrimPrefix(arg, "--root=") - case arg == "--before": - i++ - if i >= len(args) { - return opts, nil, fmt.Errorf("--before requires a value") - } - opts.before = args[i] - case strings.HasPrefix(arg, "--before="): - opts.before = strings.TrimPrefix(arg, "--before=") - case arg == "--after": - i++ - if i >= len(args) { - return opts, nil, fmt.Errorf("--after requires a value") - } - opts.after = args[i] - case strings.HasPrefix(arg, "--after="): - opts.after = strings.TrimPrefix(arg, "--after=") - case arg == "--id-out": - i++ - if i >= len(args) { - return opts, nil, fmt.Errorf("--id-out requires a value") - } - opts.idOut = args[i] - case strings.HasPrefix(arg, "--id-out="): - opts.idOut = strings.TrimPrefix(arg, "--id-out=") - case arg == "--view-out": - i++ - if i >= len(args) { - return opts, nil, fmt.Errorf("--view-out requires a value") - } - opts.viewOut = args[i] - case strings.HasPrefix(arg, "--view-out="): - opts.viewOut = strings.TrimPrefix(arg, "--view-out=") - case strings.HasPrefix(arg, "-"): - return opts, nil, fmt.Errorf("unknown option: %s", arg) - default: - rest = append(rest, arg) - } - } - return opts, rest, nil -} - -func moduleSource( - ctx context.Context, - client *dagger.Client, - workspace *dagger.Workspace, - opts moduleSourceOptions, -) (*dagger.ModuleSource, error) { - cwd := opts.cwd - if cwd == "" { - var err error - cwd, err = currentWorkspacePath(ctx, workspace) - if err != nil { - return nil, err - } - } - - candidate, err := workspacePath(cwd, opts.ref) - if err != nil { - return nil, err - } - - local, err := workspaceDirectoryExists(ctx, workspace, candidate) - if err != nil { - return nil, err - } - if local { - include, err := workspaceModuleSourceInclude(ctx, workspace, candidate) - if err != nil { - return nil, err - } - return workspace. - Directory("/", dagger.WorkspaceDirectoryOpts{Include: include}). - AsModuleSource(dagger.DirectoryAsModuleSourceOpts{SourceRootPath: candidate}), nil - } - if opts.local || mustBeLocalRef(opts.ref) { - return nil, fmt.Errorf("local module source %q does not exist in workspace at %q", opts.ref, candidate) - } - - return client.ModuleSource(opts.ref, dagger.ModuleSourceOpts{ - DisableFindUp: true, - }), nil -} - -func moduleSourceWorkspaceView( - ctx context.Context, - workspace *dagger.Workspace, - opts moduleSourceOptions, -) (*dagger.Directory, error) { - cwd := opts.cwd - if cwd == "" { - var err error - cwd, err = currentWorkspacePath(ctx, workspace) - if err != nil { - return nil, err - } - } - - candidate, err := workspacePath(cwd, opts.ref) - if err != nil { - return nil, err - } - - local, err := workspaceDirectoryExists(ctx, workspace, candidate) - if err != nil { - return nil, err - } - if !local { - return nil, fmt.Errorf("local module source %q does not exist in workspace at %q", opts.ref, candidate) - } - - include, err := workspaceModuleSourceInclude(ctx, workspace, candidate) - if err != nil { - return nil, err - } - return workspace.Directory("/", dagger.WorkspaceDirectoryOpts{Include: include}), nil -} - -func workspaceDirectoryExists(ctx context.Context, workspace *dagger.Workspace, p string) (bool, error) { - p, err := clean(p) - if err != nil { - return false, err - } - if p == "." { - return true, nil - } - return workspace. - Directory("/", dagger.WorkspaceDirectoryOpts{Include: []string{p, path.Join(p, "**")}}). - Exists(ctx, p, dagger.DirectoryExistsOpts{ExpectedType: dagger.ExistsTypeDirectoryType}) -} - -func workspaceModuleSourceInclude( - ctx context.Context, - workspace *dagger.Workspace, - modulePath string, -) ([]string, error) { - return moduleSourceInclude(ctx, modulePath, func(ctx context.Context, p string) (sourceConfig, bool, error) { - // Prefer the current dagger-module.toml config; fall back to the legacy - // dagger.json. A module's own files are loaded via "**", but local - // directory dependencies live outside the module directory, so we must - // parse the config and recurse into them — otherwise a dependency - // declared only in dagger-module.toml is dropped from the loaded context - // and the engine fails with "dir module source does not contain a dagger - // config file". - tomlPath := moduleConfigPath(p, configFilenameTOML) - ok, err := configFileExists(ctx, workspace, tomlPath) - if err != nil { - return sourceConfig{}, false, err - } - if ok { - contents, err := workspace. - Directory("/", dagger.WorkspaceDirectoryOpts{Include: []string{tomlPath}}). - File(tomlPath).Contents(ctx) - if err != nil { - return sourceConfig{}, false, err - } - config, err := parseSourceConfigTOML(contents) - if err != nil { - return sourceConfig{}, true, fmt.Errorf("parse %s: %w", tomlPath, err) - } - return config, true, nil - } - - configPath := daggerJSONPath(p) - ok, err = configFileExists(ctx, workspace, configPath) - if err != nil { - return sourceConfig{}, false, err - } - if !ok { - return sourceConfig{}, false, nil - } - contents, err := workspace. - Directory("/", dagger.WorkspaceDirectoryOpts{Include: []string{configPath}}). - File(configPath).Contents(ctx) - if err != nil { - return sourceConfig{}, false, err - } - config, err := parseSourceConfig(contents) - if err != nil { - return sourceConfig{}, true, fmt.Errorf("parse %s: %w", configPath, err) - } - return config, true, nil - }) -} - -type sourceConfig struct { - dependencies []string - include []string -} - -func moduleSourceIncludeFromConfigs(configs map[string]sourceConfig, modulePath string) ([]string, error) { - return moduleSourceInclude(context.Background(), modulePath, func(_ context.Context, p string) (sourceConfig, bool, error) { - config, ok := configs[daggerJSONPath(p)] - return config, ok, nil - }) -} - -type sourceConfigReader func(context.Context, string) (sourceConfig, bool, error) - -func moduleSourceInclude(ctx context.Context, modulePath string, readConfig sourceConfigReader) ([]string, error) { - include := map[string]struct{}{} - seen := map[string]struct{}{} - var visit func(string) error - visit = func(p string) error { - p, err := clean(p) - if err != nil { - return err - } - if _, ok := seen[p]; ok { - return nil - } - seen[p] = struct{}{} - - config, ok, err := readConfig(ctx, p) - if err != nil { - return err - } - if !ok { - return fmt.Errorf("module source config (%s or dagger.json) not found in %q", configFilenameTOML, p) - } - - if p == "." { - include["."] = struct{}{} - include["dagger.json"] = struct{}{} - include["**"] = struct{}{} - } else { - include[p] = struct{}{} - include[daggerJSONPath(p)] = struct{}{} - include[path.Join(p, "**")] = struct{}{} - } - - for _, includePath := range config.include { - resolved, err := workspacePath(p, includePath) - if err != nil { - return err - } - include[resolved] = struct{}{} - } - - for _, dep := range config.dependencies { - if mustBeLocalRef(dep) { - depPath, err := workspacePath(p, dep) - if err != nil { - return err - } - if err := visit(depPath); err != nil { - return err - } - } - } - return nil - } - if err := visit(modulePath); err != nil { - return nil, err - } - - ordered := make([]string, 0, len(include)) - for p := range include { - ordered = append(ordered, p) - } - sort.Strings(ordered) - return ordered, nil -} - -func parseSourceConfig(contents string) (sourceConfig, error) { - var config struct { - Dependencies []json.RawMessage `json:"dependencies"` - Include []json.RawMessage `json:"include"` - } - if err := json.Unmarshal([]byte(contents), &config); err != nil { - return sourceConfig{}, err - } - - var parsed sourceConfig - for _, raw := range config.Dependencies { - var source string - if err := json.Unmarshal(raw, &source); err == nil { - parsed.dependencies = append(parsed.dependencies, source) - continue - } - - var object struct { - Source string `json:"source"` - } - if err := json.Unmarshal(raw, &object); err != nil { - return sourceConfig{}, err - } - if object.Source != "" { - parsed.dependencies = append(parsed.dependencies, object.Source) - } - } - for _, raw := range config.Include { - var includePath string - if err := json.Unmarshal(raw, &includePath); err == nil && includePath != "" { - parsed.include = append(parsed.include, includePath) - } - } - return parsed, nil -} - -// parseSourceConfigTOML reads the dependencies and include paths from a -// dagger-module.toml config. -func parseSourceConfigTOML(contents string) (sourceConfig, error) { - var config struct { - Dependencies []struct { - Source string `toml:"source"` - } `toml:"dependencies"` - Include []string `toml:"include"` - } - if err := toml.Unmarshal([]byte(contents), &config); err != nil { - return sourceConfig{}, err - } - - var parsed sourceConfig - for _, dep := range config.Dependencies { - if dep.Source != "" { - parsed.dependencies = append(parsed.dependencies, dep.Source) - } - } - for _, includePath := range config.Include { - if includePath != "" { - parsed.include = append(parsed.include, includePath) - } - } - return parsed, nil -} - -const configFilenameTOML = "dagger-module.toml" - -func daggerJSONPath(modulePath string) string { - return moduleConfigPath(modulePath, "dagger.json") -} - -func moduleConfigPath(modulePath, filename string) string { - if modulePath == "." { - return filename - } - return path.Join(modulePath, filename) -} - -func configFileExists(ctx context.Context, workspace *dagger.Workspace, configPath string) (bool, error) { - return workspace. - Directory("/", dagger.WorkspaceDirectoryOpts{Include: []string{configPath}}). - Exists(ctx, configPath, dagger.DirectoryExistsOpts{ExpectedType: dagger.ExistsTypeRegularType}) -} - -func workspacePath(cwd, ref string) (string, error) { - cwd, err := clean(cwd) - if err != nil { - return "", err - } - if strings.HasPrefix(ref, "/") { - return clean(ref) - } - if cwd == "." { - return clean(ref) - } - return clean(path.Join(cwd, ref)) -} - -func currentWorkspacePath(ctx context.Context, workspace *dagger.Workspace) (string, error) { - // Newer engines do not expose Workspace.path. Searching for "." from "." - // returns the current workspace directory as a workspace-root-relative path. - cwd, err := workspace.FindUp(ctx, ".", dagger.WorkspaceFindUpOpts{From: "."}) - if err != nil { - return "", err - } - return clean(cwd) -} - -func mustBeLocalRef(ref string) bool { - if ref == "" { - return false - } - return strings.HasPrefix(ref, "/") || - strings.HasPrefix(ref, ".") || - strings.HasPrefix(ref, "..") || - !strings.Contains(ref, ".") -} - -func clean(p string) (string, error) { - p = path.Clean(strings.TrimPrefix(p, "/")) - if p == "." || p == "" { - return ".", nil - } - if p == ".." || strings.HasPrefix(p, "../") { - return "", fmt.Errorf("path escapes workspace: %s", p) - } - return p, nil -} - -func envString(name string) (string, error) { - raw := os.Getenv(name) - if raw == "" { - return "", fmt.Errorf("%s is not set", name) - } - - var decoded string - if err := json.Unmarshal([]byte(raw), &decoded); err == nil { - return decoded, nil - } - return raw, nil -} diff --git a/polyfill/helpers/workspace-module-generate/main_test.go b/polyfill/helpers/workspace-module-generate/main_test.go deleted file mode 100644 index a6e7848..0000000 --- a/polyfill/helpers/workspace-module-generate/main_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package main - -import ( - "context" - "reflect" - "testing" -) - -func TestModuleSourceIncludeFromConfigsIncludesDeclaredPaths(t *testing.T) { - configs := map[string]sourceConfig{ - "app/dagger.json": { - dependencies: []string{"../dep"}, - include: []string{"../root.txt", "assets/**/*"}, - }, - "dep/dagger.json": { - include: []string{"../shared.txt", "subdir/**/*"}, - }, - } - - got, err := moduleSourceIncludeFromConfigs(configs, "app") - if err != nil { - t.Fatal(err) - } - - want := []string{ - "app", - "app/**", - "app/assets/**/*", - "app/dagger.json", - "dep", - "dep/**", - "dep/dagger.json", - "dep/subdir/**/*", - "root.txt", - "shared.txt", - } - if !reflect.DeepEqual(got, want) { - t.Fatalf("include mismatch:\n got: %#v\nwant: %#v", got, want) - } -} - -func TestParseSourceConfigTOMLReadsDependenciesAndInclude(t *testing.T) { - contents := `name = "app" -engineVersion = "v0.20.8" -include = ["assets/**/*"] - -[[dependencies]] -source = "../dep-a" - -[[dependencies]] -name = "named" -source = "../dep-b" -` - got, err := parseSourceConfigTOML(contents) - if err != nil { - t.Fatal(err) - } - - want := sourceConfig{ - dependencies: []string{"../dep-a", "../dep-b"}, - include: []string{"assets/**/*"}, - } - if !reflect.DeepEqual(got, want) { - t.Fatalf("toml config mismatch:\n got: %#v\nwant: %#v", got, want) - } -} - -// A module whose local directory dependency is declared only in dagger-module.toml -// (the format the engine prefers) must still have the dependency's directory pulled -// into the loaded workspace context. Otherwise the engine fails to resolve the dep -// with "dir module source does not contain a dagger config file". -func TestModuleSourceIncludeReadsTOMLDependencies(t *testing.T) { - files := map[string]string{ - "app/dagger-module.toml": "name = \"app\"\n\n[[dependencies]]\nsource = \"../dep\"\n", - "dep/dagger.json": "{\"name\":\"dep\"}", - } - - got, err := moduleSourceInclude(context.Background(), "app", func(_ context.Context, p string) (sourceConfig, bool, error) { - if contents, ok := files[moduleConfigPath(p, configFilenameTOML)]; ok { - config, err := parseSourceConfigTOML(contents) - return config, true, err - } - if contents, ok := files[daggerJSONPath(p)]; ok { - config, err := parseSourceConfig(contents) - return config, true, err - } - return sourceConfig{}, false, nil - }) - if err != nil { - t.Fatal(err) - } - - want := []string{ - "app", - "app/**", - "app/dagger.json", - "dep", - "dep/**", - "dep/dagger.json", - } - if !reflect.DeepEqual(got, want) { - t.Fatalf("include mismatch:\n got: %#v\nwant: %#v", got, want) - } -} - -func TestModuleSourceIncludeReadsOnlyTargetAndLocalDependencies(t *testing.T) { - var read []string - - got, err := moduleSourceInclude(context.Background(), "app", func(_ context.Context, p string) (sourceConfig, bool, error) { - read = append(read, p) - switch p { - case "app": - return sourceConfig{dependencies: []string{"../dep"}}, true, nil - case "dep": - return sourceConfig{}, true, nil - default: - t.Fatalf("unexpected config read: %s", p) - return sourceConfig{}, false, nil - } - }) - if err != nil { - t.Fatal(err) - } - - wantRead := []string{"app", "dep"} - if !reflect.DeepEqual(read, wantRead) { - t.Fatalf("read paths mismatch:\n got: %#v\nwant: %#v", read, wantRead) - } - - want := []string{ - "app", - "app/**", - "app/dagger.json", - "dep", - "dep/**", - "dep/dagger.json", - } - if !reflect.DeepEqual(got, want) { - t.Fatalf("include mismatch:\n got: %#v\nwant: %#v", got, want) - } -} diff --git a/polyfill/helpers/workspace-snapshot/go.mod b/polyfill/helpers/workspace-snapshot/go.mod deleted file mode 100644 index a3e82d7..0000000 --- a/polyfill/helpers/workspace-snapshot/go.mod +++ /dev/null @@ -1,25 +0,0 @@ -module workspace-snapshot - -go 1.26.1 - -require dagger.io/dagger v0.21.3 - -require ( - github.com/99designs/gqlgen v0.17.89 // indirect - github.com/Khan/genqlient v0.8.1 // indirect - github.com/adrg/xdg v0.5.3 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/sosodev/duration v1.4.0 // indirect - github.com/vektah/gqlparser/v2 v2.5.32 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect -) diff --git a/polyfill/helpers/workspace-snapshot/go.sum b/polyfill/helpers/workspace-snapshot/go.sum deleted file mode 100644 index 398eaa6..0000000 --- a/polyfill/helpers/workspace-snapshot/go.sum +++ /dev/null @@ -1,55 +0,0 @@ -dagger.io/dagger v0.20.6 h1:Uac8tdqieoq+psVe5f+rhNk1sk2W8YK5t4OzpGyHuTI= -dagger.io/dagger v0.20.6/go.mod h1:ZXg8+pQZaZUC8rAw4V/gPP8aKvKARIJZ+pfcV+RC1es= -dagger.io/dagger v0.21.3 h1:Y18i1txUYNeILqqw2wZNeMRdKIK1/pYVnIEcttYvHAo= -dagger.io/dagger v0.21.3/go.mod h1:8hztpM9rKNjSmOa2nzd8ot6aw1RZFDWCawMfc5V9tXA= -github.com/99designs/gqlgen v0.17.89 h1:KzEcxPiMgQoMw3m/E85atUEHyZyt0PbAflMia5Kw8z8= -github.com/99designs/gqlgen v0.17.89/go.mod h1:GFqruTVGB7ZTdrf1uzOagpXbY7DrEt1pIxnTdhIbWvQ= -github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs= -github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU= -github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= -github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= -github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= -github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59 h1:g6vfdGRyz6fAjfHz5FyYPZgHy8qcQ31fHrBl1iCOzxw= -github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59/go.mod h1:jsdUJeYzcbyK1j/EqMGPrQgNYxl/Zfg06vvM9C/xXxs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= -github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= -github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/polyfill/helpers/workspace-snapshot/main.go b/polyfill/helpers/workspace-snapshot/main.go deleted file mode 100644 index c9ba026..0000000 --- a/polyfill/helpers/workspace-snapshot/main.go +++ /dev/null @@ -1,173 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "os" - "path" - "path/filepath" - "strings" - - "dagger.io/dagger" -) - -const workspaceIDEnv = "WORKSPACE_ID" - -func main() { - if err := run(context.Background(), os.Args[1:]); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func run(ctx context.Context, args []string) error { - fs := flag.NewFlagSet("workspace-snapshot", flag.ContinueOnError) - fs.SetOutput(os.Stderr) - kind := fs.String("kind", "", "workspace path kind: file or directory") - workspacePath := fs.String("path", "", "workspace-root-relative path") - localPath := fs.String("local-path", "", "output-relative path") - out := fs.String("out", "/out", "output directory") - if err := fs.Parse(args); err != nil { - return err - } - if fs.NArg() != 0 { - return fmt.Errorf("unexpected arguments: %v", fs.Args()) - } - if *kind != "file" && *kind != "directory" { - return fmt.Errorf("--kind must be file or directory") - } - if *workspacePath == "" { - return fmt.Errorf("--path is required") - } - if *localPath == "" { - return fmt.Errorf("--local-path is required") - } - - workspaceID, err := envString(workspaceIDEnv) - if err != nil { - return err - } - client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr)) - if err != nil { - return err - } - defer client.Close() - - workspace := dagger.Ref[*dagger.Workspace](client, dagger.ID(workspaceID)) - switch *kind { - case "file": - return exportFile(ctx, workspace, *workspacePath, *localPath, *out) - case "directory": - return exportDirectory(ctx, workspace, *workspacePath, *localPath, *out) - default: - panic("unreachable") - } -} - -func exportFile(ctx context.Context, workspace *dagger.Workspace, workspacePath, localPath, out string) error { - workspacePath, err := cleanWorkspacePath(workspacePath) - if err != nil { - return err - } - localPath, err = cleanLocalPath(localPath) - if err != nil { - return err - } - if localPath == "." { - return fmt.Errorf("cannot export file to output root") - } - - src := workspace.Directory("/", dagger.WorkspaceDirectoryOpts{Include: []string{workspacePath}}) - exists, err := src.Exists(ctx, workspacePath, dagger.DirectoryExistsOpts{ - ExpectedType: dagger.ExistsTypeRegularType, - }) - if err != nil { - return err - } - if !exists { - return os.MkdirAll(out, 0o755) - } - - dst := filepath.Join(out, filepath.FromSlash(localPath)) - if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { - return err - } - _, err = src.File(workspacePath).Export(ctx, dst) - return err -} - -func exportDirectory(ctx context.Context, workspace *dagger.Workspace, workspacePath, localPath, out string) error { - workspacePath, err := cleanWorkspacePath(workspacePath) - if err != nil { - return err - } - localPath, err = cleanLocalPath(localPath) - if err != nil { - return err - } - - src := workspace.Directory("/", dagger.WorkspaceDirectoryOpts{Include: includeDirectory(workspacePath)}) - exists, err := src.Exists(ctx, workspacePath, dagger.DirectoryExistsOpts{ - ExpectedType: dagger.ExistsTypeDirectoryType, - }) - if err != nil { - return err - } - if !exists { - return os.MkdirAll(out, 0o755) - } - - dst := out - if localPath != "." { - dst = filepath.Join(out, filepath.FromSlash(localPath)) - if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { - return err - } - } - _, err = src.Directory(workspacePath).Export(ctx, dst) - return err -} - -func includeDirectory(p string) []string { - if p == "." { - return []string{"**", "**/.*/**"} - } - return []string{p, path.Join(p, "**"), path.Join(p, "**/.*/**")} -} - -func cleanWorkspacePath(p string) (string, error) { - p = path.Clean(strings.TrimPrefix(p, "/")) - if p == "." || p == "" { - return ".", nil - } - if p == ".." || strings.HasPrefix(p, "../") { - return "", fmt.Errorf("workspace path escapes workspace: %s", p) - } - return p, nil -} - -func cleanLocalPath(p string) (string, error) { - p = path.Clean(strings.TrimPrefix(p, "/")) - if p == "" { - return ".", nil - } - if p == ".." || strings.HasPrefix(p, "../") { - return "", fmt.Errorf("local path escapes output: %s", p) - } - return p, nil -} - -func envString(name string) (string, error) { - raw := os.Getenv(name) - if raw == "" { - return "", fmt.Errorf("%s is not set", name) - } - - var decoded string - if err := json.Unmarshal([]byte(raw), &decoded); err == nil { - return decoded, nil - } - return raw, nil -} diff --git a/polyfill/helpers/workspace-snapshot/main_test.go b/polyfill/helpers/workspace-snapshot/main_test.go deleted file mode 100644 index baeb8c9..0000000 --- a/polyfill/helpers/workspace-snapshot/main_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "reflect" - "testing" -) - -func TestIncludeDirectory(t *testing.T) { - got := includeDirectory("app") - want := []string{"app", "app/**", "app/**/.*/**"} - if !reflect.DeepEqual(got, want) { - t.Fatalf("include mismatch:\n got: %#v\nwant: %#v", got, want) - } -} - -func TestCleanPathsRejectEscapes(t *testing.T) { - if _, err := cleanWorkspacePath("../out"); err == nil { - t.Fatal("cleanWorkspacePath accepted escaping path") - } - if _, err := cleanLocalPath("../out"); err == nil { - t.Fatal("cleanLocalPath accepted escaping path") - } -} diff --git a/polyfill/module-config.dang b/polyfill/module-config.dang deleted file mode 100644 index 9a2229d..0000000 --- a/polyfill/module-config.dang +++ /dev/null @@ -1,374 +0,0 @@ -""" -Read and edit a workspace module's dagger.json. -""" -type PolyfillModuleConfig { - let ws: Workspace! - let cwd: String! - let path: String! - let originalContents: String! - let contents: String! - - """ - The Dagger module name declared in dagger.json. - """ - pub name: String! { - configJSON.field(["name"]).asString - } - - """ - Return dependency names when present, otherwise dependency sources. - """ - pub dependencies: [String!]! { - dependencyFragments - .map { dependency => dependencyName(dependency) } - .filter { dependency => dependency != "" } - } - - """ - The Dagger engine version required by this module, without a leading "v". - """ - pub requiredEngineVersion: String! { - configJSON.field(["engineVersion"]).asString.trimPrefix("v") - } - - """ - Return this module config with one dependency appended. - """ - pub withDependency(source: String!, name: String! = ""): PolyfillModuleConfig! { - let dependency = if (name == "") { - toString(toJSON(source)) - } else { - toString(toJSON({{name: name, source: source}})) - } - withContents(withDependencyFragments(dependencyFragments + [dependency])) - } - - """ - Return this module config with dependencies matching the given name or source removed. - """ - pub withoutDependency(name: String!): PolyfillModuleConfig! { - withContents(withDependencyFragments( - dependencyFragments.filter { dependency => - dependencyName(dependency) != name and dependencySource(dependency) != name - }, - )) - } - - """ - Return this module config after updating one dependency, or all remote dependencies. - """ - pub withUpdatedDependencies(name: String! = ""): PolyfillModuleConfig! { - let updates = if (name == "") { [] } else { [name] } - let updatedRemote = updatedRemoteDependencyFragments(updates) - - if (updatedRemote.length == 0) { - PolyfillModuleConfig( - ws: ws, - cwd: cwd, - path: path, - originalContents: originalContents, - contents: contents, - ) - } else { - withContents(withDependencyFragments(localDependencyFragments + updatedRemote)) - } - } - - """ - Return this module config with the given required Dagger engine version. - """ - pub withRequiredEngineVersion(version: String!): PolyfillModuleConfig! { - withContents(withField(["engineVersion"], normalizedVersion(version))) - } - - """ - Return this module config with the current Dagger engine version. - """ - pub withCurrentEngineVersion: PolyfillModuleConfig! { - withRequiredEngineVersion(version) - } - - """ - Return this module config with the latest stable Dagger engine version. - """ - pub withLatestEngineVersion: PolyfillModuleConfig! { - withRequiredEngineVersion("latest") - } - - """ - Return a workspace fork containing this module config's staged dagger.json change. - """ - pub fork: PolyfillWorkspaceFork! { - if (contents == originalContents) { - PolyfillWorkspaceFork( - ws: ws, - cwd: cwd, - before: directory, - after: directory, - ) - } else { - let localPath = clientPath(configPath) - PolyfillWorkspaceFork( - ws: ws, - cwd: cwd, - before: directory.withNewFile(localPath, originalContents), - after: directory.withNewFile(localPath, contents), - ) - } - } - - """ - Return this module config with updated dagger.json contents. - """ - let withContents(updated: String!): PolyfillModuleConfig! { - PolyfillModuleConfig( - ws: ws, - cwd: cwd, - path: path, - originalContents: originalContents, - contents: updated, - ) - } - - """ - Return this module's dagger.json path from the workspace root. - """ - let configPath: String! { - if (path == ".") { "dagger.json" } else { path + "/dagger.json" } - } - - let wsID: String! { - toJSON(ws.id) - } - - """ - Return a workspace-root path converted to caller-cwd coordinates. - """ - let clientPath(path: String!): String! { - let workspacePath = cleanWorkspacePath(path) - - if (cwd == ".") { - workspacePath - } else if (workspacePath == cwd) { - "." - } else { - let relative = workspacePath.trimPrefix(cwd + "/") - if (relative == workspacePath) { - raise "workspace path " + workspacePath + " is outside changeset root " + cwd - } else { - relative - } - } - } - - """ - Return a valid workspace-root-relative path. - """ - let cleanWorkspacePath(path: String!): String! { - if (path == "" or path == ".") { - "." - } else if (path.trimPrefix("/") != path) { - raise "workspace path must be relative: " + path - } else if (path == ".." - or path.trimPrefix("../") != path - or path.contains("/../") - or path.trimSuffix("/..") != path) { - raise "workspace path escapes workspace: " + path - } else { - path.trimPrefix("./").trimSuffix("/") - } - } - - """ - Return each dependency entry as raw JSON text. - """ - let dependencyFragments: [String!]! { - let config = configJSON - if (config.fields.contains("dependencies") == true) { - let dependencies = config.field(["dependencies"]) - if (toString(dependencies.contents) == "null") { - [] - } else { - dependencies - .asArray.{contents} - .map { dependency => toString(dependency.contents) } - } - } else { - [] - } - } - - """ - Return local dependency entries exactly as they appeared in dagger.json. - """ - let localDependencyFragments: [String!]! { - dependencyFragments.filter { dependency => isLocalDependency(dependency) } - } - - """ - Return the dependency's configured name, or its source if no name is set. - """ - let dependencyName(dependency: String!): String! { - let value = json.withContents(dependency :: JSON!) - if (isJSONString(dependency)) { - value.asString - } else if (value.fields.contains("name") == true) { - value.field(["name"]).asString - } else if (value.fields.contains("source") == true) { - value.field(["source"]).asString - } else { - "" - } - } - - """ - Return the dependency source. - """ - let dependencySource(dependency: String!): String! { - let value = json.withContents(dependency :: JSON!) - if (isJSONString(dependency)) { - value.asString - } else if (value.fields.contains("source") == true) { - value.field(["source"]).asString - } else { - "" - } - } - - """ - Return the dependency pin, if one is configured. - """ - let dependencyPin(dependency: String!): String! { - let value = json.withContents(dependency :: JSON!) - if (isJSONString(dependency)) { - "" - } else if (value.fields.contains("pin") == true) { - value.field(["pin"]).asString - } else { - "" - } - } - - """ - Return true when a dependency source is a local path or bare local name. - """ - let isLocalDependency(dependency: String!): Boolean! { - let source = dependencySource(dependency) - source != "" and dependencyPin(dependency) == "" and ( - source.trimPrefix("/") != source - or source.trimPrefix(".") != source - or source.contains(".") == false - ) - } - - """ - Return true when a JSON fragment is a string value. - """ - let isJSONString(value: String!): Boolean! { - value.trimPrefix("\"") != value - } - - """ - Return dagger.json contents with the given dependency fragments as dependencies. - """ - let withDependencyFragments(dependencies: [String!]!): String! { - let updated = configJSON.withField( - ["dependencies"], - json.withContents(("[" + dependencies.join(",") + "]") :: JSON!), - ) - renderedConfig(updated) - } - - """ - Return dagger.json contents with a string field updated. - """ - let withField(path: [String!]!, value: String!): String! { - renderedConfig( - configJSON.withField( - path, - json.withContents(toJSON(value) :: JSON!), - ), - ) - } - - """ - Return rendered JSON as dagger.json contents. - """ - let renderedConfig(value: JSONValue!): String! { - jsonContentsString(value) + "\n" - } - - """ - Return rendered JSON as plain file contents. - """ - let jsonContentsString(value: JSONValue!): String! { - # JSONValue.contents is a JSON scalar. toString encodes it as a JSON string - # literal, so decode once before passing it to file APIs that need String. - json.withContents(toString(value.contents(pretty: true)) :: JSON!).asString - } - - """ - Return the current dagger.json contents as JSON. - """ - let configJSON: JSONValue! { - json.withContents(contents :: JSON!) - } - - """ - Return a valid engine version string for dagger.json. - """ - let normalizedVersion(version: String!): String! { - if (version == "") { - raise "version must not be empty" - } else if (version == "latest") { - version - } else if (version.trimPrefix("v") != version) { - version - } else { - "v" + version - } - } - - """ - Return updated remote dependencies from a core dependency update response. - """ - let updatedRemoteDependencyFragments(updates: [String!]!): [String!]! { - PolyfillModuleConfigUpdate - .updatedRemoteDependencies(wsID, path, contents, updates) - .asJSON - .asArray.{contents} - .map { dependency => toString(dependency.contents) } - } -} - -type PolyfillModuleConfigUpdate { - """ - Return updated remote dependency fragments for a module config. - """ - let updatedRemoteDependencies( - wsID: String!, - path: String!, - contents: String!, - updates: [String!]!, - ): File! { - container - .from("golang:1.26-alpine") - .withoutEntrypoint - .withMountedCache("/go/pkg/mod", cacheVolume("go-mod")) - .withMountedCache("/root/.cache/go-build", cacheVolume("go-build")) - .withDirectory( - "/helper", - currentModule.source.directory("helpers/module-config-update-dependencies"), - ) - .withWorkdir("/helper") - .withExec(["go", "build", "-o", "/usr/local/bin/module-config-update-dependencies", "."]) - .withEnvVariable("WORKSPACE_ID", wsID) - .withEnvVariable("MODULE_CONFIG_CONTENTS", contents) - .withEnvVariable("MODULE_CONFIG_UPDATES_JSON", toJSON(updates)) - .withExec( - ["module-config-update-dependencies", "--path", path, "--out", "/dependencies.json"], - experimentalPrivilegedNesting: true, - ) - .file("/dependencies.json") - } -} diff --git a/polyfill/module-source.dang b/polyfill/module-source.dang deleted file mode 100644 index 54a5f09..0000000 --- a/polyfill/module-source.dang +++ /dev/null @@ -1,222 +0,0 @@ -""" -Polyfill implementation of module source operations needed by SDK development. -""" -type PolyfillModuleSource { - let ws: Workspace! - let cwd: String! - let path: String! - - """ - Read and edit this module source's dagger.json. - """ - pub config: PolyfillModuleConfig! { - let configContents = PolyfillWorkspaceSnapshot.existingFile( - wsID, - configPath, - configPath, - ).file(configPath).contents - PolyfillModuleConfig( - ws: ws, - cwd: cwd, - path: path, - originalContents: configContents, - contents: configContents, - ) - } - - """ - Return true when this module source has a dagger.json. - """ - pub configExists: Boolean! { - ws.directory("/", include: [configPath]).exists(configPath) - } - - """ - Return this module source as a core ModuleSource. - """ - pub core: ModuleSource! { - node(moduleSourceID).{... on ModuleSource!} - } - - """ - Return a filtered workspace-rooted directory view containing the files required - to load this module source. - """ - pub workspaceView: Directory! { - PolyfillGeneration.workspaceModuleSourceView( - wsID, - path, - cwd: ".", - local: true, - ) - } - - """ - Return the module source root path inside the workspace view. - """ - pub sourceRootPath: String! { - path - } - - """ - Load this module source as a core Module. - """ - pub module: Module! { - core.asModule - } - - """ - Return this module source's introspection schema file. - """ - pub introspectionSchemaJSON: File! { - module.introspectionSchemaJSON - } - - """ - Generate files for this module source, and return staged workspace changes. - """ - pub generate: PolyfillWorkspaceFork! { - let fork = PolyfillWorkspace(ws: ws, cwd: cwd).fork - let generated = PolyfillGeneration.workspaceModuleGeneratedContext( - wsID, - path, - cwd: ".", - local: true, - root: fork.cwd, - ) - - fork.withDirectoryDiff(fork.cwd, generated.before, generated.after) - } - - """ - Return this module source's dagger.json path from the workspace root. - """ - let configPath: String! { - if (path == ".") { "dagger.json" } else { path + "/dagger.json" } - } - - let wsID: String! { - toJSON(ws.id) - } - - let moduleSourceID: ID! { - PolyfillGeneration.workspaceModuleSourceID( - wsID, - path, - cwd: ".", - local: true, - ) :: ID! - } -} - -type PolyfillGeneration { - """ - Return generated context directories for an existing workspace module. - - Temporary workaround: generated-context construction currently happens in a - nested Go helper because the SDK generation path still needs host workspace - snapshot behavior. Delete this helper once the engine exposes a direct API - usable from Dang. - """ - let workspaceModuleGeneratedContext( - wsID: String!, - ref: String!, - cwd: String! = "", - local: Boolean! = false, - root: String! = ".", - ): PolyfillDirectoryDiff! { - let cwdFlags = if (cwd == "") { [] } else { ["--cwd", cwd] } - let localFlags = if (local) { ["--local"] } else { [] } - - let output = container - .from("golang:1.26-alpine") - .withoutEntrypoint - .withMountedCache("/go/pkg/mod", cacheVolume("go-mod")) - .withMountedCache("/root/.cache/go-build", cacheVolume("go-build")) - .withDirectory( - "/helper", - currentModule.source.directory("helpers/workspace-module-generate"), - ) - .withWorkdir("/helper") - .withExec(["go", "build", "-o", "/usr/local/bin/workspace-module-generate", "."]) - .withEnvVariable("WORKSPACE_ID", wsID) - .withExec( - ["workspace-module-generate", "--root", root] + cwdFlags + localFlags + [ref], - experimentalPrivilegedNesting: true, - ) - - PolyfillDirectoryDiff( - before: output.directory("/before"), - after: output.directory("/after"), - ) - } - - """ - Return the core ModuleSource ID for an existing workspace module. - """ - let workspaceModuleSourceID( - wsID: String!, - ref: String!, - cwd: String! = "", - local: Boolean! = false, - ): String! { - let cwdFlags = if (cwd == "") { [] } else { ["--cwd", cwd] } - let localFlags = if (local) { ["--local"] } else { [] } - - container - .from("golang:1.26-alpine") - .withoutEntrypoint - .withMountedCache("/go/pkg/mod", cacheVolume("go-mod")) - .withMountedCache("/root/.cache/go-build", cacheVolume("go-build")) - .withDirectory( - "/helper", - currentModule.source.directory("helpers/workspace-module-generate"), - ) - .withWorkdir("/helper") - .withExec(["go", "build", "-o", "/usr/local/bin/workspace-module-generate", "."]) - .withEnvVariable("WORKSPACE_ID", wsID) - .withExec( - ["workspace-module-generate", "--id-out", "/module-source.id"] + cwdFlags + localFlags + [ref], - experimentalPrivilegedNesting: true, - ) - .file("/module-source.id") - .contents - } - - """ - Return the filtered workspace-rooted directory needed to load an existing - workspace module source. - """ - let workspaceModuleSourceView( - wsID: String!, - ref: String!, - cwd: String! = "", - local: Boolean! = false, - ): Directory! { - let cwdFlags = if (cwd == "") { [] } else { ["--cwd", cwd] } - let localFlags = if (local) { ["--local"] } else { [] } - - container - .from("golang:1.26-alpine") - .withoutEntrypoint - .withMountedCache("/go/pkg/mod", cacheVolume("go-mod")) - .withMountedCache("/root/.cache/go-build", cacheVolume("go-build")) - .withDirectory( - "/helper", - currentModule.source.directory("helpers/workspace-module-generate"), - ) - .withWorkdir("/helper") - .withExec(["go", "build", "-o", "/usr/local/bin/workspace-module-generate", "."]) - .withEnvVariable("WORKSPACE_ID", wsID) - .withExec( - ["workspace-module-generate", "--view-out", "/workspace-view"] + cwdFlags + localFlags + [ref], - experimentalPrivilegedNesting: true, - ) - .directory("/workspace-view") - } -} - -type PolyfillDirectoryDiff { - let before: Directory! - let after: Directory! -} diff --git a/polyfill/polyfill.dang b/polyfill/polyfill.dang deleted file mode 100644 index 0bf46e0..0000000 --- a/polyfill/polyfill.dang +++ /dev/null @@ -1,14 +0,0 @@ -""" -Polyfill for missing or broken engine calls used by SDK development. -""" -type Polyfill { - """ - Project a core workspace to a polyfill workspace. - """ - pub workspace(ws: Workspace!): PolyfillWorkspace! { - PolyfillWorkspace( - ws: ws, - cwd: PolyfillWorkspaceCwd().path(ws), - ) - } -} diff --git a/polyfill/workspace-fork.dang b/polyfill/workspace-fork.dang deleted file mode 100644 index 68f4a06..0000000 --- a/polyfill/workspace-fork.dang +++ /dev/null @@ -1,247 +0,0 @@ -""" -Helpers for staging workspace-rooted edits before returning a Changeset. - -This is a local helper for a future core concept. Callers pass paths relative to -the workspace root. The helper converts them to the caller's workspace cwd only -when producing a Changeset, because today's clients apply returned changesets -relative to the caller cwd. -""" -type PolyfillWorkspaceFork { - let ws: Workspace! - - """ - Caller cwd relative to the workspace root. - """ - let cwd: String! - - let before: Directory! - let after: Directory! - - """ - Add or replace a file at a workspace-root-relative path. - """ - pub withNewFile(path: String!, contents: String!): PolyfillWorkspaceFork! { - let workspacePath = cleanWorkspacePath(path) - let localPath = clientPath(workspacePath) - - PolyfillWorkspaceFork( - ws: ws, - cwd: cwd, - before: beforeWithExistingFile(workspacePath, localPath), - after: after.withNewFile(localPath, contents), - ) - } - - """ - Add or replace a file at a workspace-root-relative path. - """ - pub withFile(path: String!, source: File!): PolyfillWorkspaceFork! { - let workspacePath = cleanWorkspacePath(path) - let localPath = clientPath(workspacePath) - - PolyfillWorkspaceFork( - ws: ws, - cwd: cwd, - before: beforeWithExistingFile(workspacePath, localPath), - after: after.withFile(localPath, source), - ) - } - - """ - Add or replace a directory at a workspace-root-relative path. - """ - pub withDirectory(path: String!, source: Directory!): PolyfillWorkspaceFork! { - let workspacePath = cleanWorkspacePath(path) - let localPath = clientPath(workspacePath) - - PolyfillWorkspaceFork( - ws: ws, - cwd: cwd, - before: beforeWithExistingDirectory(workspacePath, localPath), - after: after.withDirectory(localPath, source), - ) - } - - """ - Stage an already-computed directory diff at a workspace-root-relative path. - """ - pub withDirectoryDiff( - path: String!, - beforeSource: Directory!, - afterSource: Directory!, - ): PolyfillWorkspaceFork! { - let workspacePath = cleanWorkspacePath(path) - let localPath = clientPath(workspacePath) - - PolyfillWorkspaceFork( - ws: ws, - cwd: cwd, - before: before.withDirectory(localPath, beforeSource), - after: after.withDirectory(localPath, afterSource), - ) - } - - """ - Return a workspace-root path converted to caller-cwd coordinates. - """ - let clientPath(path: String!): String! { - let workspacePath = cleanWorkspacePath(path) - - if (cwd == ".") { - workspacePath - } else if (workspacePath == cwd) { - "." - } else { - let relative = workspacePath.trimPrefix(cwd + "/") - if (relative == workspacePath) { - raise "workspace path " + workspacePath + " is outside changeset root " + cwd - } else { - relative - } - } - } - - """ - Merge another fork staged for the same caller cwd. - """ - pub merge(other: PolyfillWorkspaceFork!): PolyfillWorkspaceFork! { - if (other.cwd != cwd) { - raise "cannot merge workspace forks with different cwd" - } - - PolyfillWorkspaceFork( - ws: ws, - cwd: cwd, - before: before.withDirectory(".", other.before), - after: after.withDirectory(".", other.after), - ) - } - - """ - Compute the filesystem changes staged in this fork. - """ - pub changes: Changeset! { - after.changes(before) - } - - """ - Return the before directory with the current workspace file added if it exists. - """ - let beforeWithExistingFile(workspacePath: String!, localPath: String!): Directory! { - before.withDirectory( - ".", - PolyfillWorkspaceSnapshot.existingFile(wsID, workspacePath, localPath), - ) - } - - """ - Return the before directory with the current workspace directory added if it exists. - """ - let beforeWithExistingDirectory(workspacePath: String!, localPath: String!): Directory! { - before.withDirectory( - ".", - PolyfillWorkspaceSnapshot.existingDirectory(wsID, workspacePath, localPath), - ) - } - - """ - Return a valid workspace-root-relative path. - """ - let cleanWorkspacePath(path: String!): String! { - if (path == "" or path == ".") { - "." - } else if (path.trimPrefix("/") != path) { - raise "workspace path must be relative: " + path - } else if (path == ".." - or path.trimPrefix("../") != path - or path.contains("/../") - or path.trimSuffix("/..") != path) { - raise "workspace path escapes workspace: " + path - } else { - path.trimPrefix("./").trimSuffix("/") - } - } - - let wsID: String! { - toJSON(ws.id) - } -} - -""" -Read existing workspace paths. -""" -type PolyfillWorkspaceSnapshot { - """ - Return a directory containing the existing workspace file, if it exists. - """ - let existingFile(wsID: String!, workspacePath: String!, localPath: String!): Directory! { - existing("file", wsID, workspacePath, localPath) - } - - """ - Return a directory containing the existing workspace directory, if it exists. - """ - let existingDirectory( - wsID: String!, - workspacePath: String!, - localPath: String!, - ): Directory! { - existing("directory", wsID, workspacePath, localPath) - } - - """ - Return a directory snapshot of one existing workspace path. - """ - let existing( - kind: String!, - wsID: String!, - workspacePath: String!, - localPath: String!, - ): Directory! { - container - .from("golang:1.26-alpine") - .withoutEntrypoint - .withMountedCache("/go/pkg/mod", cacheVolume("go-mod")) - .withMountedCache("/root/.cache/go-build", cacheVolume("go-build")) - .withDirectory( - "/helper", - currentModule.source.directory("helpers/workspace-snapshot"), - ) - .withWorkdir("/helper") - .withExec(["go", "build", "-o", "/usr/local/bin/workspace-snapshot", "."]) - .withEnvVariable("WORKSPACE_ID", wsID) - .withExec( - [ - "workspace-snapshot", - "--kind", kind, - "--path", workspacePath, - "--local-path", localPath, - "--out", "/out", - ], - experimentalPrivilegedNesting: true, - ) - .directory("/out") - } -} - -""" -Return the caller cwd relative to the workspace root. -""" -type PolyfillWorkspaceCwd { - """ - Return the current workspace directory path relative to the workspace root. - """ - let path(ws: Workspace!): String! { - # Newer engines do not expose Workspace.path. Searching for "." from "." - # returns the current workspace directory as a workspace-root-relative path. - let path = ws.findUp(name: ".", from: ".") - - if (path == null) { - "." - } else if (path == "/") { - "." - } else { - path.trimPrefix("/") - } - } -} diff --git a/polyfill/workspace.dang b/polyfill/workspace.dang deleted file mode 100644 index 117bc6c..0000000 --- a/polyfill/workspace.dang +++ /dev/null @@ -1,76 +0,0 @@ -""" -Polyfill implementation of workspace operations needed by SDK development. -""" -type PolyfillWorkspace { - let ws: Workspace! - let cwd: String! - - """ - Return the underlying core Workspace. - """ - pub core: Workspace! { - ws - } - - """ - Return a fork of the workspace state, to stage changes without side effects. - """ - pub fork: PolyfillWorkspaceFork! { - PolyfillWorkspaceFork( - ws: ws, - cwd: cwd, - before: directory, - after: directory, - ) - } - - """ - Return a directory view of this workspace. - """ - pub directory(path: String! = "/", include: [String!]! = ["**"]): Directory! { - ws.directory(path, include: include) - } - - """ - Load a module source at the given path in the workspace. - """ - pub moduleSource(path: String!): PolyfillModuleSource! { - PolyfillModuleSource( - ws: ws, - cwd: cwd, - path: moduleSourcePath(path), - ) - } - - """ - Return a module source path resolved relative to the caller cwd. - """ - let moduleSourcePath(path: String!): String! { - if (path.trimPrefix("/") != path) { - cleanWorkspacePath(path.trimPrefix("/")) - } else if (cwd == ".") { - cleanWorkspacePath(path) - } else { - let localPath = cleanWorkspacePath(path) - if (localPath == ".") { cwd } else { cwd + "/" + localPath } - } - } - - """ - Return a valid workspace-root-relative path. - """ - let cleanWorkspacePath(path: String!): String! { - if (path == "" or path == ".") { - "." - } else if ( - path == ".." - or path.trimPrefix("../") != path - or path.contains("/../") - or path.trimSuffix("/..") != path - ) { - raise "path escapes workspace: " + path - } else { - path.trimPrefix("./").trimSuffix("/") - } - } -} diff --git a/sdk-sdk.dang b/sdk-sdk.dang index 178aff1..1816eee 100644 --- a/sdk-sdk.dang +++ b/sdk-sdk.dang @@ -1,16 +1,21 @@ """ -Shared black-box contract checks for official SDK helper modules. +Black-box contract checks for Dagger SDK modules, plus tooling to start a new +SDK helper module. -Run from an SDK helper module workspace with: +Run the checks against an SDK repository, such as github.com/dagger/go-sdk or +github.com/dagger/dang-sdk: - dagger -m github.com/dagger/sdk-sdk check + dagger -m github.com/dagger/sdk-sdk -W check -The checks load the caller's module through the Dagger CLI and validate the -public API that SDK developers actually use. +The checks vendor the SDK module into a scratch git workspace, then drive it +the way a user would through a release Dagger CLI: `dagger sdk install`, +`dagger module init`, `dagger generate`, and the `dagger module` authoring +verbs. Function-level contract checks additionally call the SDK's `initModule` +and `@generate` hooks directly and inspect the returned changesets. Under CLI 1.0 the engine owns module bookkeeping (dagger-module.toml, workspace -config, dependency and engine-version edits). An SDK helper module implements -only what is language-specific: +config, dependency and engine-version edits). An SDK module implements only +what is language-specific: - `initModule(ws, name, path): Changeset!` — seed the SDK's own files for a new module. The engine owns the module config, so `initModule` must not write @@ -18,17 +23,37 @@ only what is language-specific: - a `@generate` hook — regenerate the modules the SDK manages. Client generation (`initClient`) is optional and not exercised here. + +sdk-sdk is itself an SDK for authoring Dang SDK helper modules: install it with +`dagger sdk install github.com/dagger/sdk-sdk`, then scaffold a new SDK with +`dagger module init sdk-sdk my-sdk`. Because of that, running `dagger check` in +this repository exercises every check against sdk-sdk itself. """ type SdkSdk { """ - Create a new SDK helper module and return the files to write. + Dagger CLI release version used by black-box tests. + """ + pub daggerCliVersion: String! = "1.0.0-beta.7" + + """ + Engine runtime recorded for modules created with this SDK. + + SDK helper modules scaffolded by sdk-sdk are Dang modules, so they run on the + engine's built-in "dang" runtime. + """ + targetRuntime: String! { + "dang" + } + + """ + Scaffold a new Dang SDK helper module for `dagger module init sdk-sdk `. Only the SDK-owned files are returned; the engine writes the module config and workspace entries when it drives `dagger module init`. By default the new module is created in the current workspace directory. Pass `path` to choose a different location. """ - pub initModule(ws: Workspace!, name: String!, path: String! = "."): Changeset! { + initModule(ws: Workspace!, name: String!, path: String! = "."): Changeset! { let modPath = cleanModulePath(path) let mainPath = if (modPath == ".") { "main.dang" } else { modPath + "/main.dang" } @@ -40,58 +65,149 @@ type SdkSdk { } """ - `initModule` should render CLI module names as a valid Dang root type. + Regenerate the modules this SDK manages. + + Dang SDK helper modules need no code generation, so this is a no-op. """ - pub initModuleRendersRootType(ws: Workspace!): Void @check { - let name = "my-sdk" - let modulePath = ".dagger/modules/" + name - let changes = initModule(ws, name, modulePath) - let mainPath = modulePath + "/main.dang" + generate: Changeset! @generate { + directory.changes(directory) + } - if (changes.addedPaths.contains(mainPath) == false) { - raise "initModule should add main.dang for a CLI-case module name" + """ + Return a black-box test target for an SDK module directory view. + """ + pub target(workspaceView: Directory!, sourceRootPath: String!): SdkTarget! { + SdkTarget( + workspaceView: workspaceView, + sourceRootPath: sourceRootPath, + daggerCliVersion: daggerCliVersion, + timeout: "10m", + ) + } + + """ + `dagger sdk install` should accept the SDK module. + """ + pub installRegistersSdk(ws: Workspace!): Void @check { + sdkTarget(ws).install.assertSuccess + } + + """ + `dagger sdk install` should mark the SDK with an as-sdk marker in dagger.toml. + """ + pub installMarksAsSdk(ws: Workspace!): Void @check { + let run = sdkTarget(ws).install + run.assertSuccess + if (run.workspaceFile("dagger.toml").contains("as-sdk") == false) { + raise "sdk install should record an as-sdk marker in dagger.toml" } - if (changes.after.file(mainPath).contents.contains("type MySdk {") == false) { - raise "initModule should render the CLI-case module name as a Dang root type" + } + + """ + `dagger module init ` should scaffold a new module. + """ + pub initScaffoldsModule(ws: Workspace!): Void @check { + sdkTarget(ws).initTestModule.assertSuccess + } + + """ + `dagger module init` should write the new module's dagger-module.toml. + """ + pub initWritesModuleConfig(ws: Workspace!): Void @check { + let testTarget = sdkTarget(ws) + let run = testTarget.initTestModule + run.assertSuccess + if (run.workspaceHasFile(testTarget.moduleConfigPath) == false) { + raise "module init should write " + testTarget.moduleConfigPath } } """ - `initModule` should not write engine-owned config files. + `dagger module init` should install the new module in dagger.toml. """ - pub initModuleDoesNotWriteEngineConfig(ws: Workspace!): Void @check { - let name = "sdk-contract-no-config" - let modulePath = ".dagger/modules/" + name - let changes = initModule(ws, name, modulePath) + pub initRegistersModule(ws: Workspace!): Void @check { + let testTarget = sdkTarget(ws) + let run = testTarget.initTestModule + run.assertSuccess + if (run.workspaceFile("dagger.toml").contains("[modules." + testTarget.moduleName + "]") == false) { + raise "module init should install the new module in dagger.toml" + } + } - if (changes.addedPaths.contains(modulePath + "/dagger.json")) { - raise "initModule should not write dagger.json; the engine owns module config" + """ + `dagger module init` should record the SDK as the new module's authoring SDK. + """ + pub initRecordsAuthoringSdk(ws: Workspace!): Void @check { + let testTarget = sdkTarget(ws) + let run = testTarget.initTestModule + run.assertSuccess + if (run.workspaceFile("dagger.toml").contains(".as-sdk.modules]]") == false) { + raise "module init should record an as-sdk.modules authoring entry in dagger.toml" } - if (changes.addedPaths.contains(modulePath + "/dagger-module.toml")) { - raise "initModule should not write dagger-module.toml; the engine owns module config" + } + + """ + `dagger generate` should succeed on a freshly scaffolded module. + """ + pub generateSucceeds(ws: Workspace!): Void @check { + sdkTarget(ws).generateWorkspace.assertSuccess + } + + """ + A scaffolded module should serve at least one function after `dagger generate`. + """ + pub scaffoldedModuleServesFunctions(ws: Workspace!): Void @check { + let testTarget = sdkTarget(ws) + let run = testTarget.run(["api", "functions", testTarget.moduleName]) + run.assertSuccess + if (run.stdout.trimSuffix("\n") == "") { + raise "a scaffolded module should expose at least one function" + } + } + + """ + `dagger sdk module-options` should introspect the SDK's initModule capability. + """ + pub sdkReportsModuleOptions(ws: Workspace!): Void @check { + let testTarget = sdkTarget(ws) + testTarget.runInstalled(["sdk", "module-options", testTarget.sdkInstallName]).assertSuccess + } + + """ + `dagger module engine required` should report a version for a scaffolded module. + """ + pub engineRequiredReportsVersion(ws: Workspace!): Void @check { + let run = sdkTarget(ws).runInModule(["module", "engine", "required"]) + run.assertSuccess + if (run.stdout.trimSuffix("\n") == "") { + raise "module engine required should report a version" } } + """ + `dagger module deps list` should succeed for a scaffolded module. + """ + pub depsListSucceeds(ws: Workspace!): Void @check { + sdkTarget(ws).runInModule(["module", "deps", "list"]).assertSuccess + } + """ `initModule` should seed SDK files for a new module. """ pub initModuleSeedsFiles(ws: Workspace!): Void @check { - let module = targetModule(ws) - - target(module).assertJsonListNotEmpty( + contractTarget(ws).assertJsonListNotEmpty( defaultInit + ["added-paths"], "initModule should seed at least one SDK file", ) } """ - `initModule` should not write engine-owned config in the black-box target. + `initModule` should not write engine-owned config files. """ pub initModuleDoesNotWriteConfig(ws: Workspace!): Void @check { - let module = targetModule(ws) - let testTarget = target(module) - + let testTarget = contractTarget(ws) let configBase = ".dagger/modules/" + defaultInitName + testTarget.assertJsonListNotContains( defaultInit + ["added-paths"], configBase + "/dagger.json", @@ -108,8 +224,7 @@ type SdkSdk { `initModule` should not remove existing files when creating a new module. """ pub initModuleDoesNotRemoveExistingFiles(ws: Workspace!): Void @check { - let module = targetModule(ws) - target(module).assertJsonListEmpty( + contractTarget(ws).assertJsonListEmpty( defaultInit + ["removed-paths"], "initModule should not remove existing files", ) @@ -119,11 +234,10 @@ type SdkSdk { `initModule --path` should stage files under the requested path. """ pub initModuleHonorsCustomPath(ws: Workspace!): Void @check { - let module = targetModule(ws) let name = "sdk-contract-custom-init" let modulePath = ".dagger/sdk-contract-custom-init" let customArgs = ["init-module", "--name", name, "--path", modulePath] - let testTarget = target(module) + let testTarget = contractTarget(ws) testTarget.assertJsonListNotEmpty( customArgs + ["added-paths"], @@ -140,49 +254,73 @@ type SdkSdk { } """ - An SDK helper module should expose a `@generate` hook. + An SDK module should expose a `@generate` hook. """ pub generateExposesGenerator(ws: Workspace!): Void @check { - let module = targetModule(ws) - - target(module).assertSuccess( - ["generate", "added-paths"], - "SDK helper module should expose a generate hook", - ) + let testTarget = sdkTarget(ws) + let run = testTarget.runInstalled(["generate", "-l"]) + run.assertSuccess + if (run.stdout.contains(testTarget.sdkInstallName + ":") == false) { + raise "SDK module should expose a @generate hook\ngenerators:\n" + run.stdout + } } """ - Return a black-box CLI target for a prepared module source. - - The mounted view keeps the target module's config at /work so the CLI can - load it from the workspace root. + sdk-sdk's own `initModule` should render CLI module names as a valid Dang root type. """ - let target(module: PolyfillModuleSource!): ModTestTarget! { - let view = module.workspaceView.withFile( - "dagger.json", - module.workspaceView.file(module.sourceRootPath + "/dagger.json"), - ) - modTest.target(view, module.sourceRootPath) + pub initModuleRendersRootType(ws: Workspace!): Void @check { + let name = "my-sdk" + let modulePath = ".dagger/modules/" + name + let changes = initModule(ws, name, modulePath) + let mainPath = modulePath + "/main.dang" + + if (changes.addedPaths.contains(mainPath) == false) { + raise "initModule should add main.dang for a CLI-case module name" + } + if (changes.after.file(mainPath).contents.contains("type MySdk {") == false) { + raise "initModule should render the CLI-case module name as a Dang root type" + } } """ - Return the caller module prepared by the workspace polyfill. + Return the SDK module under test prepared by the workspace polyfill. + """ + let sdkTarget(ws: Workspace!): SdkTarget! { + let module = sdkModule(ws) + target(module.workspaceView, module.sourceRootPath) + } - When run against sdk-sdk itself, target the bundled fixture instead. """ - let targetModule(ws: Workspace!): PolyfillModuleSource! { - let workspace = polyfill.workspace(ws) - let module = workspace.moduleSource(".") + Return a function-level contract target for the SDK module under test. - if (module.configExists == false) { - raise "no SDK module detected. Start with 'dagger module init sdk-sdk my-sdk', or initialize your own Dagger module" + The mounted view keeps the target module's config at the view root so the + CLI can load it from the workspace root. + """ + let contractTarget(ws: Workspace!): ModTestTarget! { + let module = sdkModule(ws) + let view = module.workspaceView + let configView = if (module.sourceRootPath == ".") { + view + } else if (view.exists(module.sourceRootPath + "/dagger.json")) { + view.withFile("dagger.json", view.file(module.sourceRootPath + "/dagger.json")) + } else { + view.withFile("dagger-module.toml", view.file(module.sourceRootPath + "/dagger-module.toml")) } - if (module.config.name == "sdk-sdk") { - workspace.moduleSource(".dagger/modules/sdk-sdk-e2e/fixtures/sdk-helper") - } else { - module + modTest.target(configView, module.sourceRootPath) + } + + """ + Return the SDK module at the workspace root. + """ + let sdkModule(ws: Workspace!): PolyfillModuleSource! { + let configs = ws.directory("/", include: ["dagger.json", "dagger-module.toml"]) + + if (configs.exists("dagger.json") == false and configs.exists("dagger-module.toml") == false) { + raise noSdkModuleMessage } + + polyfill.workspace(ws).moduleSource(".") } """ @@ -223,6 +361,334 @@ type SdkSdk { .file("/rendered/main.dang") } + # The engine always passes an explicit path when it drives initModule, so the + # contract checks do too. let defaultInitName: String! = "sdk-contract-init" - let defaultInit: [String!]! = ["init-module", "--name", defaultInitName] + let defaultInit: [String!]! = [ + "init-module", + "--name", + defaultInitName, + "--path", + ".dagger/modules/" + defaultInitName, + ] + let noSdkModuleMessage: String! = "no SDK module detected at the workspace root. Run from an SDK module workspace, or pass one with -W " +} + +""" +A Dagger SDK module under black-box test. + +The target vendors the SDK into a scratch git workspace inside a runner +container, installs a release Dagger CLI, then exercises the SDK through the +same commands a user would run. +""" +type SdkTarget { + let workspaceView: Directory! + let sourceRootPath: String! + let daggerCliVersion: String! + let timeout: String! + + """ + Workspace install name assigned to the SDK under test. + """ + pub sdkInstallName: String! = "sdk-under-test" + + """ + Name of the module scaffolded from the SDK under test. + """ + pub moduleName: String! = "test-mod" + + """ + Return a copy of this target with a different command timeout. + """ + pub withTimeout(timeout: String!): SdkTarget! { + SdkTarget( + workspaceView: workspaceView, + sourceRootPath: sourceRootPath, + daggerCliVersion: daggerCliVersion, + timeout: timeout, + ) + } + + """ + Workspace-relative path of the scaffolded module. + """ + pub modulePath: String! { + ".dagger/modules/" + moduleName + } + + """ + Workspace-relative path of the scaffolded module's config. + """ + pub moduleConfigPath: String! { + modulePath + "/dagger-module.toml" + } + + """ + Run `dagger sdk install` for the SDK under test and capture the result. + """ + pub install: SdkRun! { + runOn(workspace, "/work", installArgs) + } + + """ + Run `dagger module init` with the SDK under test and capture the result. + """ + pub initTestModule: SdkRun! { + runOn(installedState, "/work", initArgs) + } + + """ + Run `dagger generate` on the scaffolded module and capture the result. + """ + pub generateWorkspace: SdkRun! { + runOn(initializedState, "/work", generateArgs) + } + + """ + Run a dagger command from the workspace root after the SDK is installed. + """ + pub runInstalled(args: [String!]!): SdkRun! { + runOn(installedState, "/work", args) + } + + """ + Run a dagger command from the workspace root after a module is scaffolded + and generated. + """ + pub run(args: [String!]!): SdkRun! { + runOn(generatedState, "/work", args) + } + + """ + Run a dagger command from the scaffolded module directory after generation. + """ + pub runInModule(args: [String!]!): SdkRun! { + runOn(generatedState, "/work/" + modulePath, args) + } + + """ + Scratch git workspace with the SDK under test vendored in. + """ + let workspace: Container! { + runner + .withDirectory("/work/" + vendorRoot, workspaceView, exclude: [".git", "**/.git"]) + .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 scratch workspace", + ]) + } + + """ + Workspace state after `dagger sdk install`. + """ + let installedState: Container! { + step(workspace, installArgs) + } + + """ + Workspace state after `dagger module init`. + """ + let initializedState: Container! { + step(installedState, initArgs) + } + + """ + Workspace state after `dagger generate`. + """ + let generatedState: Container! { + step(initializedState, generateArgs) + } + + """ + Workspace-relative path of the vendored SDK module source. + """ + let sdkPath: String! { + if (sourceRootPath == ".") { vendorRoot } else { vendorRoot + "/" + sourceRootPath } + } + + let installArgs: [String!]! { + ["sdk", "install", "--name", sdkInstallName, "./" + sdkPath] + } + + let initArgs: [String!]! { + ["module", "init", sdkInstallName, moduleName] + } + + let generateArgs: [String!]! { + ["generate"] + } + + """ + Run a required dagger setup step, failing the pipeline on error. + """ + let step(state: Container!, args: [String!]!): Container! { + state.withExec( + [ + "sh", + "-c", + "timeout_arg=$1; shift 1; exec timeout \"$timeout_arg\" /usr/local/bin/dagger --progress plain -y \"$@\"", + "sdk-sdk-step", + timeout, + ] + args, + experimentalPrivilegedNesting: true, + ) + } + + """ + Run a dagger command and capture stdout, stderr, and exit code. + """ + let runOn(state: Container!, workdir: String!, args: [String!]!): SdkRun! { + SdkRun( + container: state.withWorkdir(workdir).withExec( + [ + "sh", + "-c", + "mkdir -p /tmp/sdk-sdk; timeout_arg=$1; shift 1; timeout \"$timeout_arg\" /usr/local/bin/dagger --progress plain -y \"$@\" > /tmp/sdk-sdk/stdout 2> /tmp/sdk-sdk/stderr; code=$?; printf '%s' \"$code\" > /tmp/sdk-sdk/exit-code; exit 0", + "sdk-sdk-run", + timeout, + ] + args, + experimentalPrivilegedNesting: true, + ), + args: args, + ) + } + + """ + Container with a release Dagger CLI and git installed. + """ + let runner: Container! { + container(platform: runnerPlatform) + .from("alpine:3.22") + .withoutEntrypoint + .withExec(["apk", "add", "--no-cache", "git"]) + .withFile("/tmp/" + daggerCliArchiveName, daggerCliArchive) + .withFile("/tmp/checksums.txt", daggerCliChecksums) + .withExec([ + "sh", + "-c", + "archive=$1; cd /tmp; line=$(awk -v f=\"$archive\" '$2 == f { print }' checksums.txt); test -n \"$line\"; printf '%s\n' \"$line\" | sha256sum -c -; tar -xzf \"$archive\" -C /usr/local/bin dagger; chmod +x /usr/local/bin/dagger", + "install-dagger-cli", + daggerCliArchiveName, + ]) + } + + let vendorRoot: String! = "vendor/sdk-workspace" + + """ + Resolved Dagger CLI release version without a leading "v". + """ + let daggerCliReleaseVersion: String! { + if (daggerCliVersion == "latest") { + http(url: "https://dl.dagger.io/dagger/versions/latest").contents.trimSuffix("\n").trimPrefix("v") + } else { + daggerCliVersion.trimPrefix("v") + } + } + + """ + Dagger CLI release archive name for the runner platform. + """ + let daggerCliArchiveName: String! { + "dagger_v" + daggerCliReleaseVersion + "_" + runnerOS + "_" + runnerArch + ".tar.gz" + } + + """ + Dagger CLI release archive. + """ + let daggerCliArchive: File! { + http(url: daggerCliReleaseURL + "/" + daggerCliArchiveName) + } + + """ + Dagger CLI release checksum file. + """ + let daggerCliChecksums: File! { + http(url: daggerCliReleaseURL + "/checksums.txt") + } + + """ + Dagger CLI release URL. + """ + let daggerCliReleaseURL: String! { + "https://dl.dagger.io/dagger/releases/" + daggerCliReleaseVersion + } + + let runnerPlatform: Platform! = "linux/amd64" + let runnerOS: String! = "linux" + let runnerArch: String! = "amd64" +} + +""" +Result of a black-box dagger CLI command against an SDK under test. +""" +type SdkRun { + let container: Container! + let args: [String!]! + + """ + The command stdout. + """ + pub stdout: String! { + container.file("/tmp/sdk-sdk/stdout").contents + } + + """ + The command stderr. + """ + pub stderr: String! { + container.file("/tmp/sdk-sdk/stderr").contents + } + + """ + The command exit code as decimal text. + """ + pub exitCode: String! { + container.file("/tmp/sdk-sdk/exit-code").contents + } + + """ + Whether the command exited successfully. + """ + pub succeeded: Boolean! { + exitCode == "0" + } + + """ + Fail when the command did not exit successfully. + """ + pub assertSuccess: Void { + if (succeeded == false) { + raise "dagger command failed: " + toJSON(args) + "\nstderr:\n" + stderr + } + null + } + + """ + Fail when the command exited successfully. + """ + pub assertFailure: Void { + if (succeeded) { + raise "dagger command unexpectedly succeeded: " + toJSON(args) + } + null + } + + """ + Read a workspace file after the command ran. + """ + pub workspaceFile(path: String!): String! { + container.file("/work/" + path).contents + } + + """ + Return true when a workspace file exists after the command ran. + """ + pub workspaceHasFile(path: String!): Boolean! { + container.directory("/work").exists(path) + } } diff --git a/sdk-test/README.md b/sdk-test/README.md deleted file mode 100644 index b931d81..0000000 --- a/sdk-test/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# sdk-test - -Black-box contract checks for Dagger SDK modules, such as -`github.com/dagger/go-sdk`, `github.com/dagger/dang-sdk`, -`github.com/dagger/typescript-sdk`, and `github.com/dagger/python-sdk`. - -Where `mod-test` calls one module's functions, `sdk-test` exercises an SDK the -way a user does across its whole lifecycle. The checks vendor the SDK module -into a scratch git workspace inside a runner container, install a release -Dagger CLI, then drive the SDK through real CLI commands: - -- `dagger sdk install ./` registers the SDK and marks it `as-sdk` in - `dagger.toml`. -- `dagger module init test-mod` scaffolds a new module, writes its - `dagger-module.toml`, installs it in `dagger.toml`, and records the SDK as - the module's authoring SDK. -- `dagger generate` succeeds on the fresh scaffold. -- After generation the scaffolded module serves functions: - `dagger api functions test-mod`. -- `dagger sdk module-options ` introspects the SDK's `initModule` - capability. -- `dagger module engine required` and `dagger module deps list` work from the - scaffolded module directory. - -Run the checks against an SDK repository: - -```sh -dagger -m github.com/dagger/sdk-sdk/sdk-test -W check -``` - -For example: - -```sh -dagger -m . -W https://github.com/dagger/go-sdk check -``` - -When run inside the sdk-sdk repository itself, the checks redirect to the -fixture SDK at `.dagger/modules/sdk-test-e2e/fixtures/sdk-under-test`, so -`dagger -m ./sdk-test -W . check` self-tests the harness. - -Configure the CLI release with the top-level `dagger-cli-version` setting; the -default is `1.0.0-beta.7`. Individual targets accept `with-timeout` for slow -SDKs (the default command timeout is `10m`). - -Custom checks can reuse the harness through `target`: - -```dang -let testTarget = sdkTest.target(module.workspaceView, module.sourceRootPath) -testTarget.install.assertSuccess -testTarget.runInModule(["module", "deps", "list"]).assertSuccess -``` diff --git a/sdk-test/dagger-module.toml b/sdk-test/dagger-module.toml deleted file mode 100644 index 27cf17e..0000000 --- a/sdk-test/dagger-module.toml +++ /dev/null @@ -1,9 +0,0 @@ -name = "sdk-test" -engineVersion = "v1.0.0-0" - -[runtime] - source = "dang" - -[[dependencies]] - name = "polyfill" - source = "../polyfill" diff --git a/sdk-test/sdk-test.dang b/sdk-test/sdk-test.dang deleted file mode 100644 index 43949c2..0000000 --- a/sdk-test/sdk-test.dang +++ /dev/null @@ -1,481 +0,0 @@ -""" -Black-box contract checks for Dagger SDK modules. - -Run against an SDK repository with: - - dagger -m github.com/dagger/sdk-sdk/sdk-test -W check - -The checks vendor the SDK module into a scratch git workspace, then drive it -the way a user would through a release Dagger CLI: `dagger sdk install`, -`dagger module init`, and the `dagger module` authoring verbs. -""" -type SdkTest { - """ - Dagger CLI release version used by black-box tests. - """ - pub daggerCliVersion: String! = "1.0.0-beta.7" - - """ - Return a black-box test target for an SDK module directory view. - """ - pub target(workspaceView: Directory!, sourceRootPath: String!): SdkTestTarget! { - SdkTestTarget( - workspaceView: workspaceView, - sourceRootPath: sourceRootPath, - daggerCliVersion: daggerCliVersion, - timeout: "10m", - ) - } - - """ - `dagger sdk install` should accept the SDK module. - """ - pub installRegistersSdk(ws: Workspace!): Void @check { - sdkTarget(ws).install.assertSuccess - } - - """ - `dagger sdk install` should mark the SDK with an as-sdk marker in dagger.toml. - """ - pub installMarksAsSdk(ws: Workspace!): Void @check { - let run = sdkTarget(ws).install - run.assertSuccess - if (run.workspaceFile("dagger.toml").contains("as-sdk") == false) { - raise "sdk install should record an as-sdk marker in dagger.toml" - } - } - - """ - `dagger module init ` should scaffold a new module. - """ - pub initScaffoldsModule(ws: Workspace!): Void @check { - sdkTarget(ws).initModule.assertSuccess - } - - """ - `dagger module init` should write the new module's dagger-module.toml. - """ - pub initWritesModuleConfig(ws: Workspace!): Void @check { - let testTarget = sdkTarget(ws) - let run = testTarget.initModule - run.assertSuccess - if (run.workspaceHasFile(testTarget.moduleConfigPath) == false) { - raise "module init should write " + testTarget.moduleConfigPath - } - } - - """ - `dagger module init` should install the new module in dagger.toml. - """ - pub initRegistersModule(ws: Workspace!): Void @check { - let testTarget = sdkTarget(ws) - let run = testTarget.initModule - run.assertSuccess - if (run.workspaceFile("dagger.toml").contains("[modules." + testTarget.moduleName + "]") == false) { - raise "module init should install the new module in dagger.toml" - } - } - - """ - `dagger module init` should record the SDK as the new module's authoring SDK. - """ - pub initRecordsAuthoringSdk(ws: Workspace!): Void @check { - let testTarget = sdkTarget(ws) - let run = testTarget.initModule - run.assertSuccess - if (run.workspaceFile("dagger.toml").contains(".as-sdk.modules]]") == false) { - raise "module init should record an as-sdk.modules authoring entry in dagger.toml" - } - } - - """ - `dagger generate` should succeed on a freshly scaffolded module. - """ - pub generateSucceeds(ws: Workspace!): Void @check { - sdkTarget(ws).generate.assertSuccess - } - - """ - A scaffolded module should serve at least one function after `dagger generate`. - """ - pub scaffoldedModuleServesFunctions(ws: Workspace!): Void @check { - let testTarget = sdkTarget(ws) - let run = testTarget.run(["api", "functions", testTarget.moduleName]) - run.assertSuccess - if (run.stdout.trimSuffix("\n") == "") { - raise "a scaffolded module should expose at least one function" - } - } - - """ - `dagger sdk module-options` should introspect the SDK's initModule capability. - """ - pub sdkReportsModuleOptions(ws: Workspace!): Void @check { - let testTarget = sdkTarget(ws) - testTarget.runInstalled(["sdk", "module-options", testTarget.sdkInstallName]).assertSuccess - } - - """ - `dagger module engine required` should report a version for a scaffolded module. - """ - pub engineRequiredReportsVersion(ws: Workspace!): Void @check { - let run = sdkTarget(ws).runInModule(["module", "engine", "required"]) - run.assertSuccess - if (run.stdout.trimSuffix("\n") == "") { - raise "module engine required should report a version" - } - } - - """ - `dagger module deps list` should succeed for a scaffolded module. - """ - pub depsListSucceeds(ws: Workspace!): Void @check { - sdkTarget(ws).runInModule(["module", "deps", "list"]).assertSuccess - } - - """ - Return the SDK module under test prepared by the workspace polyfill. - - When run inside the sdk-sdk repository itself, redirect to the checked-in - fixture SDK so the checks stay self-testing. - """ - let sdkTarget(ws: Workspace!): SdkTestTarget! { - let workspace = polyfill.workspace(ws) - let module = workspace.moduleSource(".") - - if (module.configExists == false) { - raise noSdkModuleMessage - } - - let resolved = if (selfModules.filter { name => name == module.config.name }.length > 0) { - workspace.moduleSource(selfFixturePath) - } else { - module - } - - target(resolved.workspaceView, resolved.sourceRootPath) - } - - let selfModules: [String!]! = ["sdk-sdk", "sdk-test"] - let selfFixturePath: String! = ".dagger/modules/sdk-test-e2e/fixtures/sdk-under-test" - let noSdkModuleMessage: String! = "no SDK module detected. Run from an SDK module workspace, or pass one with -W " -} - -""" -A Dagger SDK module under black-box test. - -The target vendors the SDK into a scratch git workspace inside a runner -container, installs a release Dagger CLI, then exercises the SDK through the -same commands a user would run. -""" -type SdkTestTarget { - let workspaceView: Directory! - let sourceRootPath: String! - let daggerCliVersion: String! - let timeout: String! - - """ - Workspace install name assigned to the SDK under test. - """ - pub sdkInstallName: String! = "sdk-under-test" - - """ - Name of the module scaffolded from the SDK under test. - """ - pub moduleName: String! = "test-mod" - - """ - Return a copy of this target with a different command timeout. - """ - pub withTimeout(timeout: String!): SdkTestTarget! { - SdkTestTarget( - workspaceView: workspaceView, - sourceRootPath: sourceRootPath, - daggerCliVersion: daggerCliVersion, - timeout: timeout, - ) - } - - """ - Workspace-relative path of the scaffolded module. - """ - pub modulePath: String! { - ".dagger/modules/" + moduleName - } - - """ - Workspace-relative path of the scaffolded module's config. - """ - pub moduleConfigPath: String! { - modulePath + "/dagger-module.toml" - } - - """ - Run `dagger sdk install` for the SDK under test and capture the result. - """ - pub install: SdkTestRun! { - runOn(workspace, "/work", installArgs) - } - - """ - Run `dagger module init` with the SDK under test and capture the result. - """ - pub initModule: SdkTestRun! { - runOn(installedState, "/work", initArgs) - } - - """ - Run `dagger generate` on the scaffolded module and capture the result. - """ - pub generate: SdkTestRun! { - runOn(initializedState, "/work", generateArgs) - } - - """ - Run a dagger command from the workspace root after the SDK is installed. - """ - pub runInstalled(args: [String!]!): SdkTestRun! { - runOn(installedState, "/work", args) - } - - """ - Run a dagger command from the workspace root after a module is scaffolded - and generated. - """ - pub run(args: [String!]!): SdkTestRun! { - runOn(generatedState, "/work", args) - } - - """ - Run a dagger command from the scaffolded module directory after generation. - """ - pub runInModule(args: [String!]!): SdkTestRun! { - runOn(generatedState, "/work/" + modulePath, args) - } - - """ - Scratch git workspace with the SDK under test vendored in. - """ - let workspace: Container! { - runner - .withDirectory("/work/" + vendorRoot, workspaceView, exclude: [".git", "**/.git"]) - .withWorkdir("/work") - .withExec(["git", "init", "-q"]) - .withExec(["git", "add", "-A"]) - .withExec([ - "git", - "-c", "user.email=sdk-test@dagger.io", - "-c", "user.name=sdk-test", - "commit", "-q", "--allow-empty", "-m", "sdk-test workspace", - ]) - } - - """ - Workspace state after `dagger sdk install`. - """ - let installedState: Container! { - step(workspace, installArgs) - } - - """ - Workspace state after `dagger module init`. - """ - let initializedState: Container! { - step(installedState, initArgs) - } - - """ - Workspace state after `dagger generate`. - """ - let generatedState: Container! { - step(initializedState, generateArgs) - } - - """ - Workspace-relative path of the vendored SDK module source. - """ - let sdkPath: String! { - if (sourceRootPath == ".") { vendorRoot } else { vendorRoot + "/" + sourceRootPath } - } - - let installArgs: [String!]! { - ["sdk", "install", "--name", sdkInstallName, "./" + sdkPath] - } - - let initArgs: [String!]! { - ["module", "init", sdkInstallName, moduleName] - } - - let generateArgs: [String!]! { - ["generate"] - } - - """ - Run a required dagger setup step, failing the pipeline on error. - """ - let step(state: Container!, args: [String!]!): Container! { - state.withExec( - [ - "sh", - "-c", - "timeout_arg=$1; shift 1; exec timeout \"$timeout_arg\" /usr/local/bin/dagger --progress plain -y \"$@\"", - "sdk-test-step", - timeout, - ] + args, - experimentalPrivilegedNesting: true, - ) - } - - """ - Run a dagger command and capture stdout, stderr, and exit code. - """ - let runOn(state: Container!, workdir: String!, args: [String!]!): SdkTestRun! { - SdkTestRun( - container: state.withWorkdir(workdir).withExec( - [ - "sh", - "-c", - "mkdir -p /tmp/sdk-test; timeout_arg=$1; shift 1; timeout \"$timeout_arg\" /usr/local/bin/dagger --progress plain -y \"$@\" > /tmp/sdk-test/stdout 2> /tmp/sdk-test/stderr; code=$?; printf '%s' \"$code\" > /tmp/sdk-test/exit-code; exit 0", - "sdk-test-run", - timeout, - ] + args, - experimentalPrivilegedNesting: true, - ), - args: args, - ) - } - - """ - Container with a release Dagger CLI and git installed. - """ - let runner: Container! { - container(platform: runnerPlatform) - .from("alpine:3.22") - .withoutEntrypoint - .withExec(["apk", "add", "--no-cache", "git"]) - .withFile("/tmp/" + daggerCliArchiveName, daggerCliArchive) - .withFile("/tmp/checksums.txt", daggerCliChecksums) - .withExec([ - "sh", - "-c", - "archive=$1; cd /tmp; line=$(awk -v f=\"$archive\" '$2 == f { print }' checksums.txt); test -n \"$line\"; printf '%s\n' \"$line\" | sha256sum -c -; tar -xzf \"$archive\" -C /usr/local/bin dagger; chmod +x /usr/local/bin/dagger", - "install-dagger-cli", - daggerCliArchiveName, - ]) - } - - let vendorRoot: String! = "vendor/sdk-workspace" - - """ - Resolved Dagger CLI release version without a leading "v". - """ - let daggerCliReleaseVersion: String! { - if (daggerCliVersion == "latest") { - http(url: "https://dl.dagger.io/dagger/versions/latest").contents.trimSuffix("\n").trimPrefix("v") - } else { - daggerCliVersion.trimPrefix("v") - } - } - - """ - Dagger CLI release archive name for the runner platform. - """ - let daggerCliArchiveName: String! { - "dagger_v" + daggerCliReleaseVersion + "_" + runnerOS + "_" + runnerArch + ".tar.gz" - } - - """ - Dagger CLI release archive. - """ - let daggerCliArchive: File! { - http(url: daggerCliReleaseURL + "/" + daggerCliArchiveName) - } - - """ - Dagger CLI release checksum file. - """ - let daggerCliChecksums: File! { - http(url: daggerCliReleaseURL + "/checksums.txt") - } - - """ - Dagger CLI release URL. - """ - let daggerCliReleaseURL: String! { - "https://dl.dagger.io/dagger/releases/" + daggerCliReleaseVersion - } - - let runnerPlatform: Platform! = "linux/amd64" - let runnerOS: String! = "linux" - let runnerArch: String! = "amd64" -} - -""" -Result of a black-box dagger CLI command against an SDK under test. -""" -type SdkTestRun { - let container: Container! - let args: [String!]! - - """ - The command stdout. - """ - pub stdout: String! { - container.file("/tmp/sdk-test/stdout").contents - } - - """ - The command stderr. - """ - pub stderr: String! { - container.file("/tmp/sdk-test/stderr").contents - } - - """ - The command exit code as decimal text. - """ - pub exitCode: String! { - container.file("/tmp/sdk-test/exit-code").contents - } - - """ - Whether the command exited successfully. - """ - pub succeeded: Boolean! { - exitCode == "0" - } - - """ - Fail when the command did not exit successfully. - """ - pub assertSuccess: Void { - if (succeeded == false) { - raise "dagger command failed: " + toJSON(args) + "\nstderr:\n" + stderr - } - null - } - - """ - Fail when the command exited successfully. - """ - pub assertFailure: Void { - if (succeeded) { - raise "dagger command unexpectedly succeeded: " + toJSON(args) - } - null - } - - """ - Read a workspace file after the command ran. - """ - pub workspaceFile(path: String!): String! { - container.file("/work/" + path).contents - } - - """ - Return true when a workspace file exists after the command ran. - """ - pub workspaceHasFile(path: String!): Boolean! { - container.directory("/work").exists(path) - } -} From 5e35d672016c29f74947fbb45d4b5c2573a75efd Mon Sep 17 00:00:00 2001 From: kpenfound Date: Tue, 28 Jul 2026 13:39:26 -0400 Subject: [PATCH 3/6] feat: attribute pipeline failures to their stage in the check report Run every lifecycle command (sdk install, module init, generate, and the per-check commands) through a single capture wrapper. When a command fails, later commands in the pipeline are skipped and record a marker; their checks then fail with a "prerequisite command failed" message naming the broken stage and its stderr, while the stage's own check reports the actual error. The check report now shows exactly which stage broke and which behaviors were blocked by it, instead of repeating raw nested exec errors. Stage states and their checks now share the same execs, so the pipeline no longer runs install/init twice per pass. Co-Authored-By: Claude Fable 5 Signed-off-by: kpenfound --- README.md | 6 ++++ sdk-sdk.dang | 82 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 95cc2ef..11747b0 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,12 @@ write engine-owned config (`dagger.json` / `dagger-module.toml`), and must not remove existing files. The SDK must also list a `@generate` hook in `dagger generate -l`. +Each lifecycle stage and each contract behavior is its own check, so the check +report shows exactly what passed and what failed. All commands run through one +pipeline: when a stage fails, its own check captures the error, and the checks +that depend on it fail with a `prerequisite command failed` message naming that +stage instead of repeating raw errors. + Configure the CLI release with the top-level `dagger-cli-version` setting; the default is `1.0.0-beta.7`. Individual targets accept `with-timeout` for slow SDKs (the default command timeout is `10m`). Custom checks can reuse the diff --git a/sdk-sdk.dang b/sdk-sdk.dang index 1816eee..5d2b400 100644 --- a/sdk-sdk.dang +++ b/sdk-sdk.dang @@ -427,21 +427,21 @@ type SdkTarget { Run `dagger sdk install` for the SDK under test and capture the result. """ pub install: SdkRun! { - runOn(workspace, "/work", installArgs) + SdkRun(container: installedState, args: installArgs) } """ Run `dagger module init` with the SDK under test and capture the result. """ pub initTestModule: SdkRun! { - runOn(installedState, "/work", initArgs) + SdkRun(container: initializedState, args: initArgs) } """ Run `dagger generate` on the scaffolded module and capture the result. """ pub generateWorkspace: SdkRun! { - runOn(initializedState, "/work", generateArgs) + SdkRun(container: generatedState, args: generateArgs) } """ @@ -487,21 +487,21 @@ type SdkTarget { Workspace state after `dagger sdk install`. """ let installedState: Container! { - step(workspace, installArgs) + pipe(workspace, "/work", installArgs) } """ Workspace state after `dagger module init`. """ let initializedState: Container! { - step(installedState, initArgs) + pipe(installedState, "/work", initArgs) } """ Workspace state after `dagger generate`. """ let generatedState: Container! { - step(initializedState, generateArgs) + pipe(initializedState, "/work", generateArgs) } """ @@ -524,40 +524,32 @@ type SdkTarget { } """ - Run a required dagger setup step, failing the pipeline on error. + Run a dagger command on a workspace state and capture the result. """ - let step(state: Container!, args: [String!]!): Container! { - state.withExec( + let runOn(state: Container!, workdir: String!, args: [String!]!): SdkRun! { + SdkRun(container: pipe(state, workdir, args), args: args) + } + + """ + Run a dagger command and capture stdout, stderr, and exit code. + + Every command runs through the same wrapper. When an earlier pipeline command + failed, later commands are skipped and record a `skipped` marker instead, so + each check reports either its own failure or a clear prerequisite failure. + """ + let pipe(state: Container!, workdir: String!, args: [String!]!): Container! { + state.withWorkdir(workdir).withExec( [ "sh", "-c", - "timeout_arg=$1; shift 1; exec timeout \"$timeout_arg\" /usr/local/bin/dagger --progress plain -y \"$@\"", - "sdk-sdk-step", + "mkdir -p /tmp/sdk-sdk; timeout_arg=$1; shift 1; if [ -f /tmp/sdk-sdk/failed-command ]; then touch /tmp/sdk-sdk/skipped; exit 0; fi; timeout \"$timeout_arg\" /usr/local/bin/dagger --progress plain -y \"$@\" > /tmp/sdk-sdk/stdout 2> /tmp/sdk-sdk/stderr; code=$?; printf '%s' \"$code\" > /tmp/sdk-sdk/exit-code; if [ \"$code\" -ne 0 ]; then printf 'dagger %s' \"$*\" > /tmp/sdk-sdk/failed-command; cp /tmp/sdk-sdk/stderr /tmp/sdk-sdk/failed-stderr; fi; exit 0", + "sdk-sdk-run", timeout, ] + args, experimentalPrivilegedNesting: true, ) } - """ - Run a dagger command and capture stdout, stderr, and exit code. - """ - let runOn(state: Container!, workdir: String!, args: [String!]!): SdkRun! { - SdkRun( - container: state.withWorkdir(workdir).withExec( - [ - "sh", - "-c", - "mkdir -p /tmp/sdk-sdk; timeout_arg=$1; shift 1; timeout \"$timeout_arg\" /usr/local/bin/dagger --progress plain -y \"$@\" > /tmp/sdk-sdk/stdout 2> /tmp/sdk-sdk/stderr; code=$?; printf '%s' \"$code\" > /tmp/sdk-sdk/exit-code; exit 0", - "sdk-sdk-run", - timeout, - ] + args, - experimentalPrivilegedNesting: true, - ), - args: args, - ) - } - """ Container with a release Dagger CLI and git installed. """ @@ -658,10 +650,23 @@ type SdkRun { exitCode == "0" } + """ + Whether the command was skipped because an earlier pipeline command failed. + """ + pub skipped: Boolean! { + container.directory("/tmp/sdk-sdk").exists("skipped") + } + """ Fail when the command did not exit successfully. + + When the command never ran because an earlier pipeline command failed, report + that prerequisite failure instead. """ pub assertSuccess: Void { + if (skipped) { + raise "prerequisite command failed: " + failedCommand + "\nstderr:\n" + failedStderr + } if (succeeded == false) { raise "dagger command failed: " + toJSON(args) + "\nstderr:\n" + stderr } @@ -672,6 +677,9 @@ type SdkRun { Fail when the command exited successfully. """ pub assertFailure: Void { + if (skipped) { + raise "prerequisite command failed: " + failedCommand + "\nstderr:\n" + failedStderr + } if (succeeded) { raise "dagger command unexpectedly succeeded: " + toJSON(args) } @@ -691,4 +699,18 @@ type SdkRun { pub workspaceHasFile(path: String!): Boolean! { container.directory("/work").exists(path) } + + """ + The first failed pipeline command, as recorded by the run wrapper. + """ + let failedCommand: String! { + container.file("/tmp/sdk-sdk/failed-command").contents + } + + """ + The stderr of the first failed pipeline command. + """ + let failedStderr: String! { + container.file("/tmp/sdk-sdk/failed-stderr").contents + } } From 91c2b9d6159a50b155b73f5f1c960fdf2c7f2a87 Mon Sep 17 00:00:00 2001 From: kpenfound Date: Wed, 29 Jul 2026 11:22:08 -0400 Subject: [PATCH 4/6] feat!: standardize on the 1.0.0-beta.8 CLI Bump the pinned CLI release default to 1.0.0-beta.8 in both sdk-sdk and mod-test ahead of the beta.8 release, and cut mod-test over to the CLI 1.0 command surface: `dagger api call -j` replaces `dagger call -j`, which was removed in beta.7. mod-test was the last piece pinned to a pre-beta.7 CLI for the old verb. The full suite (21/21 checks, including mod-test-e2e and the contract checks that route through mod-test) passes with the new command surface on the beta.7 CLI; runs will fail on the CLI download until beta.8 artifacts are published, at which point no further change is expected beyond re-validating the official SDKs. Co-Authored-By: Claude Fable 5 Signed-off-by: kpenfound --- README.md | 2 +- mod-test/README.md | 4 ++-- mod-test/mod-test.dang | 10 +++++----- sdk-sdk.dang | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 11747b0..9a9b5d1 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ that depend on it fail with a `prerequisite command failed` message naming that stage instead of repeating raw errors. Configure the CLI release with the top-level `dagger-cli-version` setting; the -default is `1.0.0-beta.7`. Individual targets accept `with-timeout` for slow +default is `1.0.0-beta.8`. Individual targets accept `with-timeout` for slow SDKs (the default command timeout is `10m`). Custom checks can reuse the harness through `target`: diff --git a/mod-test/README.md b/mod-test/README.md index fe2d06b..e10e669 100644 --- a/mod-test/README.md +++ b/mod-test/README.md @@ -3,11 +3,11 @@ Lightweight black-box testing helpers for Dagger modules. `mod-test` mounts a workspace-rooted directory view, installs a Dagger CLI -release from `dl.dagger.io`, then runs readable `dagger call -j` commands +release from `dl.dagger.io`, then runs readable `dagger api call -j` commands against the target module. Configure the CLI release with the top-level `dagger-cli-version` setting. The -default is `latest`. +default is `1.0.0-beta.8`. Callers provide: diff --git a/mod-test/mod-test.dang b/mod-test/mod-test.dang index bbb3841..9b20425 100644 --- a/mod-test/mod-test.dang +++ b/mod-test/mod-test.dang @@ -5,7 +5,7 @@ type ModTest { """ Dagger CLI release version used by black-box tests. """ - pub daggerCliVersion: String! = "1.0.0-beta.5" + pub daggerCliVersion: String! = "1.0.0-beta.8" """ Return a black-box test target for a workspace-rooted module directory view. @@ -42,7 +42,7 @@ type ModTestTarget { } """ - Run `dagger call -j` against the target module and require success. + Run `dagger api call -j` against the target module and require success. """ pub call(args: [String!]!): ModTestCall! { let result = runCall(args) @@ -51,7 +51,7 @@ type ModTestTarget { } """ - Run `dagger call -j` against the target module and capture failures. + Run `dagger api call -j` against the target module and capture failures. """ pub tryCall(args: [String!]!): ModTestCall! { runCall(args) @@ -175,7 +175,7 @@ type ModTestTarget { } """ - Run `dagger call -j` against the target module. + Run `dagger api call -j` against the target module. """ let runCall(args: [String!]!): ModTestCall! { ModTestCall( @@ -183,7 +183,7 @@ type ModTestTarget { [ "sh", "-c", - "mkdir -p /tmp/mod-test; timeout_arg=$1; module=$2; shift 2; timeout \"$timeout_arg\" /usr/local/bin/dagger --progress plain call -j -m \"$module\" \"$@\" > /tmp/mod-test/stdout 2> /tmp/mod-test/stderr; code=$?; printf '%s' \"$code\" > /tmp/mod-test/exit-code; exit 0", + "mkdir -p /tmp/mod-test; timeout_arg=$1; module=$2; shift 2; timeout \"$timeout_arg\" /usr/local/bin/dagger --progress plain api call -j -m \"$module\" \"$@\" > /tmp/mod-test/stdout 2> /tmp/mod-test/stderr; code=$?; printf '%s' \"$code\" > /tmp/mod-test/exit-code; exit 0", "mod-test-run", timeout, "/work/" + sourceRootPath, diff --git a/sdk-sdk.dang b/sdk-sdk.dang index 5d2b400..697ba4f 100644 --- a/sdk-sdk.dang +++ b/sdk-sdk.dang @@ -33,7 +33,7 @@ type SdkSdk { """ Dagger CLI release version used by black-box tests. """ - pub daggerCliVersion: String! = "1.0.0-beta.7" + pub daggerCliVersion: String! = "1.0.0-beta.8" """ Engine runtime recorded for modules created with this SDK. From 25866c9752d32ee49e0719d9c8b27f0b1f1aae01 Mon Sep 17 00:00:00 2001 From: kpenfound Date: Thu, 30 Jul 2026 13:27:57 -0400 Subject: [PATCH 5/6] feat: check that generate is anchored at the caller's cwd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a generate-respects-cwd check encoding the cwd contract from go-sdk's respect_cwd work: generation from inside one module's directory acts on the module cone at or below that directory, so a sibling module elsewhere in the workspace must be left untouched. The harness scaffolds a second module (sibling-mod), commits the scratch workspace as a git baseline, runs `dagger generate` from inside the first module's directory, and asserts via git status that the sibling saw no changes. For SDKs with no-op generation the check passes vacuously; for codegen SDKs it catches cwd-ignoring generate implementations. SdkRun gains workspaceChanges(path) for git-based drift inspection. Verified on a dev main engine (beta.8 preview): 22/22 in-repo, 19/19 against dang-sdk, and 17/18 against go-sdk main — the one failure is a genuine go-sdk finding (a scaffolded module serves no functions after init + generate). Co-Authored-By: Claude Fable 5 Signed-off-by: kpenfound --- README.md | 3 ++ sdk-sdk.dang | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/README.md b/README.md index 9a9b5d1..2911d7a 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ commands the way a user would: capability. - `dagger module engine required` and `dagger module deps list` work from the scaffolded module directory. +- `dagger generate` is anchored at the caller's cwd: run from inside one + module's directory with a sibling module scaffolded elsewhere, the sibling + is left untouched. Function-level contract checks additionally call the SDK's `initModule` directly (always with an explicit `--path`, as the engine does) and inspect diff --git a/sdk-sdk.dang b/sdk-sdk.dang index 697ba4f..df7468b 100644 --- a/sdk-sdk.dang +++ b/sdk-sdk.dang @@ -191,6 +191,24 @@ type SdkSdk { sdkTarget(ws).runInModule(["module", "deps", "list"]).assertSuccess } + """ + `dagger generate` should be anchored at the caller's cwd. + + Generation from inside one module's directory acts on the module cone at or + below that directory, so a sibling module elsewhere in the workspace must be + left untouched. + """ + pub generateRespectsCwd(ws: Workspace!): Void @check { + let testTarget = sdkTarget(ws) + let run = testTarget.generateInModuleDir + run.assertSuccess + + let drift = run.workspaceChanges(testTarget.siblingModulePath) + if (drift != "") { + raise "generate from " + testTarget.modulePath + " should not touch the sibling module " + testTarget.siblingModulePath + "\nchanged:\n" + drift + } + } + """ `initModule` should seed SDK files for a new module. """ @@ -397,6 +415,11 @@ type SdkTarget { """ pub moduleName: String! = "test-mod" + """ + Name of the second module scaffolded for cwd-awareness checks. + """ + pub siblingModuleName: String! = "sibling-mod" + """ Return a copy of this target with a different command timeout. """ @@ -423,6 +446,13 @@ type SdkTarget { modulePath + "/dagger-module.toml" } + """ + Workspace-relative path of the sibling module. + """ + pub siblingModulePath: String! { + ".dagger/modules/" + siblingModuleName + } + """ Run `dagger sdk install` for the SDK under test and capture the result. """ @@ -444,6 +474,15 @@ type SdkTarget { SdkRun(container: generatedState, args: generateArgs) } + """ + Run `dagger generate` from inside the first module's directory, with a + sibling module scaffolded elsewhere and the workspace committed beforehand, + and capture the result. + """ + pub generateInModuleDir: SdkRun! { + SdkRun(container: cwdGeneratedState, args: generateArgs) + } + """ Run a dagger command from the workspace root after the SDK is installed. """ @@ -504,6 +543,35 @@ type SdkTarget { pipe(initializedState, "/work", generateArgs) } + """ + Workspace state with a sibling module scaffolded next to the first one. + """ + let siblingInitializedState: Container! { + pipe(initializedState, "/work", ["module", "init", sdkInstallName, siblingModuleName]) + } + + """ + Sibling workspace state with every file committed, so later git status calls + see only the changes made after this point. + """ + let cwdSnapshotState: Container! { + siblingInitializedState + .withExec(["git", "add", "-A"]) + .withExec([ + "git", + "-c", "user.email=sdk-sdk@dagger.io", + "-c", "user.name=sdk-sdk", + "commit", "-q", "--allow-empty", "-m", "before cwd-scoped generate", + ]) + } + + """ + Snapshot state after `dagger generate` from inside the first module's directory. + """ + let cwdGeneratedState: Container! { + pipe(cwdSnapshotState, "/work/" + modulePath, generateArgs) + } + """ Workspace-relative path of the vendored SDK module source. """ @@ -700,6 +768,16 @@ type SdkRun { container.directory("/work").exists(path) } + """ + Return the git status of a workspace path after the command ran, relative to + the last committed snapshot. Empty when the path is untouched. + """ + pub workspaceChanges(path: String!): String! { + container + .withExec(["git", "-C", "/work", "status", "--porcelain", "--", path]) + .stdout + } + """ The first failed pipeline command, as recorded by the run wrapper. """ From 6140bda4bf153f808745d8cc7bd66a92bb70b0f1 Mon Sep 17 00:00:00 2001 From: kpenfound Date: Tue, 4 Aug 2026 13:58:00 -0400 Subject: [PATCH 6/6] feat: validate on 1.0.0-beta.9 and require load, not functions, from scaffolds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the pinned CLI default from the unreleased 1.0.0-beta.8 to the released 1.0.0-beta.9, and relax scaffolded-module-serves-functions into scaffolded-module-loads: `dagger api functions` must succeed against a fresh scaffold, but starter templates may expose no functions — go-sdk deliberately scaffolds an empty root type, and "No functions found." on a loadable module satisfies the contract. A broken scaffold (e.g. missing generated bindings) still fails the check at module load. Validated on the released beta.9 CLI and engine: 22/22 in-repo, and 19/19 against the latest main of go-sdk, dang-sdk, typescript-sdk, and python-sdk. Co-Authored-By: Claude Fable 5 Signed-off-by: kpenfound --- README.md | 7 ++++--- mod-test/README.md | 2 +- mod-test/mod-test.dang | 2 +- sdk-sdk.dang | 16 ++++++++-------- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2911d7a..a39960b 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,9 @@ commands the way a user would: `dagger-module.toml`, installs it in `dagger.toml`, and records the SDK as the module's authoring SDK. - `dagger generate` succeeds on the fresh scaffold. -- After generation the scaffolded module serves functions: - `dagger api functions test-mod`. +- After generation the scaffolded module loads and serves its API: + `dagger api functions test-mod` succeeds (starter templates may expose no + functions yet). - `dagger sdk module-options ` introspects the SDK's `initModule` capability. - `dagger module engine required` and `dagger module deps list` work from the @@ -47,7 +48,7 @@ that depend on it fail with a `prerequisite command failed` message naming that stage instead of repeating raw errors. Configure the CLI release with the top-level `dagger-cli-version` setting; the -default is `1.0.0-beta.8`. Individual targets accept `with-timeout` for slow +default is `1.0.0-beta.9`. Individual targets accept `with-timeout` for slow SDKs (the default command timeout is `10m`). Custom checks can reuse the harness through `target`: diff --git a/mod-test/README.md b/mod-test/README.md index e10e669..8907f2c 100644 --- a/mod-test/README.md +++ b/mod-test/README.md @@ -7,7 +7,7 @@ release from `dl.dagger.io`, then runs readable `dagger api call -j` commands against the target module. Configure the CLI release with the top-level `dagger-cli-version` setting. The -default is `1.0.0-beta.8`. +default is `1.0.0-beta.9`. Callers provide: diff --git a/mod-test/mod-test.dang b/mod-test/mod-test.dang index 9b20425..e7c7d6c 100644 --- a/mod-test/mod-test.dang +++ b/mod-test/mod-test.dang @@ -5,7 +5,7 @@ type ModTest { """ Dagger CLI release version used by black-box tests. """ - pub daggerCliVersion: String! = "1.0.0-beta.8" + pub daggerCliVersion: String! = "1.0.0-beta.9" """ Return a black-box test target for a workspace-rooted module directory view. diff --git a/sdk-sdk.dang b/sdk-sdk.dang index df7468b..2bab051 100644 --- a/sdk-sdk.dang +++ b/sdk-sdk.dang @@ -33,7 +33,7 @@ type SdkSdk { """ Dagger CLI release version used by black-box tests. """ - pub daggerCliVersion: String! = "1.0.0-beta.8" + pub daggerCliVersion: String! = "1.0.0-beta.9" """ Engine runtime recorded for modules created with this SDK. @@ -154,15 +154,15 @@ type SdkSdk { } """ - A scaffolded module should serve at least one function after `dagger generate`. + A scaffolded module should load and serve its API after `dagger generate`. + + `dagger api functions` must succeed against the scaffolded module. Starter + templates may expose no functions yet (the go-sdk template scaffolds an + empty root type), so only introspection success is required. """ - pub scaffoldedModuleServesFunctions(ws: Workspace!): Void @check { + pub scaffoldedModuleLoads(ws: Workspace!): Void @check { let testTarget = sdkTarget(ws) - let run = testTarget.run(["api", "functions", testTarget.moduleName]) - run.assertSuccess - if (run.stdout.trimSuffix("\n") == "") { - raise "a scaffolded module should expose at least one function" - } + testTarget.run(["api", "functions", testTarget.moduleName]).assertSuccess } """