.agents/skills/devops-automation/SKILL.md
Guide for managing and maintaining dagger CI/CD tooling in this repository. Use when adding, modifying, or running dagger tasks.
npx skillsauth add em-jones/staccato-toolkit devops-automationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill guides developers and agents in managing the dagger-based CI/CD tooling for this repository.
This repository uses dagger to provide portable, reproducible CI/CD pipeline execution. Dagger tasks run in containers and behave identically on a developer laptop and in GitHub Actions.
Key capabilities:
dagger call <task>Technology stack:
dagger-for-github actionThe dagger module lives under the src/ops/platform/ directory. All dagger-related code is self-contained within this directory.
src/ops/platform/ # dagger module root (Go SDK)
├── dagger.json # dagger module manifest
├── go.mod # Go module file
├── go.sum # Go module checksums
├── main.go # module entry point; exports all task functions
├── dagger.gen.go # auto-generated dagger bindings (do not edit)
├── platform_test.go # Go unit tests (alongside source, Go convention)
└── tests/
└── integration_test.go # //go:build integration (requires Docker)
Key files:
dagger.json — dagger module manifest; declares module name and SDK versionmain.go — entry point; all task methods are defined on the Platform struct — this is where you add new tasksgo.mod / go.sum — Go module dependencies; managed by go mod commandsdagger.gen.go — auto-generated dagger SDK bindings; do not edit manually*_test.go — Go unit tests; live alongside source files per Go conventionsPer repository-layout pattern, the dagger module is a self-contained component with its own go.mod and no shared source files with other components.
All dagger tasks can be run locally from within a devbox shell. No additional setup is required beyond entering the devbox environment.
Ensure you are in a devbox shell:
devbox shell
The dagger CLI is available in devbox — verify with:
dagger version
From the repository root, invoke any task using dagger call:
dagger call <task-name>
Examples:
# Run linting
dagger call lint
# Run tests
dagger call test
# Run build
dagger call build
List all available tasks in the dagger module:
dagger functions
This shows all exported methods from the Platform struct in main.go.
Follow these steps to add a new dagger task to the module.
Edit src/ops/platform/main.go and add a new exported method on the Platform struct:
// MyNewTask performs a specific CI/CD operation.
// Brief description of what this task does.
func (m *Platform) MyNewTask(ctx context.Context, source *dagger.Directory) (string, error) {
return dag.Container().
From("alpine:latest").
WithExec([]string{"echo", "Hello from my new task"}).
Stdout(ctx)
}
Naming conventions (per naming pattern):
MyNewTask, RunLint, BuildImageTest not RunGoTestMethod design (per functions pattern):
context.Context as first parameter for cancellation support(result, error) — use Go's standard error handlingCreate or update a test file alongside your source (Go convention):
// src/ops/platform/platform_test.go
package main
import (
"testing"
)
func TestValidateMyNewTaskInput(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"valid input", "valid-value", false},
{"invalid input", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateMyNewTaskInput(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("validateMyNewTaskInput() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Test pure logic only — unit tests should not invoke the dagger engine. Extract input validation and result handling into pure functions and test those.
If the task has complex container orchestration, add an integration test in a separate directory with a build tag:
// src/ops/platform/tests/platform_integration_test.go
//go:build integration
package tests
import (
"context"
"testing"
)
func TestMyNewTaskIntegration(t *testing.T) {
ctx := context.Background()
// Create Platform instance and test real dagger execution
// This requires Docker to be running
result, err := platform.MyNewTask(ctx, nil)
if err != nil {
t.Fatalf("MyNewTask failed: %v", err)
}
if !strings.Contains(result, "expected output") {
t.Errorf("unexpected output: %s", result)
}
}
Integration tests require Docker — they invoke the real dagger engine. Use the //go:build integration build tag so they only run when explicitly requested.
Edit .github/workflows/ci.yml and add a new job for your task:
my-new-task:
name: My New Task
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Dagger
uses: dagger/dagger-for-github@v6
with:
version: "latest"
- name: Run my new task
run: dagger call my-new-task
working-directory: ./src/ops/platform
Job ordering: Use needs: to declare dependencies between jobs if your task depends on another (e.g., build depends on lint and test passing).
Before committing, verify the task works locally:
devbox shell
dagger call my-new-task
Ensure:
cd src/ops/platform && go test ./...If the new task introduces a new workflow or pattern, update this SKILL.md file to document it. Add an example to the "Running Locally" section if the task has non-obvious usage.
To change the behavior of an existing task:
Edit src/ops/platform/main.go and modify the method implementation.
Update or add tests in src/ops/platform/*_test.go files to cover the new behavior. Ensure all existing tests still pass or are updated to reflect the intentional behavior change.
If the task has integration tests under src/ops/platform/tests/, update them to match the new behavior.
Run the task locally to confirm the change:
devbox shell
dagger call <task-name>
Run all tests:
cd src/ops/platform
go test ./...
If the task is used in .github/workflows/ci.yml, ensure the workflow job still passes. Check the GitHub Actions logs after pushing your branch.
To remove a task that is no longer needed:
Delete the exported method from src/ops/platform/main.go.
Delete the corresponding test code:
src/ops/platform/*_test.go filessrc/ops/platform/tests/<task-name>_integration_test.go (if present)Edit .github/workflows/ci.yml and remove the job that invokes the deleted task.
Update job dependencies: If other jobs have needs: [deleted-task-name], remove that dependency or replace it with the appropriate alternative.
Remove any references to the deleted task from this SKILL.md file.
Ensure no other code references the deleted task:
# Search for references
rg '<task-name>' --type go --type yaml
Run remaining tests to ensure nothing broke:
cd src/ops/platform
go test ./...
Unit tests live alongside source files as *_test.go files and test pure functions without invoking the dagger engine.
Run unit tests:
cd src/ops/platform
go test ./...
What to test:
What NOT to test in unit tests:
Integration tests live under src/ops/platform/tests/ with the //go:build integration build tag. They invoke the real dagger engine and require Docker to be available.
Run integration tests:
cd src/ops/platform
go test -tags integration ./...
What to test:
Build tag usage: The //go:build integration tag ensures these tests only run when explicitly requested, preventing failures when Docker is unavailable.
cd src/ops/platform
go test ./... # unit tests only
go test -tags integration ./... # unit + integration tests
Unit tests run by default. Integration tests require the -tags integration flag and a running Docker daemon.
Use dagger's secret API to pass sensitive values:
// DeployWithSecret demonstrates secret handling in dagger.
func (m *Platform) DeployWithSecret(ctx context.Context, token *dagger.Secret) (string, error) {
return dag.Container().
From("alpine:latest").
WithSecretVariable("TOKEN", token).
WithExec([]string{"sh", "-c", "echo Token length: ${#TOKEN}"}).
Stdout(ctx)
}
In GitHub Actions, pass secrets via the --secret flag:
deploy-with-secret:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Dagger
uses: dagger/dagger-for-github@v6
with:
version: "latest"
- name: Deploy
run: dagger call deploy-with-secret --token=env:DEPLOY_TOKEN
working-directory: ./src/ops/platform
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Use dagger's caching primitives to speed up repeated builds:
// BuildWithCache demonstrates dependency caching in dagger.
func (m *Platform) BuildWithCache(ctx context.Context, source *dagger.Directory) (string, error) {
goCache := dag.CacheVolume("go-build-cache")
goModCache := dag.CacheVolume("go-mod-cache")
return dag.Container().
From("golang:1.22-alpine").
WithDirectory("/app", source).
WithMountedCache("/go/pkg/mod", goModCache).
WithMountedCache("/root/.cache/go-build", goCache).
WithWorkdir("/app").
WithExec([]string{"go", "mod", "download"}).
WithExec([]string{"go", "build", "-o", "app", "."}).
Stdout(ctx)
}
Break complex tasks into multiple methods and compose them:
// BuildImage runs lint, test, and build in sequence.
func (m *Platform) BuildImage(ctx context.Context, source *dagger.Directory) (string, error) {
// Run lint
lintResult, err := m.Lint(ctx, source)
if err != nil {
return "", fmt.Errorf("lint failed: %w", err)
}
// Run tests
testResult, err := m.Test(ctx, source)
if err != nil {
return "", fmt.Errorf("tests failed: %w", err)
}
// Build
return m.Build(ctx, source)
}
You are not in a devbox shell. Run:
devbox shell
Dagger requires Docker to be running. Start Docker and try again.
On Linux, ensure your user is in the docker group:
sudo usermod -aG docker $USER
newgrp docker
Check for environment differences:
Add logging to the task method to diagnose:
fmt.Fprintf(os.Stderr, "Debug: inspecting environment\n")
This is expected if Docker is not running. Integration tests use the //go:build integration build tag, so they only run when explicitly requested with go test -tags integration.
If you need to skip tests conditionally within an integration test:
func TestIntegrationTask(t *testing.T) {
// Check if Docker is available
cmd := exec.Command("docker", "info")
if err := cmd.Run(); err != nil {
t.Skip("Docker not available, skipping integration test")
}
// Test implementation...
}
The platform uses an OCI-based GitOps bootstrap where CI renders manifests as OCI artifacts pushed to an in-cluster Harbor registry. Flux reconciles from Harbor, NOT from a Git repository.
This replaces the previous Gitea-based GitRepository source (flux-local-bootstrap — superseded).
CI pipeline
└─ renders manifests
└─ flux push artifact → Harbor OCI registry
↓
Flux OCIRepository source
↓
Flux Kustomization reconciler
↓
Kubernetes cluster state
Run AFTER k0s cluster provisioned (Phase 1) and KubeVela installed (Phase 2):
Phase 3b now enables the st-environment addon (supersedes the standalone flux-operator
addon). st-environment installs flux-operator, creates the FluxInstance, capability
GitRepository sources, and per-system ResourceSets in one operation.
# Inject credentials (REQUIRED before addon enable)
kubectl create secret generic harbor-admin-credentials -n harbor \
--from-literal=HARBOR_ADMIN_PASSWORD=<pw> \
--from-literal=values.yaml="harborAdminPassword: <pw>"
# Full bootstrap (Phases 3a–3d)
staccato bootstrap init \
--addons-dir src/staccato-toolkit/core/assets/addons \
--app-manifest src/staccato-toolkit/core/assets/bootstrap/gitops-provider-app.yaml
# Phase 3a: Enable Harbor addon
vela addon enable ./src/staccato-toolkit/core/assets/addons/harbor
# Phase 3b: Enable st-environment addon (installs flux-operator, FluxInstance, ResourceSets)
vela addon enable ./src/staccato-toolkit/core/assets/addons/st-environment \
--set name=local \
--set target=local \
--set gitopsConfig.url=oci://harbor-core.harbor.svc.cluster.local/staccato/manifests \
--set gitopsConfig.configRepoURL=<your-config-repo-url>
# Phase 3c: Seed initial OCI artifact
staccato bootstrap oci-seed \
--registry-url oci://harbor-core.harbor.svc.cluster.local/staccato/manifests \
--tag bootstrap
# Phase 3d: Apply gitops-provider Application
kubectl apply -f src/staccato-toolkit/core/assets/bootstrap/gitops-provider-app.yaml
To push manifests from a CI pipeline (GitHub Actions):
- name: Push manifests to Harbor
run: |
flux push artifact \
oci://harbor-core.harbor.svc.cluster.local/staccato/manifests:${{ github.sha }} \
--source=. \
--path=.
flux tag artifact \
oci://harbor-core.harbor.svc.cluster.local/staccato/manifests:${{ github.sha }} \
--tag latest
src/ops/workloads/)The workloads dagger module (src/ops/workloads/) provides two production tasks for the
GitOps manifest pipeline: render and publish-module.
Note: This module lives at
src/ops/workloads/, notsrc/ops/platform/. Rundagger callcommands from withinsrc/ops/workloads/or pass--module src/ops/workloadsfrom root.
render — Kustomize Build + OCI PushReads a kustomization.yaml containing helmCharts entries, runs
kustomize build --enable-helm per chart, and pushes each chart's rendered manifests as an OCI
artifact to a Harbor registry.
OCI artifact URL pattern per chart:
<registryURL>/<chart.name>:<env>-<short-sha>
Local testing (with RegistryService):
cd src/ops/workloads
# Bind a local registry:2 service on port 5000 (alias "harbor")
dagger call render \
--source ../.. \
--kustomization-dir ../../src/staccato-toolkit/core/assets/addons/st-workloads/src \
--env local \
--registry-url oci://harbor:5000/staccato/manifests
CI (pointing at in-cluster Harbor):
dagger call render \
--source ../.. \
--kustomization-dir ../../envs/ops \
--env dev \
--registry-url oci://harbor-core.harbor.svc.cluster.local/staccato/manifests \
--registry-credentials env:DOCKER_CONFIG_JSON
Parameters:
| Parameter | Required | Description |
|---|---|---|
| --source | yes | Git repository root (for SHA resolution and provenance) |
| --kustomization-dir | yes | Directory containing kustomization.yaml with helmCharts list |
| --env | yes | One of: local, dev, staging, prod |
| --registry-url | yes | OCI registry base URL (no trailing slash) |
| --registry-credentials | no | Docker config JSON secret (for authenticated registries) |
Atomicity guarantee: All charts are rendered before any push. A render failure leaves nothing pushed. Push failures abort the sequence.
publish-module — Push Dagger Module to DaggerverseBuilds and publishes the Platform Dagger module to Daggerverse, tagged with the current git SHA.
cd src/ops/workloads
dagger call publish-module \
--source ../.. \
--dagger-token env:DAGGER_TOKEN
Returns the published module reference (e.g. github.com/org/repo/src/ops/workloads@<sha>).
Parameters:
| Parameter | Required | Description |
|---|---|---|
| --source | yes | Git repository root (for SHA tagging) |
| --dagger-token | yes | Daggerverse authentication token |
registry-service — Local OCI Registry for TestingStarts a registry:2 container as a Dagger service on port 5000 with alias harbor.
Use to test render locally without an external Harbor deployment.
cd src/ops/workloads
dagger call registry-service
The alias harbor matches the in-cluster Harbor DNS pattern, so test URLs
(oci://harbor:5000/...) require no modification when targeting the real Harbor in CI.
The canonical way to trigger manifest rendering in production is by applying an
ops-environment KubeVela component with gitopsConfig.ref set to the current git
SHA. KubeVela's Application workflow automatically runs dagger call render via a
Kubernetes Job — no manual CI step needed.
kubectl apply → KubeVela reconciles ops-environment Application
│
├─ Step 1: applies staccato-environment component (creates ConfigMap)
└─ Step 2: runs render-manifests Job
└─ alpine + dagger CLI
└─ mounts ConfigMap /kustomization/kustomization.yaml
└─ DAGGER_CLOUD_TOKEN from dagger-cloud-token secret
└─ dagger call render --sha <gitopsConfig.ref> ...
└─ pushes OCI artifacts to Harbor
Set gitopsConfig.ref to the git SHA you want rendered and apply the Application:
# Set ref to current commit SHA — this becomes the OCI tag suffix
SHA=$(git rev-parse --short HEAD)
kubectl apply -f - <<EOF
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: ops-env-config
namespace: vela-system
spec:
components:
- type: ops-environment
name: ops
properties:
environment:
name: ops
gitopsConfig:
url: oci://harbor-core.harbor.svc.cluster.local/staccato/manifests
ref: "${SHA}"
pullSecret: harbor-oci-credentials
EOF
KubeVela runs the two workflow steps automatically. The render Job appears in
vela-system as render-ops-<appname>.
# Watch the workflow progress
vela status <app-name> -n vela-system --watch
# Check the render Job directly
kubectl get jobs -n vela-system -l st-environment/render=true
kubectl logs -n vela-system job/render-ops-<appname>
The dagger-cloud-token secret must exist in vela-system before the Application
is applied. Create it once during bootstrap:
kubectl create secret generic dagger-cloud-token \
--namespace vela-system \
--from-literal=token=<DAGGER_CLOUD_TOKEN>
See Dagger Cloud usage rules for full credential and caching guidance.
dagger call render (direct invocation)Both paths call the same Dagger function. Use the in-cluster path for production;
use direct dagger call render for local debugging:
# Local debugging — bypasses the Job, runs against local Dagger engine
cd src/ops/workloads
dagger call render \
--kustomization-dir ../../path/to/envs/ops \
--env local \
--registry-url oci://harbor:5000/staccato/manifests \
--sha $(git rev-parse --short HEAD)
tools
<!--VITE PLUS START--> # Using Vite+, the Unified Toolchain for the Web This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`. Vite+ is distinct from Vite, but it invokes Vite through `vp dev` and `vp build`. ## Vite+ Workflow `vp` is a global binary that handles the full development lifecycle. Run `vp help` to pr
development
Guide for building performant data tables. Uses tanstack-table for table logic (sorting, filtering, pagination) and tanstack-virtual for rendering large datasets efficiently.
development
Expert guidance for building observable, expressive, and fault-tolerant TypeScript applications using the effect-ts/effect ecosystem. Covers Effect<A, E, R> type, error management, dependency injection via Layers, observability (logging, metrics, tracing), concurrency with Fibers, retry/scheduling, Schema validation, Streams, and Sinks.
tools
Complete E2E (end-to-end) and integration testing skill for TypeScript/NestJS projects using Jest, real infrastructure via Docker, and GWT pattern. ALWAYS use this skill when user needs to: **SETUP** - Initialize or configure E2E testing infrastructure: - Set up E2E testing for a new project - Configure docker-compose for testing (Kafka, PostgreSQL, MongoDB, Redis) - Create jest-e2e.config.ts or E2E Jest configuration - Set up test helpers for database, Kafka, or Redis - Configure .env.e2e environment variables - Create test/e2e directory structure **WRITE** - Create or add E2E/integration tests: - Write, create, add, or generate e2e tests or integration tests - Test API endpoints, workflows, or complete features end-to-end - Test with real databases, message brokers, or external services - Test Kafka consumers/producers, event-driven workflows - Working on any file ending in .e2e-spec.ts or in test/e2e/ directory - Use GWT (Given-When-Then) pattern for tests **REVIEW** - Audit or evaluate E2E tests: - Review existing E2E tests for quality - Check test isolation and cleanup patterns - Audit GWT pattern compliance - Evaluate assertion quality and specificity - Check for anti-patterns (multiple WHEN actions, conditional assertions) **RUN** - Execute or analyze E2E test results: - Run E2E tests - Start/stop Docker infrastructure for testing - Analyze E2E test results - Verify Docker services are healthy - Interpret test output and failures **DEBUG** - Fix failing or flaky E2E tests: - Fix failing E2E tests - Debug flaky tests or test isolation issues - Troubleshoot connection errors (database, Kafka, Redis) - Fix timeout issues or async operation failures - Diagnose race conditions or state leakage - Debug Kafka message consumption issues **OPTIMIZE** - Improve E2E test performance: - Speed up slow E2E tests - Optimize Docker infrastructure startup - Replace fixed waits with smart polling - Reduce beforeEach cleanup time - Improve test parallelization where safe Keywords: e2e, end-to-end, integration test, e2e-spec.ts, test/e2e, Jest, supertest, NestJS, Kafka, Redpanda, PostgreSQL, MongoDB, Redis, docker-compose, GWT pattern, Given-When-Then, real infrastructure, test isolation, flaky test, MSW, nock, waitForMessages, fix e2e, debug e2e, run e2e, review e2e, optimize e2e, setup e2e