Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 86 additions & 10 deletions daggie/src/daggie/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from agent_base import constants, github_tools, llm_helpers, routing, workspace

_DAGGER_CONFIG_FILE = "DAGGER.md"
_DAGGER_ACTION_FALLBACK_VERSION = "v8.4.1"

# File patterns to read from cloned Dagger modules
_MODULE_SOURCE_GLOBS = [
Expand Down Expand Up @@ -39,6 +40,26 @@
return repo_url, branch, path


async def _fetch_repo_tags(repo_url: str) -> list[str]:
"""Fetch all Git tags from a remote repository via git ls-remote."""
try:
output = await (
dag.container()
.from_("alpine/git:latest")
.with_exec(["git", "ls-remote", "--tags", "--refs", repo_url])
.stdout()
)
# Parse "sha\trefs/tags/tag-name" lines → ["tag-name", ...]
tags = []
for line in output.strip().splitlines():
if "\trefs/tags/" in line:
tag = line.split("refs/tags/", 1)[1]
tags.append(tag)
return sorted(tags)
except Exception:
return []


async def _read_module_tree(tree: dagger.Directory) -> str:
"""Read key files from a cloned Dagger module and return formatted docs."""
sections = []
Expand All @@ -65,8 +86,9 @@
Specializes in creating, explaining, and debugging Dagger CI modules
and pipelines across all SDKs (Python, TypeScript, Go, Java).

Accepts Git URLs of Dagger modules at runtime, clones them via dag.git(),
reads their source code, and uses that knowledge to propose implementations.
Accepts Git URLs of Dagger module repositories at runtime, clones them via
dag.git(), and auto-discovers all modules within (by finding dagger.json files).
You can also point to a specific module with the url#branch:path format.

Pass your project as --source and optionally reference Dagger modules via
--module-urls for the agent to learn from.
Expand All @@ -79,7 +101,9 @@
] = field()
module_urls: Annotated[
list[str],
Doc("Git URLs of Dagger modules to clone and read for reference (e.g. 'https://github.com/org/repo.git#main:path/to/module')"),
Doc("Git URLs of Dagger module repositories to clone and read for reference. "
"Points to a repo root to auto-discover all modules (e.g. 'https://github.com/org/modules.git'), "
"or to a specific module with 'url#branch:path' (e.g. 'https://github.com/org/repo.git#main:my-module')"),
] = field(default=list)
self_improve: Annotated[
str,
Expand Down Expand Up @@ -125,8 +149,16 @@
def _load_prompt(self, filename: str) -> dagger.File:
return dag.current_module().source().file(f"src/daggie/prompts/{filename}")

async def _load_module_sources(self) -> str:

Check failure on line 152 in daggie/src/daggie/main.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 36 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=telchak_daggerverse&issues=AZ0njZY3ApNLOrO5OZHE&open=AZ0njZY3ApNLOrO5OZHE&pullRequest=58
"""Clone and read all referenced Dagger modules."""
"""Clone and read all referenced Dagger modules.

When a URL points to a repository root (no path), auto-discovers
all Dagger modules by finding dagger.json files in the tree.
When a URL includes a path, reads only that specific module.

Also fetches Git tags from each repository so the agent knows
available versions for toolchain/dependency pinning.
"""
if not self.module_urls:
return ""

Expand All @@ -135,15 +167,49 @@
try:
repo_url, branch, path = _parse_module_url(url)
tree = dag.git(repo_url).branch(branch).tree()
if path:
tree = tree.directory(path)

module_docs = await _read_module_tree(tree)
if module_docs:
sections.append(f"## Reference Module: {url}\n\n{module_docs}")
# Fetch available version tags from the repository
tags = await _fetch_repo_tags(repo_url)
tags_section = ""
if tags:
tags_section = "\n\n### Available version tags\n```\n" + "\n".join(tags) + "\n```"

if path:
# Explicit path — read that single module
module_tree = tree.directory(path)
module_docs = await _read_module_tree(module_tree)
if module_docs:
sections.append(f"## Reference Module: {url} ({path})\n\n{module_docs}{tags_section}")
else:
# No path — auto-discover all modules in the repo
dagger_jsons = await tree.glob("**/dagger.json")
# Add tags once at the repo level, before individual modules
if tags_section:
sections.append(f"## Repository: {repo_url}{tags_section}")
for dj_path in dagger_jsons:
module_dir = "/".join(dj_path.split("/")[:-1]) if "/" in dj_path else ""
module_tree = tree.directory(module_dir) if module_dir else tree
module_docs = await _read_module_tree(module_tree)
if module_docs:
label = module_dir or repo_url
sections.append(f"## Reference Module: {label}\n\n{module_docs}")
except Exception as exc:
sections.append(f"## Reference Module: {url}\n\n*Failed to clone: {exc}*")

# Fetch latest dagger-for-github action version
action_tags = await _fetch_repo_tags("https://github.com/dagger/dagger-for-github.git")
action_version = _DAGGER_ACTION_FALLBACK_VERSION
if action_tags:
# Filter to vN.N.N tags (ignore pre-releases, rc, etc.)
version_tags = [t for t in action_tags if t.startswith("v") and t.count(".") == 2 and all(p.isdigit() for p in t[1:].split("."))]
if version_tags:
action_version = version_tags[-1] # sorted, last = latest

Check warning on line 206 in daggie/src/daggie/main.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=telchak_daggerverse&issues=AZ0njZY3ApNLOrO5OZHF&open=AZ0njZY3ApNLOrO5OZHF&pullRequest=58
sections.append(
f"## GitHub Action: dagger/dagger-for-github\n\n"
f"Latest stable version tag: **{action_version}**\n"
f"Use `dagger/dagger-for-github@{action_version}` in all workflow files."
)

if not sections:
return ""

Expand Down Expand Up @@ -211,14 +277,24 @@
@function
async def assist(
self,
assignment: Annotated[str, Doc("What you want the agent to do (e.g. 'Create a Dagger pipeline for building and testing a Python project')")],
assignment: Annotated[str, Doc("What you want the agent to do (e.g. 'Create a Dagger pipeline for building and testing a Python project')")] = "",
assignment_file: Annotated[dagger.File | None, Doc("File containing the assignment instructions (used if --assignment is not set)")] = None,
source: Annotated[dagger.Directory | None, Doc("Override source directory (uses constructor source if omitted)")] = None,
) -> dagger.Directory:
"""Create Dagger pipelines and modules, implement features.

Reads reference modules, understands your project, and implements
Dagger CI pipelines. Returns the modified workspace directory.

Provide instructions via --assignment (takes precedence) or
--assignment-file for longer/structured prompts.
"""
if not assignment and assignment_file:
assignment = await assignment_file.contents()
if not assignment:
msg = "Provide either --assignment or --assignment-file"
raise ValueError(msg)

ws = source or self.source
env = (
dag.env()
Expand Down
22 changes: 21 additions & 1 deletion daggie/src/daggie/prompts/assist_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,27 @@ Accomplish the coding task described in the **assignment** input.
- Any helper modules or utilities
5. **Verify**: Read back modified files to confirm correctness

## Dagger Module Checklist
## Choosing the Right Approach

Determine whether the assignment calls for:

**A) Toolchain-only setup** (keywords: "toolchains", "dagger check", "zero code", "no pipeline code"):
- Generate `dagger.json` with `toolchains` array only — NO `sdk`, `include`, or `dependencies` fields
- Set `engineVersion` from the reference modules' `dagger.json` (use the latest version seen)
- For monorepos: add `customizations` with `defaultPath` for EVERY `@check` function's `source` param on EVERY toolchain — including deploy modules with `scan` checks. Do NOT skip any toolchain.
- Generate `.github/workflows/ci.yml` using the `dagger/dagger-for-github` action at the version discovered in the "Pre-loaded Module References" section
- The action only accepts `version`, `verb`, and `args` fields — no other fields exist
- Use the `engineVersion` from `dagger.json` (without `v` prefix) as the `version` field
- Do NOT create `.dagger/`, `src/`, or any pipeline code
- Call external agents via `-m` in CI workflow `args` field, not as local functions

**B) SDK pipeline module** (keywords: "pipeline", "orchestrate", "custom logic"):
- Generate `.dagger/` module with `dagger.json` containing `sdk` and `dependencies`
- Write pipeline code with `@object_type`, `@function` decorators

If the assignment mentions toolchains, always use approach A.

## Dagger Module Checklist (approach B only)

When creating a new module, ensure:
- [ ] `dagger.json` has correct `name`, `sdk`, `engineVersion`
Expand Down
112 changes: 110 additions & 2 deletions daggie/src/daggie/prompts/system_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ dagger toolchain install github.com/example/toolchain --name mytool
"name": "my-app",
"toolchains": [
{
"name": "acme-backend",
"source": "github.com/org/modules/acme-backend@v1.0.0",
"name": "my-backend",
"source": "github.com/org/modules/my-backend@v1.0.0",
"customizations": [
{
"function": ["test"],
Expand All @@ -114,6 +114,8 @@ dagger toolchain install github.com/example/toolchain --name mytool

**IMPORTANT — `function` field in customizations:** The `function` field scopes a customization to a specific function. Without it, the override targets the module's **constructor** arguments only. If the argument you're overriding lives on a function (e.g., `source` on `test()` or `lint()`), you **must** include `"function": ["<function_name>"]` — otherwise the override is silently ignored. Each function needs its own customization entry.

**IMPORTANT — Monorepo `source` customizations:** In a monorepo, EVERY `@check` function that has a `source` parameter needs a `customizations` entry with `defaultPath` pointing to the correct subdirectory. This applies to ALL toolchains, not just backend/frontend — deploy toolchains with `@check` functions (e.g., `scan`) also need their `source` customized. Inspect each module's functions and ensure none are missed.

**Making modules toolchain-ready:** Add `@check` decorator to validation functions (test, lint, audit) and `DefaultPath(".")` to their source parameter. This makes them discoverable via `dagger check` and auto-injects the project source:
```python
@function
Expand Down Expand Up @@ -199,8 +201,114 @@ dagger check pytest:* # Run toolchain-namespaced checks

**When to use `@check`:** Any function that validates code quality, security, or standards — tests, linting, audits, vulnerability scans, type checking, formatting checks. Do NOT mark build/deploy/assist functions as checks.

### Toolchain-Only Projects (No SDK)
When the user asks for a toolchain-based setup, the `dagger.json` should contain **only** `name`, `engineVersion`, and `toolchains`. Do NOT include `sdk`, `include`, or `dependencies` — those are for SDK-based pipeline modules. A toolchain-only project has zero pipeline code:

```json
{
"name": "my-project",
"engineVersion": "v0.20.3",
"toolchains": [...]
}
```

Do NOT create a `.dagger/` directory, `src/` module, or any pipeline Python/Go/TS code for toolchain-only setups. The entire CI configuration lives in `dagger.json` and the CI workflow file.

### GitHub Actions Integration
Always use the official `dagger/dagger-for-github` action for CI — never install Dagger manually via `curl`.

**CRITICAL — Action version and fields:**
- The action version MUST come from the "Pre-loaded Module References" section below, which contains the latest discovered version tag for `dagger/dagger-for-github`. Use that exact tag (e.g., `@v8.4.1`). If no version was discovered, the fallback version will be provided automatically.
- The action accepts **exactly three** `with:` fields: `version`, `verb`, and `args`. No other fields exist — do NOT invent fields like `module:`, `tool:`, `command:`, etc.
- The `version` field is the **Dagger engine version** and MUST match the `engineVersion` value from the project's `dagger.json` (strip the `v` prefix). For example, if `dagger.json` has `"engineVersion": "v0.20.3"`, use `version: "0.20.3"`. Never hardcode an arbitrary version — always read it from `dagger.json`.

**Running checks:**
```yaml
- uses: dagger/dagger-for-github@v8.4.1
with:
version: "0.20.3" # Must match dagger.json engineVersion
verb: check
```

**Calling toolchain functions:**
```yaml
- uses: dagger/dagger-for-github@v8.4.1
with:
version: "0.20.3" # Must match dagger.json engineVersion
verb: call
args: my-toolchain my-function --source=./app --flag=value ...
```

**Calling external modules (agents, tools):** Use `-m` in the `args` field:
```yaml
- uses: dagger/dagger-for-github@v8.4.1
with:
version: "0.20.3" # Must match dagger.json engineVersion
verb: call
args: >-
-m github.com/org/agent-module@v1.0.0
--source=./app
suggest-github-fix
--github-token=env:GITHUB_TOKEN
--pr-number=${{ github.event.pull_request.number }}
--repo=${{ github.repository }}
--commit-sha=${{ github.event.pull_request.head.sha }}
--error-output="${{ steps.checks.outputs.stderr }}"
```

**Workflow structure for toolchain projects:**
```yaml
permissions:
contents: read
pull-requests: write # For agent PR comments
id-token: write # For GCP OIDC auth

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Run all checks
id: checks
uses: dagger/dagger-for-github@v8.4.1
with:
version: "0.20.3" # Must match dagger.json engineVersion
verb: check
- name: Suggest fix on failure
if: failure() && github.event_name == 'pull_request'
uses: dagger/dagger-for-github@v8.4.1
with:
version: "0.20.3" # Must match dagger.json engineVersion
verb: call
args: -m github.com/org/coding-agent@v1.0.0 --source=./app suggest-github-fix ...
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

deploy:
runs-on: ubuntu-latest
needs: check
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v6
- uses: dagger/dagger-for-github@v8.4.1
with:
version: "0.20.3" # Must match dagger.json engineVersion
verb: call
args: my-deploy-toolchain cloud-run --source=./backend ...
```

**OIDC tokens for GCP auth:** GitHub Actions auto-exposes `ACTIONS_ID_TOKEN_REQUEST_TOKEN` and `ACTIONS_ID_TOKEN_REQUEST_URL` when the workflow has `id-token: write` permission. Pass them via `env:`:
```yaml
args: >-
my-deploy-toolchain cloud-run
--oidc-request-token=env:ACTIONS_ID_TOKEN_REQUEST_TOKEN
--oidc-request-url=env:ACTIONS_ID_TOKEN_REQUEST_URL
```

### CLI Commands
- `dagger call <function> [args]` — call a module function
- `dagger call -m <module> <function> [args]` — call an external module's function
- `dagger functions` — list available functions
- `dagger develop` — generate SDK bindings
- `dagger init --sdk=python` — initialize a new module
Expand Down
Loading