🏡 index : quine-global/quintodrome.git

package main

import (
	"context"

	"dagger/quintodrome/internal/dagger"
)

type Quintodrome struct{}

// Ci runs the full Linux CI: lint, build, and test for both Go and JS.
func (m *Quintodrome) Ci(ctx context.Context,
	// +defaultPath="."
	src *dagger.Directory,
) error {
	if err := m.LintGo(ctx, src); err != nil {
		return err
	}
	if err := m.FmtGo(ctx, src); err != nil {
		return err
	}
	if err := m.BuildGo(ctx, src); err != nil {
		return err
	}
	if err := m.TestGo(ctx, src); err != nil {
		return err
	}
	if err := m.LintJS(ctx, src); err != nil {
		return err
	}
	if err := m.BuildJS(ctx, src); err != nil {
		return err
	}
	if err := m.TestJS(ctx, src); err != nil {
		return err
	}
	return nil
}

// LintGo runs golangci-lint with the repo's .golangci.yml.
func (m *Quintodrome) LintGo(ctx context.Context, src *dagger.Directory) error {
	_, err := goContainer().
		WithDirectory("/repo", src, dagger.ContainerWithDirectoryOpts{Owner: goUser}).
		WithWorkdir("/repo").
		// The version upstream pins (Makefile's GOLANGCI_LINT_VERSION), so it
		// moves with upstream merges; one built with an older Go refuses a
		// go.mod that needs a newer one. The release tarball directly, not the
		// install script (whose hardcoded SHA256 for v2.12.0 didn't match the
		// re-uploaded tarball), for this machine's architecture.
		WithExec([]string{"sh", "-c", `
set -e
v=$(grep '^GOLANGCI_LINT_VERSION' Makefile | cut -d ' ' -f 3)
v=${v#v}
mkdir -p "$HOME/bin"
curl -sSfL "https://github.com/golangci/golangci-lint/releases/download/v$v/golangci-lint-$v-linux-$(go env GOARCH).tar.gz" | tar -xz --strip-components=1 -C "$HOME/bin"
`}).
		WithExec([]string{"sh", "-c", `"$HOME/bin/golangci-lint" run --timeout 2m`}).
		Sync(ctx)
	return err
}

// FmtGo checks that goimports and go mod tidy leave the tree clean.
func (m *Quintodrome) FmtGo(ctx context.Context, src *dagger.Directory) error {
	_, err := goContainer().
		WithDirectory("/repo", src, dagger.ContainerWithDirectoryOpts{Owner: goUser}).
		WithWorkdir("/repo").
		WithExec([]string{"sh", "-c", `
go run golang.org/x/tools/cmd/goimports@latest -w $(find . -name '*.go' | grep -v '_gen.go$' | grep -v '.pb.go$')
go mod tidy
test -z "$(git status --porcelain)"
`}).
		Sync(ctx)
	return err
}

// BuildGo compiles the Navidrome server and all packages.
func (m *Quintodrome) BuildGo(ctx context.Context, src *dagger.Directory) error {
	_, err := goContainer().
		WithDirectory("/repo", src, dagger.ContainerWithDirectoryOpts{Owner: goUser}).
		WithWorkdir("/repo").
		WithExec([]string{"go", "build", "-tags", "netgo,sqlite_fts5", "./..."}).
		Sync(ctx)
	return err
}

// TestGo runs the Go test suite.
func (m *Quintodrome) TestGo(ctx context.Context, src *dagger.Directory) error {
	_, err := goContainer().
		WithDirectory("/repo", src, dagger.ContainerWithDirectoryOpts{Owner: goUser}).
		WithWorkdir("/repo").
		WithExec([]string{"go", "test", "-tags", "netgo,sqlite_fts5", "./..."}).
		Sync(ctx)
	return err
}

// LintJS runs prettier and eslint in the web UI.
func (m *Quintodrome) LintJS(ctx context.Context, src *dagger.Directory) error {
	_, err := jsContainer().
		WithDirectory("/repo", src).
		WithWorkdir("/repo/ui").
		WithExec([]string{"npm", "ci", "--ignore-scripts"}).
		WithExec([]string{"npm", "run", "check-formatting"}).
		WithExec([]string{"npm", "run", "lint"}).
		Sync(ctx)
	return err
}

// BuildJS builds the Navidrome web UI.
func (m *Quintodrome) BuildJS(ctx context.Context, src *dagger.Directory) error {
	_, err := jsContainer().
		WithDirectory("/repo", src).
		WithWorkdir("/repo/ui").
		WithExec([]string{"npm", "ci", "--ignore-scripts"}).
		WithExec([]string{"npm", "run", "build"}).
		Sync(ctx)
	return err
}

// TestJS runs the JS test suite.
func (m *Quintodrome) TestJS(ctx context.Context, src *dagger.Directory) error {
	_, err := jsContainer().
		WithDirectory("/repo", src).
		WithWorkdir("/repo/ui").
		WithExec([]string{"npm", "ci", "--ignore-scripts"}).
		WithExec([]string{"npm", "test"}).
		Sync(ctx)
	return err
}

// The containers below keep their package and build caches in cache
// volumes, which outlive a run in the (shared, long-running) dagger engine:
// without them every step downloaded every Go module, or ran npm ci, from
// scratch. Go's caches and npm's are safe to share between steps and between
// runs at the same time.

// goUser is who the Go steps run as: not root, like upstream's CI. Root
// reads any file whatever its mode, which fails the tests that make an
// upload unreadable with chmod 000 and expect it not to open
// (core/artwork). The repo and the caches are its own.
const goUser = "ci"

func goContainer() *dagger.Container {
	owned := dagger.ContainerWithMountedCacheOpts{Owner: goUser}
	return dag.Container().
		From("golang:1.27").
		WithExec([]string{"apt-get", "update"}).
		WithExec([]string{"apt-get", "install", "-y", "zip"}).
		WithExec([]string{"useradd", "--create-home", "--uid", "1000", goUser}).
		WithUser(goUser).
		WithEnvVariable("HOME", "/home/"+goUser).
		WithEnvVariable("GOMODCACHE", "/home/"+goUser+"/go/pkg/mod").
		WithMountedCache("/home/"+goUser+"/go/pkg/mod", dag.CacheVolume("quintodrome-ci-go-mod"), owned).
		WithMountedCache("/home/"+goUser+"/.cache/go-build", dag.CacheVolume("quintodrome-ci-go-build"), owned).
		WithMountedCache("/home/"+goUser+"/.cache/golangci-lint", dag.CacheVolume("quintodrome-ci-golangci-lint"), owned)
}

func jsContainer() *dagger.Container {
	return dag.Container().
		From("node:24").
		WithMountedCache("/root/.npm", dag.CacheVolume("quintodrome-npm"))
}

// BuildDesktopWindows builds the Windows desktop app (.exe/.msi) inside a
// Windows container.
//
// Experimental: this requires the Dagger engine to run on a Windows host with
// BuildKit's Windows container support enabled (the engine uses the containerd
// worker with `platforms = ["windows/amd64"]`). Unverified end-to-end — the
// Rust MSVC toolchain and Tauri's NSIS/WiX bundling may not behave in a
// Windows Server Core container the way they do on a full Windows runner.
func (m *Quintodrome) BuildDesktopWindows(ctx context.Context, src *dagger.Directory) *dagger.Directory {
	setup := `Set-ExecutionPolicy Bypass -Scope Process -Force
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
choco install -y nodejs-lts golang rustup.install visualstudio2022buildtools visualstudio2022-workload-vctools --no-progress`

	return dag.Container().
		From("mcr.microsoft.com/windows/servercore:ltsc2022").
		WithDirectory("C:/src", src).
		WithWorkdir("C:/src").
		WithExec([]string{"powershell", "-NoProfile", "-Command", setup}).
		WithExec([]string{"cmd", "/c", "npm --version && go version && rustc --version"}).
		Directory("C:/src/desktop/src-tauri/target/release/bundle")
}