2026-05-16 00:20:09 +02:00
package main
import (
"context"
"fmt"
"dagger/ci/internal/dagger"
)
type Ci struct {}
// Base container with all dependencies for Flutter and Linux builds
func ( m * Ci ) Base ( source * dagger . Directory ) * dagger . Container {
2026-05-17 07:15:12 +02:00
// Surgical inclusion: only take what is strictly needed for the build/test.
// This improves caching by ignoring transient or irrelevant files.
source = source . Filter ( dagger . DirectoryFilterOpts {
Include : [] string {
"lib/" ,
"test/" ,
"assets/" ,
"scripts/" ,
"pubspec.yaml" ,
"analysis_options.yaml" ,
"linux/" ,
"android/" ,
"integration_test/" ,
"drift_schemas/" ,
2026-05-17 08:47:15 +02:00
"stalwart-dev/" ,
2026-05-17 07:15:12 +02:00
},
})
2026-05-16 00:20:09 +02:00
return dag . Container ().
2026-05-17 00:02:41 +02:00
From ( "ghcr.io/cirruslabs/flutter:3.41.6" ).
2026-05-16 00:20:09 +02:00
WithExec ([] string { "apt-get" , "update" }).
WithExec ([] string { "apt-get" , "install" , "-y" ,
"clang" , "cmake" , "ninja-build" , "pkg-config" ,
"libgtk-3-dev" , "liblzma-dev" , "libsecret-1-dev" ,
2026-05-17 08:47:15 +02:00
"libgcrypt20-dev" , "libjsoncpp-dev" , "sqlite3" , "curl" , "python3" , "iproute2" }).
WithExec ([] string { "curl" , "-sL" , "https://github.com/stalwartlabs/mail-server/releases/download/v0.14.1/stalwart-x86_64-unknown-linux-gnu.tar.gz" , "-o" , "/tmp/stalwart.tar.gz" }).
WithExec ([] string { "tar" , "-xzf" , "/tmp/stalwart.tar.gz" , "-C" , "/usr/local/bin" , "stalwart" }).
WithExec ([] string { "chmod" , "+x" , "/usr/local/bin/stalwart" }).
WithExec ([] string { "rm" , "/tmp/stalwart.tar.gz" }).
2026-05-17 00:02:41 +02:00
WithMountedCache ( "/root/.pub-cache" , dag . CacheVolume ( "flutter-pub-cache" )).
WithMountedCache ( "/root/.gradle" , dag . CacheVolume ( "gradle-cache" )).
WithEnvVariable ( "PUB_CACHE" , "/root/.pub-cache" ).
2026-05-17 06:28:32 +02:00
WithDirectory ( "/src" , source ).
2026-05-16 00:20:09 +02:00
WithWorkdir ( "/src" )
}
2026-05-17 10:14:40 +02:00
// Hugo container for website builds
func ( m * Ci ) Hugo () * dagger . Container {
return dag . Container ().
From ( "alpine:3.21" ).
WithExec ([] string { "apk" , "add" , "--no-cache" , "curl" , "tar" , "libc6-compat" , "libstdc++" , "gcompat" }).
WithExec ([] string { "curl" , "-sL" , "https://github.com/gohugoio/hugo/releases/download/v0.152.2/hugo_extended_0.152.2_linux-amd64.tar.gz" , "-o" , "/tmp/hugo.tar.gz" }).
WithExec ([] string { "tar" , "-xzf" , "/tmp/hugo.tar.gz" , "-C" , "/usr/local/bin" , "hugo" }).
WithExec ([] string { "rm" , "/tmp/hugo.tar.gz" })
}
2026-05-16 00:20:09 +02:00
// Setup environment: pub get and build_runner
func ( m * Ci ) Setup ( source * dagger . Directory ) * dagger . Container {
return m . Base ( source ).
WithExec ([] string { "flutter" , "pub" , "get" }).
2026-05-17 06:28:32 +02:00
// Use --delete-conflicting-outputs to ensure generated files match the current source
2026-05-16 00:20:09 +02:00
WithExec ([] string { "flutter" , "pub" , "run" , "build_runner" , "build" , "--delete-conflicting-outputs" })
}
// Run hygiene check
func ( m * Ci ) CheckHygiene ( ctx context . Context , source * dagger . Directory ) ( string , error ) {
return m . Base ( source ).
WithExec ([] string { "/bin/bash" , "-c" , "FORBIDDEN=\".ssh .bashrc .config .local .cache .gitconfig .android Android .gradle .pub-cache .dartServer .flutter .dart-cli-completion .atuin .bash_logout .profile .zcompdump .zshrc snap .emulator_console_auth_token .lesshst .metadata .tmux.conf\"; for f in $FORBIDDEN; do if [ -e \"$f\" ]; then echo \"ERROR: Forbidden file/dir found in source: $f\"; exit 1; fi; done; echo \"Hygiene check passed.\"" }).
Stdout ( ctx )
}
// Enforce architecture — ui/ must not import data/
func ( m * Ci ) CheckLayers ( ctx context . Context , source * dagger . Directory ) ( string , error ) {
return m . Base ( source ).
WithExec ([] string { "/bin/bash" , "-c" , "VIOLATIONS=$(grep -rn \"package:sharedinbox/data/\" lib/ui/ 2>/dev/null || true); if [ -n \"$VIOLATIONS\" ]; then echo \"ERROR: UI layer imports data layer (only core/ interfaces are allowed from ui/):\"; echo \"$VIOLATIONS\"; exit 1; fi; echo \"Layer check passed.\"" }).
Stdout ( ctx )
}
2026-05-17 08:51:17 +02:00
// Run dart format check
func ( m * Ci ) Format ( ctx context . Context , source * dagger . Directory ) ( string , error ) {
2026-05-17 10:10:23 +02:00
// pub get is required so .dart_tool/package_config.json exists; without it dart format
// ignores the package's language version (3.3) and applies tall-style formatting.
2026-05-17 08:51:17 +02:00
return m . Base ( source ).
2026-05-17 10:10:23 +02:00
WithExec ([] string { "flutter" , "pub" , "get" }).
2026-05-17 08:51:17 +02:00
WithExec ([] string { "dart" , "format" , "--output=none" , "--set-exit-if-changed" , "lib" , "test" }).
Stdout ( ctx )
}
// Verify that mocks are up to date
func ( m * Ci ) CheckMocks ( ctx context . Context , source * dagger . Directory ) ( string , error ) {
return m . Setup ( source ).
2026-05-17 09:07:55 +02:00
WithExec ([] string { "git" , "init" }).
WithExec ([] string { "git" , "config" , "user.email" , "ci@sharedinbox.de" }).
WithExec ([] string { "git" , "config" , "user.name" , "CI" }).
WithExec ([] string { "git" , "add" , "." }).
WithExec ([] string { "git" , "commit" , "-m" , "baseline" }).
WithExec ([] string { "flutter" , "pub" , "run" , "build_runner" , "build" , "--delete-conflicting-outputs" }).
2026-05-17 08:51:17 +02:00
WithExec ([] string { "/bin/bash" , "-c" , "CHANGED=$(find . -name '*.mocks.dart' | xargs -r git diff --exit-code); if [ $? -ne 0 ]; then echo \"ERROR: Mocks are out of date\"; exit 1; fi; echo \"Mocks are up to date.\"" }).
Stdout ( ctx )
}
2026-05-17 09:15:53 +02:00
// Run coverage check
func ( m * Ci ) Coverage ( ctx context . Context , source * dagger . Directory ) ( string , error ) {
return m . Setup ( source ).
WithExec ([] string { "flutter" , "test" , "test/unit" , "--coverage" }).
WithExec ([] string { "dart" , "scripts/check_coverage.dart" }).
Stdout ( ctx )
}
2026-05-16 00:20:09 +02:00
// Full check suite (equivalent to task check)
func ( m * Ci ) Check ( ctx context . Context , source * dagger . Directory ) ( string , error ) {
setup := m . Setup ( source )
// Hygiene & Layers
if _ , err := m . CheckHygiene ( ctx , source ); err != nil {
return "Hygiene check failed" , err
}
if _ , err := m . CheckLayers ( ctx , source ); err != nil {
return "Layer check failed" , err
}
2026-05-17 10:05:52 +02:00
// Format (Running after Setup/pub get ensures package resolution context)
if _ , err := setup . WithExec ([] string { "dart" , "format" , "--output=none" , "--set-exit-if-changed" , "lib" , "test" }). Stdout ( ctx ); err != nil {
2026-05-17 08:51:17 +02:00
return "Format check failed" , err
}
2026-05-16 00:20:09 +02:00
// Run analyze
analyze , err := setup . WithExec ([] string { "flutter" , "analyze" }). Stdout ( ctx )
if err != nil {
return analyze , err
}
2026-05-17 09:15:53 +02:00
// Run coverage gate (includes unit tests)
coverage , err := m . Coverage ( ctx , source )
2026-05-16 00:20:09 +02:00
if err != nil {
2026-05-17 09:15:53 +02:00
return coverage , err
2026-05-16 00:20:09 +02:00
}
2026-05-17 08:47:15 +02:00
// Run backend tests (requires Stalwart)
testBackend , err := setup . WithExec ([] string { "stalwart-dev/test.sh" }). Stdout ( ctx )
if err != nil {
return testBackend , err
}
2026-05-17 09:15:53 +02:00
return fmt . Sprintf ( "All checks passed!\n\nAnalysis:\n%s\n\n%s\n\nBackend Tests:\n%s\n" , analyze , coverage , testBackend ), nil
2026-05-16 00:20:09 +02:00
}
2026-05-17 10:14:40 +02:00
// Build and return the Hugo-based website bundle
func ( m * Ci ) BuildWebsite ( source * dagger . Directory ) * dagger . Directory {
// Surgical inclusion for website
websiteSource := source . Filter ( dagger . DirectoryFilterOpts {
Include : [] string { "website/" },
})
return m . Hugo ().
WithDirectory ( "/src" , websiteSource ).
WithWorkdir ( "/src/website" ).
WithExec ([] string { "hugo" , "--minify" }).
Directory ( "public" )
}
2026-05-16 00:20:09 +02:00
// Build and return the Linux bundle
func ( m * Ci ) BuildLinux ( source * dagger . Directory ) * dagger . Directory {
return m . Setup ( source ).
WithExec ([] string { "flutter" , "build" , "linux" , "--debug" }).
Directory ( "build/linux/x64/debug/bundle" )
}
// Build and return the Linux bundle (release)
func ( m * Ci ) BuildLinuxRelease ( source * dagger . Directory ) * dagger . Directory {
return m . Setup ( source ).
WithExec ([] string { "flutter" , "build" , "linux" , "--release" }).
Directory ( "build/linux/x64/release/bundle" )
}
// Build and return the Android App Bundle (AAB)
func ( m * Ci ) BuildAndroidRelease ( source * dagger . Directory ) * dagger . File {
return m . Setup ( source ).
WithExec ([] string { "flutter" , "build" , "appbundle" , "--release" }).
File ( "build/app/outputs/bundle/release/app-release.aab" )
}