Catalog Tutorial#
This tutorial walks you through using scafctl’s local catalog to build, version, inspect, export, and share solutions. You’ll start by building your first solution into the catalog and progressively work through versioning, cleanup, air-gapped transfers, remote registries, tagging, and advanced bundling with file dependencies.
Prerequisites#
- scafctl installed and available in your PATH
- Basic familiarity with YAML syntax
- Completion of the Resolver Tutorial
Table of Contents#
- Building Your First Solution
- Running from the Catalog
- Listing and Inspecting
- Managing Multiple Versions
- Deleting and Pruning
- Exporting and Importing
- Tagging Artifacts
- Remote Registries
- Bundling File Dependencies
- Verifying and Extracting Bundles
- Comparing Bundle Versions
Building Your First Solution#
Let’s build a simple solution and store it in the local catalog so you can run it by name from anywhere.
Step 1: Create the Solution File#
Create a file called greeting.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: greeting
version: 1.0.0
description: A simple greeting solution
spec:
resolvers:
name:
type: string
resolve:
with:
- provider: parameter
inputs:
key: name
- provider: static
inputs:
value: World
message:
type: string
dependsOn:
- name
resolve:
with:
- provider: static
inputs:
value:
expr: "'Hello, ' + _.name + '!'"This solution accepts a name parameter (defaulting to “World”) and produces a greeting message.
Step 2: Build It into the Catalog#
scafctl build solution -f greeting.yamlscafctl build solution -f greeting.yamlExpected output:
✅ Built greeting@1.0.0
💡 Digest: sha256:abc123...
💡 Catalog: ~/.local/share/scafctl/catalogThe solution is now stored in your local catalog. The version (1.0.0) was read from metadata.version in the YAML file.
Step 3: Override the Version#
You can also specify the version on the command line, which overrides metadata.version:
scafctl build solution -f greeting.yaml --version 1.0.1scafctl build solution -f greeting.yaml --version 1.0.1Expected output:
✅ Built greeting@1.0.1
💡 Digest: sha256:def456...
💡 Catalog: ~/.local/share/scafctl/catalogWhat You Learned#
scafctl build solution -f FILEpackages a solution YAML into the local OCI catalog- The name and version come from
metadata.nameandmetadata.versionby default - Use
--versionto override the version at build time - Use
--nameto override the name at build time
Running from the Catalog#
Once a solution is in the catalog, you can run it by name instead of providing a file path.
Step 1: Run by Name#
scafctl run resolver -f greeting -o yaml --hide-executionscafctl run resolver -f greeting -o yaml --hide-executionExpected output:
message: Hello, World!
name: WorldNo file path needed – scafctl looked up greeting in the catalog and found the highest version.
Step 2: Pass a Parameter#
scafctl run resolver -f greeting -o yaml --hide-execution -r name=Alicescafctl run resolver -f greeting -o yaml --hide-execution -r name=AliceExpected output:
message: Hello, Alice!
name: AliceStep 3: Run a Specific Version#
When you have multiple versions, you can pin to a specific one:
scafctl run resolver -f greeting@1.0.0 -o yaml --hide-execution -r name=Bobscafctl run resolver -f greeting@1.0.0 -o yaml --hide-execution -r name=BobExpected output:
message: Hello, Bob!
name: BobStep 4: Use an Expression to Filter Output#
Use -e to extract just the value you care about:
scafctl run resolver -f greeting -o yaml -e '_.message' -r name=Carolscafctl run resolver -f greeting -o yaml -e '_.message' -r name=CarolExpected output:
Hello, Carol!What You Learned#
scafctl run resolver -f NAMEruns a solution from the catalog by name- Without a version, it picks the highest semantic version available
- Use
NAME@VERSIONto pin a specific version - Parameters work the same way as with file-based solutions (
-r key=value) - Use
-eto filter output to specific values
Listing and Inspecting#
Step 1: List Everything in the Catalog#
scafctl catalog list -o yamlscafctl catalog list -o yamlExpected output:
- name: greeting
version: 1.0.0
kind: solution
digest: sha256:abc123...
createdAt: "2026-02-17 10:00:00"
catalog: local
- name: greeting
version: 1.0.1
kind: solution
digest: sha256:def456...
createdAt: "2026-02-17 10:01:00"
catalog: localStep 2: Filter by Name#
scafctl catalog list --name greeting -o yamlscafctl catalog list --name greeting -o yamlThis shows only artifacts with the name greeting.
Step 3: Inspect a Specific Artifact#
scafctl catalog inspect greeting -o yamlscafctl catalog inspect greeting -o yamlExpected output:
name: greeting
version: 1.0.1
kind: solution
digest: sha256:def456...
size: 573
createdAt: "2026-02-17 10:01:00"
catalog: local
annotations:
dev.scafctl.artifact.name: greeting
dev.scafctl.artifact.type: solution
org.opencontainers.image.created: "2026-02-17T10:01:00Z"
org.opencontainers.image.source: greeting.yaml
org.opencontainers.image.version: 1.0.1Without a version, inspect shows the highest version. Pin a version with greeting@1.0.0.
Step 4: Use a CEL Expression to Extract Fields#
scafctl catalog inspect greeting -o yaml -e '_.annotations'scafctl catalog inspect greeting -o yaml -e '_.annotations'Expected output:
dev.scafctl.artifact.name: greeting
dev.scafctl.artifact.type: solution
org.opencontainers.image.created: "2026-02-17T10:01:00Z"
org.opencontainers.image.source: greeting.yaml
org.opencontainers.image.version: 1.0.1What You Learned#
scafctl catalog listshows every artifact in the catalog--namefilters by solution namescafctl catalog inspect NAMEshows detailed metadata for a specific artifact- Without a version, inspect/list show all versions or the highest version respectively
-ewith CEL expressions can extract sub-fields from the output
Managing Multiple Versions#
Step 1: Create a v2 of the Solution#
Create a file called greeting-v2.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: greeting
version: 2.0.0
description: An improved greeting solution with timestamps
spec:
resolvers:
name:
type: string
resolve:
with:
- provider: parameter
inputs:
key: name
- provider: static
inputs:
value: World
timestamp:
type: string
resolve:
with:
- provider: static
inputs:
value:
expr: "timestamp(now)"
message:
type: string
dependsOn:
- name
- timestamp
resolve:
with:
- provider: static
inputs:
value:
expr: "'Hello, ' + _.name + '! The time is ' + _.timestamp"Step 2: Build v2#
scafctl build solution -f greeting-v2.yamlscafctl build solution -f greeting-v2.yamlExpected output:
✅ Built greeting@2.0.0
💡 Digest: sha256:789abc...
💡 Catalog: ~/.local/share/scafctl/catalogStep 3: Verify Both Versions Exist#
scafctl catalog list --name greeting -o yamlscafctl catalog list --name greeting -o yamlExpected output:
- name: greeting
version: 1.0.0
kind: solution
...
- name: greeting
version: 1.0.1
kind: solution
...
- name: greeting
version: 2.0.0
kind: solution
...Step 4: Run Without a Version#
scafctl run resolver -f greeting -o yaml --hide-execution -r name=Alicescafctl run resolver -f greeting -o yaml --hide-execution -r name=AliceExpected output:
message: Hello, Alice! The time is 2026-02-17T10:05:00Z
name: Alice
timestamp: "2026-02-17T10:05:00Z"Without a version, scafctl runs the highest semantic version – in this case 2.0.0.
Step 5: Pin to the Old Version#
scafctl run resolver -f greeting@1.0.0 -o yaml --hide-execution -r name=Alicescafctl run resolver -f greeting@1.0.0 -o yaml --hide-execution -r name=AliceExpected output:
message: Hello, Alice!
name: AliceThe v1 solution doesn’t have a timestamp – confirming you’re running the original version.
Step 6: Try to Overwrite an Existing Version#
scafctl build solution -f greeting-v2.yaml --version 2.0.0scafctl build solution -f greeting-v2.yaml --version 2.0.0Expected output:
❌ artifact greeting@2.0.0 already exists in catalog "local" (use --force to overwrite)Use --force to overwrite:
scafctl build solution -f greeting-v2.yaml --version 2.0.0 --forcescafctl build solution -f greeting-v2.yaml --version 2.0.0 --forceWhat You Learned#
- Multiple versions of the same solution coexist in the catalog
- Without a version,
runpicks the highest semantic version - Use
NAME@VERSIONto pin to a specific version - Use
--forceto overwrite an existing version
Deleting and Pruning#
Step 1: Delete a Specific Version#
scafctl catalog delete greeting@1.0.1 --kind solutionscafctl catalog delete greeting@1.0.1 --kind solutionExpected output:
✅ Deleted greeting@1.0.1You must specify both the name and version. The --kind solution flag tells scafctl which artifact kind to delete.
Step 2: Verify It’s Gone#
scafctl catalog list --name greeting -o yamlscafctl catalog list --name greeting -o yamlThe 1.0.1 entry should no longer appear.
Step 3: Prune Orphaned Data#
After deleting artifacts, blob data may remain on disk. Clean it up:
scafctl catalog prunescafctl catalog pruneExpected output:
✅ Pruned catalog
💡 Removed manifests: 1
💡 Removed blobs: 2
💡 Reclaimed: 1.5 KBStep 4: Delete Multiple Versions#
Clean up the remaining test artifacts:
scafctl catalog delete greeting@1.0.0 --kind solution
scafctl catalog delete greeting@2.0.0 --kind solution
scafctl catalog prunescafctl catalog delete greeting@1.0.0 --kind solution
scafctl catalog delete greeting@2.0.0 --kind solution
scafctl catalog pruneWhat You Learned#
scafctl catalog delete NAME@VERSION --kind solutionremoves a single version- You must specify the version – this prevents accidental bulk deletion
scafctl catalog pruneremoves orphaned blobs and reclaims disk space- Always prune after deleting to free up storage
Exporting and Importing#
The save and load commands let you transfer catalog artifacts between machines – useful for air-gapped environments where there’s no network access to a registry.
Step 1: Build a Solution to Export#
First, rebuild the greeting solution:
scafctl build solution -f greeting.yamlscafctl build solution -f greeting.yamlStep 2: Export to a Tar Archive#
scafctl catalog save greeting@1.0.0 -o greeting-v1.tarscafctl catalog save greeting@1.0.0 -o greeting-v1.tarExpected output:
✅ Saved greeting@1.0.0 to greeting-v1.tar (5.5 KB)The archive uses the standard OCI Image Layout format.
Step 3: Delete the Local Copy#
Simulate receiving the tar on a different machine by deleting the local version:
scafctl catalog delete greeting@1.0.0 --kind solution
scafctl catalog prunescafctl catalog delete greeting@1.0.0 --kind solution
scafctl catalog pruneStep 4: Verify It’s Gone#
scafctl catalog list --name greeting -o yamlscafctl catalog list --name greeting -o yamlExpected output (empty or no greeting entries).
Step 5: Import from the Tar Archive#
scafctl catalog load --input greeting-v1.tarscafctl catalog load --input greeting-v1.tarExpected output:
✅ Loaded artifact from greeting-v1.tarStep 6: Confirm It Was Loaded#
scafctl catalog list --name greeting -o yamlscafctl catalog list --name greeting -o yamlExpected output:
- name: greeting
version: 1.0.0
kind: solution
digest: sha256:abc123...
createdAt: "2026-02-17 10:00:00"
catalog: localStep 7: Try Loading Again (Conflict)#
scafctl catalog load --input greeting-v1.tarscafctl catalog load --input greeting-v1.tarExpected output:
❌ artifact already exists (use --force to overwrite)Use --force to overwrite:
scafctl catalog load --input greeting-v1.tar --forcescafctl catalog load --input greeting-v1.tar --forceAir-Gapped Transfer Workflow#
Here’s how the full workflow looks in practice:
# On the connected machine:
scafctl build solution -f deploy.yaml --version 1.0.0
scafctl catalog save deploy@1.0.0 -o deploy-v1.tar
cp deploy-v1.tar /Volumes/USB/
# Transfer USB to the air-gapped machine, then:
scafctl catalog load --input /Volumes/USB/deploy-v1.tar
scafctl run resolver -f deploy -o yaml --hide-execution -r env=prod# On the connected machine:
scafctl build solution -f deploy.yaml --version 1.0.0
scafctl catalog save deploy@1.0.0 -o deploy-v1.tar
Copy-Item deploy-v1.tar /Volumes/USB/
# Transfer USB to the air-gapped machine, then:
scafctl catalog load --input /Volumes/USB/deploy-v1.tar
scafctl run resolver -f deploy -o yaml --hide-execution -r env=prodWhat You Learned#
scafctl catalog save NAME@VERSION -o FILEexports an artifact as an OCI tar archivescafctl catalog load --input FILEimports an artifact from a tar archive- Use
--forceto overwrite if the artifact already exists - This workflow enables air-gapped transfers without any registry access
Tagging Artifacts#
Tags let you create freeform aliases for specific versions. Common uses include marking a version as “stable”, “latest”, or “production”.
Step 1: Tag a Version as Stable#
Make sure you have greeting@1.0.0 in the catalog, then tag it:
scafctl catalog tag greeting@1.0.0 stablescafctl catalog tag greeting@1.0.0 stableExpected output:
✅ Tagged greeting@1.0.0 as "stable"Step 2: View the Tag in the Catalog#
scafctl catalog list --name greeting -o yamlscafctl catalog list --name greeting -o yamlThe tag creates an alias that points to the same digest as the original version.
Step 3: Tag for Different Environments#
scafctl catalog tag greeting@1.0.0 productionscafctl catalog tag greeting@1.0.0 productionYou can create as many tags as needed. Tags are freeform strings – they cannot be valid semver versions (use scafctl build for that).
What You Learned#
scafctl catalog tag NAME@VERSION ALIAScreates a named alias pointing to a specific version- Tags are useful for marking releases as stable, production, etc.
- Tags can also be created in remote registries with
--catalog
Remote Registries#
scafctl supports pushing and pulling artifacts to/from OCI-compliant container registries like GitHub Container Registry (ghcr.io), Docker Hub, Azure Container Registry, and others.
Native Authentication (No Docker Required)#
scafctl provides built-in registry authentication – no Docker or Podman installation needed. This is the recommended approach for most users.
Cloud registries (GitHub, GCP, Azure):
Authenticate with your cloud provider’s auth handler, then bridge the credentials to the registry:
# GitHub Container Registry
scafctl auth login github
scafctl catalog login ghcr.io
# Google Artifact Registry / Container Registry
scafctl auth login gcp
scafctl catalog login us-docker.pkg.dev
# Azure Container Registry
scafctl auth login entra
scafctl catalog login myacr.azurecr.ioDirect credentials (any registry):
For registries that use tokens or passwords directly (Docker Hub, Quay.io, self-hosted):
# Using a token via stdin
echo "YOUR_TOKEN" | scafctl catalog login quay.io --username myorg+deployer --password-stdin
# Using a token from an environment variable (CI/automation)
scafctl catalog login quay.io --username admin --password-env REGISTRY_PASSWORDConfig-based automatic authentication:
For catalogs defined in your scafctl config, set authProvider to enable automatic authentication without a separate login step:
# ~/.config/scafctl/config.yaml
catalogs:
- name: company-registry
type: oci
url: oci://ghcr.io/myorg/scafctl
authProvider: githubWith this config, scafctl catalog pull and scafctl catalog push automatically use your GitHub auth session – no catalog login needed.
Docker/Podman interop:
If you also need Docker or Podman to access the same credentials, add --write-registry-auth:
scafctl catalog login ghcr.io --write-registry-authThis writes credentials to both scafctl’s native store and the container auth file.
Combined auth and catalog login:
You can authenticate and bridge to a registry in a single command using --registry on auth login:
scafctl auth login github --registry ghcr.ioRemoving credentials:
# Remove credentials for a specific registry
scafctl catalog logout ghcr.io
# Remove all stored registry credentials
scafctl catalog logout --allscafctl resolves credentials in this order:
| Priority | Source |
|---|---|
| 1 | Docker/Podman config files |
| 2 | scafctl native credential store (~/.config/scafctl/registries.json) |
| 3 | Dynamic auth handler bridge (if authProvider is configured) |
Step 1: Set Up Authentication (Docker/Podman Alternative)#
scafctl reads container credentials from the same locations as Docker and Podman. The easiest way to authenticate is with Docker or Podman’s login command.
Using GitHub CLI (recommended):
gh auth login -s write:packages -s read:packages -s delete:packages
gh auth token | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdinUsing a Personal Access Token:
- Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
- Generate a new token with
write:packagesandread:packagesscopes - Log in:
echo "YOUR_GITHUB_TOKEN" | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdinscafctl checks these credential locations in order:
| Priority | Location |
|---|---|
| 1 | $DOCKER_CONFIG/config.json |
| 2 | ~/.docker/config.json |
| 3 | $XDG_RUNTIME_DIR/containers/auth.json |
| 4 | ~/.config/containers/auth.json |
Step 2: Push a Solution to a Remote Registry#
Make sure greeting@1.0.0 is in your local catalog, then push it:
scafctl catalog push greeting@1.0.0 --catalog ghcr.io/myorg/scafctlscafctl catalog push greeting@1.0.0 --catalog ghcr.io/myorg/scafctlExpected output:
✅ Pushed greeting@1.0.0The artifact is stored at: ghcr.io/myorg/scafctl/solutions/greeting:1.0.0
The path structure is: <registry>/<repository>/solutions/<name>:<version>
Step 3: Push with a Different Name#
scafctl catalog push greeting@1.0.0 --as hello-world --catalog ghcr.io/myorg/scafctlscafctl catalog push greeting@1.0.0 --as hello-world --catalog ghcr.io/myorg/scafctlThis pushes the same artifact under a different name in the remote registry.
Step 4: Pull from a Remote Registry#
On a different machine (or after deleting the local copy):
scafctl catalog pull ghcr.io/myorg/scafctl/solutions/greeting@1.0.0scafctl catalog pull ghcr.io/myorg/scafctl/solutions/greeting@1.0.0Expected output:
✅ Pulled greeting@1.0.0The artifact is now in your local catalog. You can run it with:
scafctl run resolver -f greeting -o yaml --hide-execution -r name=Alicescafctl run resolver -f greeting -o yaml --hide-execution -r name=AliceStep 5: Pull with a Different Local Name#
scafctl catalog pull ghcr.io/myorg/scafctl/solutions/greeting@1.0.0 --as my-greetingscafctl catalog pull ghcr.io/myorg/scafctl/solutions/greeting@1.0.0 --as my-greetingThis stores the artifact locally under the name my-greeting.
Step 6: Delete from a Remote Registry#
scafctl catalog delete ghcr.io/myorg/scafctl/solutions/greeting@1.0.0scafctl catalog delete ghcr.io/myorg/scafctl/solutions/greeting@1.0.0Note: Not all registries support OCI DELETE. GitHub Container Registry (ghcr.io) requires deletion through the web interface at
https://github.com/orgs/YOUR_ORG/packages. Docker Hub, Azure Container Registry, Harbor, and Amazon ECR support API-based deletion.
Troubleshooting#
403 Forbidden errors:
# Enable debug logging to see which auth config is being used
scafctl catalog push greeting@1.0.0 --catalog ghcr.io/myorg -d# Enable debug logging to see which auth config is being used
scafctl catalog push greeting@1.0.0 --catalog ghcr.io/myorg -dCheck that:
- Your token has
write:packagesscope - You’re logged in:
docker login ghcr.io - The org/repo exists and you have access
Insecure registries (HTTP):
For local testing with registries that don’t use HTTPS:
scafctl catalog push greeting@1.0.0 --catalog localhost:5000 --insecure
scafctl catalog pull localhost:5000/solutions/greeting@1.0.0 --insecurescafctl catalog push greeting@1.0.0 --catalog localhost:5000 --insecure
scafctl catalog pull localhost:5000/solutions/greeting@1.0.0 --insecureSupported Registries#
scafctl works with any OCI-compliant registry:
| Registry | URL Format |
|---|---|
| GitHub Container Registry | ghcr.io/OWNER |
| Docker Hub | docker.io/NAMESPACE |
| Azure Container Registry | REGISTRY.azurecr.io |
| Amazon ECR | ACCOUNT.dkr.ecr.REGION.amazonaws.com |
| Google Artifact Registry | REGION-docker.pkg.dev/PROJECT |
| Harbor | harbor.example.com/PROJECT |
| Local Registry | localhost:5000 |
What You Learned#
scafctl catalog push NAME@VERSION --catalog REGISTRYpushes to a remote registryscafctl catalog pull REGISTRY/solutions/NAME@VERSIONpulls to your local catalog--aslets you rename artifacts during push or pull--forceoverwrites existing artifacts--insecureallows HTTP connections for local testing- Authentication uses standard Docker/Podman credential files
Bundling File Dependencies#
When a solution references local files (via the file provider), those files need to be packaged into the bundle so the solution works when run from the catalog. This tutorial walks you through building a solution with file dependencies.
Step 1: Create the Project Structure#
Create a directory with the following files:
mkdir -p deploy-app/templates deploy-app/configsCreate deploy-app/configs/dev.yaml:
name: my-app
namespace: dev
replicas: 1
image: my-app:latest
port: 8080Create deploy-app/configs/prod.yaml:
name: my-app
namespace: production
replicas: 3
image: my-app:1.2.0
port: 8080Create deploy-app/templates/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .name }}
namespace: {{ .namespace }}
spec:
replicas: {{ .replicas }}
selector:
matchLabels:
app: {{ .name }}
template:
metadata:
labels:
app: {{ .name }}
spec:
containers:
- name: {{ .name }}
image: {{ .image }}
ports:
- containerPort: {{ .port }}Step 2: Create the Solution File#
Create deploy-app/solution.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: deploy-app
version: 1.0.0
description: Renders a Kubernetes deployment for a given environment
bundle:
include:
- "configs/**/*.yaml"
spec:
resolvers:
environment:
type: string
description: "Target environment (dev or prod)"
resolve:
with:
- provider: parameter
inputs:
key: environment
- provider: static
inputs:
value: dev
config:
type: any
description: "Environment-specific configuration"
dependsOn:
- environment
resolve:
with:
- provider: file
inputs:
path:
expr: "'configs/' + _.environment + '.yaml'"
format: yaml
deployment-template:
type: string
description: "Kubernetes deployment template"
resolve:
with:
- provider: file
inputs:
path: "templates/deployment.yaml"
rendered-deployment:
type: string
description: "Rendered deployment manifest"
dependsOn:
- deployment-template
- config
resolve:
with:
- provider: go-template
inputs:
template:
rslvr: deployment-template
data:
rslvr: configNotice the bundle.include section – this is needed because config uses a dynamic path (computed via CEL expression at runtime). scafctl can’t statically discover which config files to bundle, so you tell it to include all YAML files under configs/.
The deployment-template resolver uses a static path (templates/deployment.yaml), so scafctl discovers it automatically – no bundle.include entry needed.
Step 3: Preview What Gets Bundled#
scafctl build solution -f deploy-app/solution.yaml --dry-runscafctl build solution -f deploy-app/solution.yaml --dry-runExpected output:
Bundle analysis for deploy-app/solution.yaml:
Static analysis discovered:
templates/deployment.yaml
Explicit includes (bundle.include):
configs/dev.yaml
configs/prod.yaml
⚠️ Dynamic paths detected (ensure these are covered by bundle.include):
resolver 'config' (file provider): expr in 'configs/' + _.environment + '.yaml'
Total: 3 bundled file(s)
💡 Dry run: would build deploy-app@1.0.0The dry-run shows:
- Static analysis discovered – files scafctl found by analyzing your resolvers
- Explicit includes – files matched by your
bundle.includepatterns - Dynamic paths – warnings about paths that can’t be statically resolved
Step 4: Build the Solution#
scafctl build solution -f deploy-app/solution.yamlscafctl build solution -f deploy-app/solution.yamlExpected output:
💡 Bundled 3 file(s) (1.0 KB, deduplicated: 1 layer(s))
✅ Built deploy-app@1.0.0
💡 Digest: sha256:abc123...
💡 Catalog: ~/.local/share/scafctl/catalogStep 5: Run from the Catalog with Dev Config#
scafctl run resolver -f deploy-app -o yaml -e '_.["rendered-deployment"]' -r environment=devscafctl run resolver -f deploy-app -o yaml -e '_.["rendered-deployment"]' -r environment=devExpected output:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: dev
spec:
replicas: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: my-app:latest
ports:
- containerPort: 8080Step 6: Switch to Prod#
scafctl run resolver -f deploy-app -o yaml -e '_.["rendered-deployment"]' -r environment=prodscafctl run resolver -f deploy-app -o yaml -e '_.["rendered-deployment"]' -r environment=prodExpected output:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: my-app:1.2.0
ports:
- containerPort: 8080The config values (namespace, replicas, image) changed based on the environment file – all loaded from the bundled files inside the catalog artifact.
Step 7: Add Exclude Patterns#
Suppose you add test files that you don’t want in the bundle. Update deploy-app/solution.yaml to add an exclude pattern:
bundle:
include:
- "configs/**/*.yaml"
exclude:
- "**/*_test.yaml"Now any file ending in _test.yaml will be excluded, even if it matches an include pattern.
What You Learned#
- The
fileprovider references local files that must be bundled - Static paths (literal strings) are auto-discovered during build
- Dynamic paths (CEL expressions, Go templates) require explicit
bundle.includepatterns --dry-runshows exactly what would be bundled, including warnings for dynamic pathsbundle.excludefilters out files that match include patterns (e.g., test files)- Bundled solutions are self-contained – all file dependencies travel with the artifact
Nested Bundle Support#
When a parent solution references sub-solutions via the solution provider, scafctl automatically discovers and bundles the sub-solution files recursively. This means nested solutions are fully self-contained – everything a sub-solution needs is included in the parent’s bundle.
Step 1: Create the Project Structure#
mkdir -p nested-demo/sub/templatesCreate nested-demo/parent-config.txt:
parent data contentCreate nested-demo/sub/child.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: nested-child
version: 1.0.0
description: Child sub-solution with its own local template
spec:
resolvers:
child-template:
type: string
resolve:
with:
- provider: file
inputs:
operation: read
path: "templates/greeting.tmpl"
child-greeting:
type: string
resolve:
with:
- provider: cel
inputs:
expression: "'hello from child'"Create nested-demo/sub/templates/greeting.tmpl:
Hello from the child template!Step 2: Create the Parent Solution#
Create nested-demo/parent.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: nested-demo
version: 1.0.0
description: Parent solution referencing a child sub-solution
spec:
resolvers:
parent-config:
resolve:
with:
- provider: file
inputs:
operation: read
path: "parent-config.txt"
child-result:
type: any
resolve:
with:
- provider: solution
inputs:
source: "./sub/child.yaml"Step 3: Preview the Bundle#
scafctl build solution -f nested-demo/parent.yaml --dry-runscafctl build solution -f nested-demo/parent.yaml --dry-runExpected output:
Bundle analysis for nested-demo/parent.yaml:
Static analysis discovered:
parent-config.txt
sub/child.yaml
sub/templates/greeting.tmpl
Total: 3 bundled file(s)
💡 Dry run: would build nested-demo@1.0.0Notice that scafctl recursively discovered the child sub-solution (sub/child.yaml) and its file dependency (sub/templates/greeting.tmpl). No bundle.include is needed – the solution provider reference is detected by static analysis.
Step 4: Build and Run#
scafctl build solution -f nested-demo/parent.yaml
scafctl run resolver -f nested-demo -o jsonscafctl build solution -f nested-demo/parent.yaml
scafctl run resolver -f nested-demo -o jsonHow It Works#
- Static analysis –
scafctl buildparses the parent solution and finds thesolutionprovider reference to./sub/child.yaml - Recursive discovery – It then parses
sub/child.yamland discovers its own file dependencies (templates/greeting.tmpl) - Path normalization – All paths are normalized relative to the parent bundle root (
sub/templates/greeting.tmplnottemplates/greeting.tmpl) - Circular reference detection – If solution A references B and B references A, the build fails with a clear error
What You Learned#
- Sub-solutions referenced via the
solutionprovider are automatically discovered duringbuild - All nested file dependencies are included in the parent bundle – no extra
bundle.includeneeded - Path normalization ensures sub-solution paths resolve correctly within the bundle
- Circular sub-solution references are detected and reported at build time
--dry-runshows the full recursive file tree
Verifying and Extracting Bundles#
After building a bundle, you can verify its integrity and examine its contents.
Step 1: Verify the Bundle#
scafctl bundle verify deploy-app@1.0.0scafctl bundle verify deploy-app@1.0.0Expected output:
💡 Verifying deploy-app@1.0.0...
Static paths:
Bundle includes (glob coverage): ✅
✓ configs/**/*.yaml
✅ Verification passed: 1 item(s) checkedThis checks that:
- All files referenced in the solution exist in the bundle
- Glob patterns in
bundle.includecover the expected files
Step 2: List Bundle Contents#
See what files are inside the bundle without extracting them:
scafctl bundle extract deploy-app@1.0.0 --list-onlyscafctl bundle extract deploy-app@1.0.0 --list-onlyExpected output:
templates/deployment.yaml (500 B)
configs/dev.yaml (100 B)
configs/prod.yaml (105 B)
💡 Total: 3 file(s), 705 BStep 3: Extract to a Directory#
Extract the bundled files to inspect them:
scafctl bundle extract deploy-app@1.0.0 --output-dir ./extractedscafctl bundle extract deploy-app@1.0.0 --output-dir ./extractedCheck the extracted files:
ls -R extracted/You’ll see the full directory structure preserved:
extracted/
├── configs/
│ ├── dev.yaml
│ └── prod.yaml
└── templates/
└── deployment.yamlStep 4: Extract Files for a Specific Resolver#
You can extract only the files needed by a specific resolver:
scafctl bundle extract deploy-app@1.0.0 --resolver config --output-dir ./config-onlyscafctl bundle extract deploy-app@1.0.0 --resolver config --output-dir ./config-onlyThis uses static analysis to determine which files the config resolver references.
Step 5: Clean Up#
rm -rf extracted/ config-only/What You Learned#
scafctl bundle verifychecks that a bundle contains all required filesscafctl bundle extract --list-onlyshows bundle contents without extractingscafctl bundle extract --output-dir DIRextracts files to a directory--resolver NAMEextracts only files needed by a specific resolver- Use
--flattento extract all files to a single directory (no subdirectories)
Comparing Bundle Versions#
When you release a new version of a bundled solution, bundle diff shows exactly what changed.
Step 1: Create a v2 with Changes#
Add a new config file and modify the template. First, create deploy-app/configs/staging.yaml:
name: my-app
namespace: staging
replicas: 2
image: my-app:1.2.0-rc1
port: 8080Then update deploy-app/solution.yaml to bump the version:
metadata:
name: deploy-app
version: 2.0.0Step 2: Build v2#
scafctl build solution -f deploy-app/solution.yamlscafctl build solution -f deploy-app/solution.yamlExpected output:
💡 Bundled 4 file(s) (1.2 KB, deduplicated: 1 layer(s))
✅ Built deploy-app@2.0.0
💡 Digest: sha256:xyz789...
💡 Catalog: ~/.local/share/scafctl/catalogNotice it now bundles 4 files (the new staging config was picked up by configs/**/*.yaml).
Step 3: Compare the Two Versions#
scafctl bundle diff deploy-app@1.0.0 deploy-app@2.0.0scafctl bundle diff deploy-app@1.0.0 deploy-app@2.0.0The output shows files added, modified, and removed between the two versions.
Step 4: Show Only File Changes#
scafctl bundle diff deploy-app@1.0.0 deploy-app@2.0.0 --files-onlyscafctl bundle diff deploy-app@1.0.0 deploy-app@2.0.0 --files-onlyStep 5: Show Only Solution Structure Changes#
scafctl bundle diff deploy-app@1.0.0 deploy-app@2.0.0 --solution-onlyscafctl bundle diff deploy-app@1.0.0 deploy-app@2.0.0 --solution-onlyThis shows only changes to the solution YAML itself (resolvers added/removed, actions changed, etc.).
Step 6: Get Diff Output as YAML#
scafctl bundle diff deploy-app@1.0.0 deploy-app@2.0.0 -o yamlscafctl bundle diff deploy-app@1.0.0 deploy-app@2.0.0 -o yamlStep 7: Clean Up#
scafctl catalog delete deploy-app@1.0.0 --kind solution
scafctl catalog delete deploy-app@2.0.0 --kind solution
scafctl catalog prune
rm -rf deploy-app/scafctl catalog delete deploy-app@1.0.0 --kind solution
scafctl catalog delete deploy-app@2.0.0 --kind solution
scafctl catalog prune
rm -rf deploy-app/What You Learned#
scafctl bundle diff REF_A REF_Bcompares two versions of a bundled solution--files-onlyshows only file-level changes (added, modified, removed)--solution-onlyshows only solution structure changes (resolvers, actions)-o yamlor-o jsongives machine-readable diff output
Using the Catalog with the MCP Server#
When using AI agents (VS Code Copilot, Claude, Cursor), the MCP server provides catalog tools:
catalog_list– List catalog entries filtered by kind and namecatalog_inspect– Get detailed metadata for a specific catalog artifact – version, kind, digest, created timestamp, and dependency list
The AI can inspect catalog artifacts, look up solution versions, and help you manage your catalog.
Next Steps#
- Go Templates Tutorial – Generate structured text with Go templates
- Snapshots Tutorial – Capture and compare execution snapshots
- Functional Testing Tutorial – Write and run automated tests
- Configuration Tutorial – Manage application configuration
- MCP Server Tutorial – AI-assisted catalog management