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/README.md b/README.md index 3ded792..a39960b 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,67 @@ # 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. -Start a new Dang SDK helper module: +## Checking an SDK + +Run the checks against an SDK repository: ```sh -dagger module init sdk-sdk my-sdk +dagger -m github.com/dagger/sdk-sdk -W check ``` -The module name is the Dagger module name. The generated Dang root type is -derived from it, for example `my-sdk` becomes `MySdk`. +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: -Run the checks from an SDK helper module workspace: +- `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 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 + 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. -```sh -cd ./my/sdk/repo -dagger -m github.com/dagger/sdk-sdk check +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`. + +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.9`. 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 = 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 @@ -37,3 +75,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/mod-test/README.md b/mod-test/README.md index fe2d06b..8907f2c 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.9`. Callers provide: diff --git a/mod-test/mod-test.dang b/mod-test/mod-test.dang index bbb3841..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.5" + pub daggerCliVersion: String! = "1.0.0-beta.9" """ 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/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..2bab051 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.9" + + """ + 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,35 +65,147 @@ 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 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 scaffoldedModuleLoads(ws: Workspace!): Void @check { + let testTarget = sdkTarget(ws) + testTarget.run(["api", "functions", testTarget.moduleName]).assertSuccess + } + + """ + `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 + } + + """ + `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 } } @@ -76,22 +213,19 @@ type SdkSdk { `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 +242,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 +252,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 +272,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 +379,416 @@ 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" + + """ + 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. + """ + 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" + } + + """ + 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. + """ + pub install: SdkRun! { + SdkRun(container: installedState, args: installArgs) + } + + """ + Run `dagger module init` with the SDK under test and capture the result. + """ + pub initTestModule: SdkRun! { + SdkRun(container: initializedState, args: initArgs) + } + + """ + Run `dagger generate` on the scaffolded module and capture the result. + """ + pub generateWorkspace: SdkRun! { + 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. + """ + 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! { + pipe(workspace, "/work", installArgs) + } + + """ + Workspace state after `dagger module init`. + """ + let initializedState: Container! { + pipe(installedState, "/work", initArgs) + } + + """ + Workspace state after `dagger generate`. + """ + let generatedState: Container! { + 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. + """ + 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 dagger command on a workspace state and capture the result. + """ + 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", + "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, + ) + } + + """ + 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" + } + + """ + 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 + } + null + } + + """ + 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) + } + 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) + } + + """ + 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. + """ + 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 + } }