Native Dagger container builds + Navidrome v0.61.1 (#330)
## Summary
- Move Dagger module from `.dagger/` to repo root (`src/blumeops/`), rename `blumeops-ci` → `blumeops`
- Replace opaque `docker_build()` with native Dagger pipelines that surface full build errors per step
- Migrate navidrome as the first container (`containers/navidrome/container.py`)
- Upgrade navidrome from v0.60.3 to v0.61.1 (major artwork overhaul, SQLite FTS5 search, server-managed transcoding)
- Add `dagger call container-version` for CI version extraction without Dockerfile parsing
- All mise tasks (`container-list`, `container-version-check`, `container-build-and-release`) updated for hybrid mode
- Legacy `docker_build()` fallback preserved for all other containers
## Motivation
When navidrome v0.61.0 added a new Go build tag (`sqlite_fts5`), `docker_build()` showed only "exit code: 1". We had to run `docker build --progress=plain` manually to find `undefined: buildtags.SQLITE_FTS5`. Native Dagger pipelines show the full error inline.
## Container build dispatch needed
After merge, dispatch container build for navidrome:
```
mise run container-build-and-release navidrome --ref 470b4bd
```
## Deploy steps
1. Wait for container build to complete
2. Back up navidrome-data PVC (non-reversible DB migrations)
3. `argocd app set navidrome --revision main && argocd app sync navidrome`
4. Verify at https://dj.ops.eblu.me
## Future
Remaining containers migrate incrementally in follow-up PRs using the same pattern.
Reviewed-on: #330
This commit is contained in:
parent
4fc0192731
commit
c86b5d7772
33 changed files with 422 additions and 929 deletions
|
|
@ -54,6 +54,8 @@ def list_containers() -> None:
|
|||
if not d.is_dir():
|
||||
continue
|
||||
types = []
|
||||
if (d / "container.py").exists():
|
||||
types.append("dagger")
|
||||
if (d / "Dockerfile").exists():
|
||||
types.append("dockerfile")
|
||||
if (d / "default.nix").exists():
|
||||
|
|
@ -70,11 +72,12 @@ def main(
|
|||
) -> None:
|
||||
"""Trigger container build workflows via Forgejo API dispatch."""
|
||||
container_dir = Path("containers") / container
|
||||
has_container_py = (container_dir / "container.py").exists()
|
||||
has_dockerfile = (container_dir / "Dockerfile").exists()
|
||||
has_nix = (container_dir / "default.nix").exists()
|
||||
|
||||
if not has_dockerfile and not has_nix:
|
||||
typer.echo(f"Error: No Dockerfile or default.nix found in '{container_dir}'")
|
||||
if not has_container_py and not has_dockerfile and not has_nix:
|
||||
typer.echo(f"Error: No container.py, Dockerfile, or default.nix found in '{container_dir}'")
|
||||
typer.echo()
|
||||
list_containers()
|
||||
raise typer.Exit(1)
|
||||
|
|
@ -90,10 +93,11 @@ def main(
|
|||
|
||||
# Show expected builds
|
||||
builds = []
|
||||
if has_dockerfile:
|
||||
builds.append(f" dockerfile -> {REGISTRY}/{image}:v<version>-{short_sha}")
|
||||
if has_container_py or has_dockerfile:
|
||||
label = "dagger" if has_container_py else "dockerfile"
|
||||
builds.append(f" {label:12s} -> {REGISTRY}/{image}:v<version>-{short_sha}")
|
||||
if has_nix:
|
||||
builds.append(f" nix -> {REGISTRY}/{image}:v<version>-{short_sha}-nix")
|
||||
builds.append(f" {'nix':12s} -> {REGISTRY}/{image}:v<version>-{short_sha}-nix")
|
||||
|
||||
if dry_run:
|
||||
typer.echo("[dry-run mode]")
|
||||
|
|
|
|||
|
|
@ -78,11 +78,14 @@ def discover_containers() -> list[dict]:
|
|||
for d in sorted(CONTAINER_DIR.iterdir()):
|
||||
if not d.is_dir():
|
||||
continue
|
||||
has_container_py = (d / "container.py").exists()
|
||||
has_dockerfile = (d / "Dockerfile").exists()
|
||||
has_nix = (d / "default.nix").exists()
|
||||
if not has_dockerfile and not has_nix:
|
||||
if not has_container_py and not has_dockerfile and not has_nix:
|
||||
continue
|
||||
types = []
|
||||
if has_container_py:
|
||||
types.append("dagger")
|
||||
if has_dockerfile:
|
||||
types.append("dockerfile")
|
||||
if has_nix:
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@
|
|||
# requires-python = ">=3.12"
|
||||
# dependencies = ["pyyaml>=6.0.2", "rich>=14.0.0", "typer>=0.24.0"]
|
||||
# ///
|
||||
#MISE description="Validate container version consistency across Dockerfiles, nix derivations, and service-versions.yaml"
|
||||
#MISE description="Validate container version consistency across container.py, Dockerfiles, nix derivations, and service-versions.yaml"
|
||||
#USAGE flag "--all-files" help="Check all containers, not just changed ones"
|
||||
"""Validate that container versions are consistent across all declaration sites.
|
||||
|
||||
For each container directory under containers/, checks:
|
||||
1. Any Dockerfile must declare ARG CONTAINER_APP_VERSION=<value>
|
||||
2. Any default.nix must produce a version (via dagger call nix-version)
|
||||
3. At least one build file (Dockerfile or default.nix) must exist
|
||||
4. A matching entry in service-versions.yaml must exist with non-null current-version
|
||||
5. All resolved versions from (1), (2), and (4) must agree
|
||||
1. Any container.py must declare VERSION=<value>
|
||||
2. Any Dockerfile must declare ARG CONTAINER_APP_VERSION=<value>
|
||||
3. Any default.nix must produce a version (via dagger call nix-version)
|
||||
4. At least one build file (container.py, Dockerfile, or default.nix) must exist
|
||||
5. A matching entry in service-versions.yaml must exist with non-null current-version
|
||||
6. All resolved versions from (1), (2), (3), and (5) must agree
|
||||
|
||||
By default, only checks containers whose files differ from main.
|
||||
Pass --all-files to check every container.
|
||||
|
|
@ -53,6 +54,7 @@ NIX_PACKAGE_MAP = {
|
|||
"authentik": "authentik",
|
||||
}
|
||||
|
||||
CONTAINER_PY_VERSION_PATTERN = re.compile(r'^VERSION\s*=\s*["\']([^"\']+)["\']', re.MULTILINE)
|
||||
VERSION_ARG_PATTERN = re.compile(r"^ARG\s+CONTAINER_APP_VERSION=(\S+)", re.MULTILINE)
|
||||
NIX_VERSION_PATTERN = re.compile(r'^\s*version\s*=\s*"([^"]+)"\s*;', re.MULTILINE)
|
||||
|
||||
|
|
@ -109,7 +111,7 @@ def get_nix_version(container_name: str, nix_file: Path) -> str | None:
|
|||
return None
|
||||
|
||||
result = subprocess.run(
|
||||
["dagger", "-m", ".dagger", "call", "nix-version", f"--package={pkg}"],
|
||||
["dagger", "call", "nix-version", f"--package={pkg}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=REPO_ROOT,
|
||||
|
|
@ -148,26 +150,37 @@ def main(
|
|||
if scope is not None and name not in scope:
|
||||
continue
|
||||
|
||||
container_py = container_dir / "container.py"
|
||||
dockerfile = container_dir / "Dockerfile"
|
||||
nix_file = container_dir / "default.nix"
|
||||
has_container_py = container_py.exists()
|
||||
has_dockerfile = dockerfile.exists()
|
||||
has_nix = nix_file.exists()
|
||||
|
||||
versions: dict[str, str] = {}
|
||||
entry = {
|
||||
"name": name,
|
||||
"has_container_py": has_container_py,
|
||||
"has_dockerfile": has_dockerfile,
|
||||
"has_nix": has_nix,
|
||||
"versions": versions,
|
||||
}
|
||||
results.append(entry)
|
||||
|
||||
# Rule 3: at least one build file
|
||||
if not has_dockerfile and not has_nix:
|
||||
errors.append((name, "No Dockerfile or default.nix found"))
|
||||
# Rule 4: at least one build file
|
||||
if not has_container_py and not has_dockerfile and not has_nix:
|
||||
errors.append((name, "No container.py, Dockerfile, or default.nix found"))
|
||||
continue
|
||||
|
||||
# Rule 1: Dockerfile must declare CONTAINER_APP_VERSION
|
||||
# Rule 1: container.py must declare VERSION
|
||||
if has_container_py:
|
||||
match = CONTAINER_PY_VERSION_PATTERN.search(container_py.read_text())
|
||||
if match:
|
||||
versions["container.py"] = match.group(1)
|
||||
else:
|
||||
errors.append((name, "container.py missing VERSION declaration"))
|
||||
|
||||
# Rule 2: Dockerfile must declare CONTAINER_APP_VERSION
|
||||
if has_dockerfile:
|
||||
match = VERSION_ARG_PATTERN.search(dockerfile.read_text())
|
||||
if match:
|
||||
|
|
@ -175,7 +188,7 @@ def main(
|
|||
else:
|
||||
errors.append((name, "Dockerfile missing ARG CONTAINER_APP_VERSION"))
|
||||
|
||||
# Rule 2: nix derivation must produce a version
|
||||
# Rule 3: nix derivation must produce a version
|
||||
if has_nix:
|
||||
nix_ver = get_nix_version(name, nix_file)
|
||||
if nix_ver is not None:
|
||||
|
|
@ -219,6 +232,8 @@ def main(
|
|||
for entry in results:
|
||||
name = entry["name"]
|
||||
build_parts = []
|
||||
if entry["has_container_py"]:
|
||||
build_parts.append("dagger")
|
||||
if entry["has_dockerfile"]:
|
||||
build_parts.append("dockerfile")
|
||||
if entry["has_nix"]:
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ After reviewing, update the service entry in the YAML file:
|
|||
Usage: mise run service-review [-- --limit 15] [-- --type argocd]
|
||||
"""
|
||||
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
|
@ -166,12 +165,25 @@ def main(
|
|||
]
|
||||
|
||||
svc_type = top_svc.get("type", "")
|
||||
container_dir = Path(__file__).parent.parent / "containers" / top_svc["name"]
|
||||
has_dockerfile_only = (
|
||||
(container_dir / "Dockerfile").exists()
|
||||
and not (container_dir / "container.py").exists()
|
||||
)
|
||||
|
||||
if svc_type == "argocd":
|
||||
checklist_parts += [
|
||||
"\n[bold]ArgoCD Deployment:[/bold]\n",
|
||||
"• Update image tag in argocd/manifests/<service>/kustomization.yaml\n",
|
||||
f"• Verify sync status: argocd app get {top_svc['name']}\n",
|
||||
]
|
||||
if has_dockerfile_only:
|
||||
checklist_parts += [
|
||||
"\n[bold yellow]Dagger Migration:[/bold yellow]\n",
|
||||
"• This container still uses a Dockerfile (no container.py)\n",
|
||||
"• Consider migrating to a native Dagger build for better error visibility\n",
|
||||
f"• See containers/{top_svc['name']}/Dockerfile\n",
|
||||
]
|
||||
elif svc_type == "ansible":
|
||||
checklist_parts += [
|
||||
"\n[bold]Ansible Deployment:[/bold]\n",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue