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#

  1. Building Your First Solution
  2. Running from the Catalog
  3. Listing and Inspecting
  4. Managing Multiple Versions
  5. Deleting and Pruning
  6. Exporting and Importing
  7. Tagging Artifacts
  8. Remote Registries
  9. Bundling File Dependencies
  10. Verifying and Extracting Bundles
  11. 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#

Note: build is a backward-compatible alias for package (e.g. scafctl build solution still works).

scafctl package solution -f greeting.yaml
scafctl package solution -f greeting.yaml

Expected output:

 ✅ Built greeting@1.0.0
 💡   Digest: sha256:abc123...
 💡   Catalog: ~/.local/share/scafctl/catalog

The 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 package solution -f greeting.yaml --version 1.0.1
scafctl package solution -f greeting.yaml --version 1.0.1

Expected output:

 ✅ Built greeting@1.0.1
 💡   Digest: sha256:def456...
 💡   Catalog: ~/.local/share/scafctl/catalog

What You Learned#

  • scafctl package solution -f FILE packages a solution YAML into the local OCI catalog
  • The name and version come from metadata.name and metadata.version by default
  • Use --version to override the version at build time
  • Use --name to 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
scafctl run resolver -f greeting -o yaml

Expected output:

message: Hello, World!
name: World

No 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 -r name=Alice
scafctl run resolver -f greeting -o yaml -r name=Alice

Expected output:

message: Hello, Alice!
name: Alice

Step 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 -r name=Bob
scafctl run resolver -f greeting@1.0.0 -o yaml -r name=Bob

Expected output:

message: Hello, Bob!
name: Bob

Step 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=Carol
scafctl run resolver -f greeting -o yaml -e '_.message' -r name=Carol

Expected output:

Hello, Carol!

What You Learned#

  • scafctl run resolver -f NAME runs a solution from the catalog by name
  • Without a version, it picks the highest semantic version available
  • Use NAME@VERSION to pin a specific version
  • Parameters work the same way as with file-based solutions (-r key=value)
  • Use -e to filter output to specific values

Listing and Inspecting#

Step 1: List Everything in the Catalog#

scafctl catalog list -o yaml
scafctl catalog list -o yaml

Note: Pre-release versions (e.g., 1.0.0-rc.1) are hidden by default. When artifacts are hidden, a warning shows how many were filtered. Use --pre-release to include them, or --pre-release --all-versions to see every version.

Expected 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: local

Step 2: Filter by Name#

scafctl catalog list --name greeting -o yaml
scafctl catalog list --name greeting -o yaml

This shows only artifacts with the name greeting.

Step 3: Inspect a Specific Artifact#

scafctl catalog inspect greeting -o yaml
scafctl catalog inspect greeting -o yaml

Expected 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.1

Without 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.1

What You Learned#

  • scafctl catalog list shows every artifact in the catalog
  • --name filters by solution name
  • scafctl catalog inspect NAME shows detailed metadata for a specific artifact
  • Without a version, inspect/list show all versions or the highest version respectively
  • -e with 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 package solution -f greeting-v2.yaml
scafctl package solution -f greeting-v2.yaml

Expected output:

 ✅ Built greeting@2.0.0
 💡   Digest: sha256:789abc...
 💡   Catalog: ~/.local/share/scafctl/catalog

Step 3: Verify Both Versions Exist#

scafctl catalog list --name greeting -o yaml
scafctl catalog list --name greeting -o yaml

Expected 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 -r name=Alice
scafctl run resolver -f greeting -o yaml -r name=Alice

Expected 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 -r name=Alice
scafctl run resolver -f greeting@1.0.0 -o yaml -r name=Alice

Expected output:

message: Hello, Alice!
name: Alice

The v1 solution doesn’t have a timestamp – confirming you’re running the original version.

Step 6: Try to Overwrite an Existing Version#

scafctl package solution -f greeting-v2.yaml --version 2.0.0
scafctl package solution -f greeting-v2.yaml --version 2.0.0

Expected output:

 ❌ artifact greeting@2.0.0 already exists in catalog "local" (use --force to overwrite)

Use --force to overwrite:

scafctl package solution -f greeting-v2.yaml --version 2.0.0 --force
scafctl package solution -f greeting-v2.yaml --version 2.0.0 --force

What You Learned#

  • Multiple versions of the same solution coexist in the catalog
  • Without a version, run picks the highest semantic version
  • Use NAME@VERSION to pin to a specific version
  • Use --force to overwrite an existing version

Deleting and Pruning#

Step 1: Delete a Specific Version#

scafctl catalog delete greeting@1.0.1 --kind solution
scafctl catalog delete greeting@1.0.1 --kind solution

Expected output:

 ✅ Deleted greeting@1.0.1

You 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 yaml
scafctl catalog list --name greeting -o yaml

The 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 prune
scafctl catalog prune

Expected output:

 ✅ Pruned catalog
 💡   Removed manifests: 1
 💡   Removed blobs: 2
 💡   Reclaimed: 1.5 KB

Step 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 prune
scafctl catalog delete greeting@1.0.0 --kind solution
scafctl catalog delete greeting@2.0.0 --kind solution
scafctl catalog prune

What You Learned#

  • scafctl catalog delete NAME@VERSION --kind solution removes a single version
  • You must specify the version – this prevents accidental bulk deletion
  • scafctl catalog prune removes orphaned blobs and reclaims disk space
  • Always prune after deleting to free up storage

Delete All Local Artifacts#

Use --all to remove every artifact from the local catalog at once. This is useful for resetting your local environment:

# Interactive (prompts for confirmation)
scafctl catalog delete --all

# Non-interactive (skip confirmation)
scafctl catalog delete --all --force
# Interactive (prompts for confirmation)
scafctl catalog delete --all

# Non-interactive (skip confirmation)
scafctl catalog delete --all --force

Expected output:

 ✅ Deleted 5 artifact(s) from local catalog

--all only applies to the local catalog. It cannot be combined with --catalog for remote registries. Positional arguments are not allowed with --all.


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 package solution -f greeting.yaml
scafctl package solution -f greeting.yaml

Step 2: Export to a Tar Archive#

scafctl catalog save greeting@1.0.0 -o greeting-v1.tar
scafctl catalog save greeting@1.0.0 -o greeting-v1.tar

Expected 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 prune
scafctl catalog delete greeting@1.0.0 --kind solution
scafctl catalog prune

Step 4: Verify It’s Gone#

scafctl catalog list --name greeting -o yaml
scafctl catalog list --name greeting -o yaml

Expected output (empty or no greeting entries).

Step 5: Import from the Tar Archive#

scafctl catalog load --input greeting-v1.tar
scafctl catalog load --input greeting-v1.tar

Expected output:

 ✅ Loaded artifact from greeting-v1.tar

Step 6: Confirm It Was Loaded#

scafctl catalog list --name greeting -o yaml
scafctl catalog list --name greeting -o yaml

Expected output:

- name: greeting
  version: 1.0.0
  kind: solution
  digest: sha256:abc123...
  createdAt: "2026-02-17 10:00:00"
  catalog: local

Step 7: Try Loading Again (Conflict)#

scafctl catalog load --input greeting-v1.tar
scafctl catalog load --input greeting-v1.tar

Expected output:

 ❌ artifact already exists (use --force to overwrite)

Use --force to overwrite:

scafctl catalog load --input greeting-v1.tar --force
scafctl catalog load --input greeting-v1.tar --force

Air-Gapped Transfer Workflow#

Here’s how the full workflow looks in practice:

# On the connected machine:
scafctl package 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 -r env=prod
# On the connected machine:
scafctl package 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 -r env=prod

What You Learned#

  • scafctl catalog save NAME@VERSION -o FILE exports an artifact as an OCI tar archive
  • scafctl catalog load --input FILE imports an artifact from a tar archive
  • Use --force to 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 stable
scafctl catalog tag greeting@1.0.0 stable

Expected output:

 ✅ Tagged greeting@1.0.0 as "stable"

Step 2: View the Tag in the Catalog#

scafctl catalog list --name greeting -o yaml
scafctl catalog list --name greeting -o yaml

The 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 production
scafctl catalog tag greeting@1.0.0 production

You can create as many tags as needed. Tags are freeform strings – they cannot be valid semver versions (use scafctl package for that).

What You Learned#

  • scafctl catalog tag NAME@VERSION ALIAS creates 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
# Scope is auto-detected from catalog config's authScope field
scafctl auth login gcp
scafctl catalog login us-docker.pkg.dev

# Azure Container Registry
scafctl auth login entra
scafctl catalog login myacr.azurecr.io

Direct 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 @-

# Using a token directly (CI/automation)
scafctl catalog login quay.io --username admin --password "$REGISTRY_PASSWORD"

Config-based automatic authentication:

For catalogs defined in your scafctl config, set authProvider to enable automatic authentication without a separate login step. For handlers that require an OAuth scope (GCP, Entra), set authScope on the catalog – catalog login will auto-detect it:

# ~/.config/scafctl/config.yaml
catalogs:
  - name: company-registry
    type: oci
    url: oci://ghcr.io/myorg/scafctl
    authProvider: github

  - name: gcp-registry
    type: oci
    url: oci://us-docker.pkg.dev/my-project/scafctl
    authProvider: gcp
    authScope: https://www.googleapis.com/auth/cloud-platform

With 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-auth

This 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.io

Removing credentials:

# Remove credentials for a specific registry
scafctl catalog logout ghcr.io

# Remove all stored registry credentials
scafctl catalog logout --all

scafctl resolves credentials in this order:

PrioritySource
1Docker/Podman config files
2scafctl native credential store (~/.config/scafctl/registries.json)
3Dynamic 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-stdin

Using a Personal Access Token:

  1. Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
  2. Generate a new token with write:packages and read:packages scopes
  3. Log in:
echo "YOUR_GITHUB_TOKEN" | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin

scafctl checks these credential locations in order:

PriorityLocation
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/scafctl
scafctl catalog push greeting@1.0.0 --catalog ghcr.io/myorg/scafctl

Expected output:

 ✅ Pushed greeting@1.0.0

The 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/scafctl
scafctl catalog push greeting@1.0.0 --as hello-world --catalog ghcr.io/myorg/scafctl

This 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.0
scafctl catalog pull ghcr.io/myorg/scafctl/solutions/greeting@1.0.0

Expected output:

 ✅ Pulled greeting@1.0.0

The artifact is now in your local catalog. You can run it with:

scafctl run resolver -f greeting -o yaml -r name=Alice
scafctl run resolver -f greeting -o yaml -r name=Alice

Step 5: Pull with a Different Local Name#

scafctl catalog pull ghcr.io/myorg/scafctl/solutions/greeting@1.0.0 --as my-greeting
scafctl catalog pull ghcr.io/myorg/scafctl/solutions/greeting@1.0.0 --as my-greeting

This 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.0
scafctl catalog delete ghcr.io/myorg/scafctl/solutions/greeting@1.0.0

Note: 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 -d

Check that:

  1. Your token has write:packages scope
  2. You’re logged in: docker login ghcr.io
  3. 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 --insecure
scafctl catalog push greeting@1.0.0 --catalog localhost:5000 --insecure
scafctl catalog pull localhost:5000/solutions/greeting@1.0.0 --insecure

Supported Registries#

scafctl works with any OCI-compliant registry:

RegistryURL Format
GitHub Container Registryghcr.io/OWNER
Docker Hubdocker.io/NAMESPACE
Azure Container RegistryREGISTRY.azurecr.io
Amazon ECRACCOUNT.dkr.ecr.REGION.amazonaws.com
Google Artifact RegistryREGION-docker.pkg.dev/PROJECT
Harborharbor.example.com/PROJECT
Local Registrylocalhost:5000

What You Learned#

  • scafctl catalog push NAME@VERSION --catalog REGISTRY pushes to a remote registry
  • scafctl catalog pull REGISTRY/solutions/NAME@VERSION pulls to your local catalog
  • --as lets you rename artifacts during push or pull
  • --force overwrites existing artifacts
  • --insecure allows 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/configs

Create deploy-app/configs/dev.yaml:

name: my-app
namespace: dev
replicas: 1
image: my-app:latest
port: 8080

Create deploy-app/configs/prod.yaml:

name: my-app
namespace: production
replicas: 3
image: my-app:1.2.0
port: 8080

Create 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: config

Notice 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 package solution -f deploy-app/solution.yaml --dry-run
scafctl package solution -f deploy-app/solution.yaml --dry-run

Expected 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.0

The dry-run shows:

  • Static analysis discovered – files scafctl found by analyzing your resolvers
  • Explicit includes – files matched by your bundle.include patterns
  • Dynamic paths – warnings about paths that can’t be statically resolved

Step 4: Build the Solution#

scafctl package solution -f deploy-app/solution.yaml
scafctl package solution -f deploy-app/solution.yaml

Expected 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/catalog

Step 5: Run from the Catalog with Dev Config#

scafctl run resolver -f deploy-app -o yaml -e '_.["rendered-deployment"]' -r environment=dev
scafctl run resolver -f deploy-app -o yaml -e '_.["rendered-deployment"]' -r environment=dev

Expected 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: 8080

Step 6: Switch to Prod#

scafctl run resolver -f deploy-app -o yaml -e '_.["rendered-deployment"]' -r environment=prod
scafctl run resolver -f deploy-app -o yaml -e '_.["rendered-deployment"]' -r environment=prod

Expected 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: 8080

The 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 file provider 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.include patterns
  • --dry-run shows exactly what would be bundled, including warnings for dynamic paths
  • bundle.exclude filters 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/templates

Create nested-demo/parent-config.txt:

parent data content

Create 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 package solution -f nested-demo/parent.yaml --dry-run
scafctl package solution -f nested-demo/parent.yaml --dry-run

Expected 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.0

Notice 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.

Tip: To let a parent override a value the child computes internally (for example, enriching a map the child renders from), use the child-opt-in value override contract. See Solution Provider – Value Override Contract .

Step 4: Build and Run#

scafctl package solution -f nested-demo/parent.yaml
scafctl run resolver -f nested-demo -o json
scafctl package solution -f nested-demo/parent.yaml
scafctl run resolver -f nested-demo -o json

How It Works#

  1. Static analysisscafctl package parses the parent solution and finds the solution provider reference to ./sub/child.yaml
  2. Recursive discovery – It then parses sub/child.yaml and discovers its own file dependencies (templates/greeting.tmpl)
  3. Path normalization – All paths are normalized relative to the parent bundle root (sub/templates/greeting.tmpl not templates/greeting.tmpl)
  4. 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 solution provider are automatically discovered during build
  • All nested file dependencies are included in the parent bundle – no extra bundle.include needed
  • Path normalization ensures sub-solution paths resolve correctly within the bundle
  • Circular sub-solution references are detected and reported at build time
  • --dry-run shows the full recursive file tree

Examining Bundles#

Bundle completeness is verified automatically – you do not run a standalone verify command. When you scafctl package solution, the build checks that all files referenced in the solution exist in the bundle and that bundle.include glob patterns cover the expected files; the build fails if the bundle is incomplete (use --strict to make warnings fatal, or --no-verify to skip the check). Completeness is re-checked when a consumer runs scafctl catalog pull (warns by default, or fails with --strict). After building, you can examine the bundle’s contents.

Step 1: List Bundle Contents#

See what files are inside the bundle without extracting them:

scafctl extract bundle deploy-app@1.0.0 --list-only
scafctl extract bundle deploy-app@1.0.0 --list-only

Expected output:

  templates/deployment.yaml    (500 B)
  configs/dev.yaml             (100 B)
  configs/prod.yaml            (105 B)
💡 Total: 3 file(s), 705 B

Step 2: Extract to a Directory#

Extract the bundled files to inspect them:

scafctl extract bundle deploy-app@1.0.0 --output-dir ./extracted
scafctl extract bundle deploy-app@1.0.0 --output-dir ./extracted

Check the extracted files:

ls -R extracted/

You’ll see the full directory structure preserved:

extracted/
├── configs/
│   ├── dev.yaml
│   └── prod.yaml
└── templates/
    └── deployment.yaml

Step 3: Extract Files for a Specific Resolver#

You can extract only the files needed by a specific resolver:

scafctl extract bundle deploy-app@1.0.0 --resolver config --output-dir ./config-only
scafctl extract bundle deploy-app@1.0.0 --resolver config --output-dir ./config-only

This uses static analysis to determine which files the config resolver references.

Step 4: Clean Up#

rm -rf extracted/ config-only/

What You Learned#

  • Bundle completeness is verified automatically – scafctl package solution fails the build if the bundle is incomplete (use --strict/--no-verify), and scafctl catalog pull re-checks it (warns by default, or fails with --strict)
  • scafctl extract bundle --list-only shows bundle contents without extracting
  • scafctl extract bundle --output-dir DIR extracts files to a directory
  • --resolver NAME extracts only files needed by a specific resolver
  • Use --flatten to extract all files to a single directory (no subdirectories)

Comparing Bundle Versions#

When you release a new version of a bundled solution, diff bundle 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: 8080

Then update deploy-app/solution.yaml to bump the version:

metadata:
  name: deploy-app
  version: 2.0.0

Step 2: Build v2#

scafctl package solution -f deploy-app/solution.yaml
scafctl package solution -f deploy-app/solution.yaml

Expected 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/catalog

Notice it now bundles 4 files (the new staging config was picked up by configs/**/*.yaml).

Step 3: Compare the Two Versions#

scafctl diff bundle deploy-app@1.0.0 deploy-app@2.0.0
scafctl diff bundle deploy-app@1.0.0 deploy-app@2.0.0

The output shows files added, modified, and removed between the two versions.

Step 4: Show Only File Changes#

scafctl diff bundle deploy-app@1.0.0 deploy-app@2.0.0 --files-only
scafctl diff bundle deploy-app@1.0.0 deploy-app@2.0.0 --files-only

Step 5: Show Only Solution Structure Changes#

scafctl diff bundle deploy-app@1.0.0 deploy-app@2.0.0 --solution-only
scafctl diff bundle deploy-app@1.0.0 deploy-app@2.0.0 --solution-only

This shows only changes to the solution YAML itself (resolvers added/removed, actions changed, etc.).

Step 6: Get Diff Output as YAML#

scafctl diff bundle deploy-app@1.0.0 deploy-app@2.0.0 -o yaml
scafctl diff bundle deploy-app@1.0.0 deploy-app@2.0.0 -o yaml

Step 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 diff bundle REF_A REF_B compares two versions of a bundled solution
  • --files-only shows only file-level changes (added, modified, removed)
  • --solution-only shows only solution structure changes (resolvers, actions)
  • -o yaml or -o json gives 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 name
  • catalog_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#