Callback Contract
The framework calls your workflows (callbacks) during CI/CD execution. This document defines the contract your workflows must follow.
Every callback (validate, build, deploy, publish) is a reusable workflow that you declare with workflow: in the manifest. The framework invokes it with workflow_call.
Migrating from inline run:/shell: callbacks
Section titled “Migrating from inline run:/shell: callbacks”Inline run:/shell: callbacks were removed. A callback can no longer carry a run: script or a shell: setting in the manifest; it must point at a reusable workflow via workflow:. The manifest still parses these keys, but validation now rejects them.
To migrate, move the script into a reusable workflow under .github/workflows/, expose it with on: workflow_call (declaring the standard environment, sha, and dry_run inputs), and replace the callback’s run:/shell: with workflow: .github/workflows/<name>.yaml. The script text becomes a run: step inside that workflow’s job. The sections below show the required structure for each callback type.
Overview
Section titled “Overview”Adopting repositories provide callback workflows that the framework invokes:
Framework Your Repository┌─────────────────┐ ┌──────────────────┐│ orchestrate.yaml│──workflow_call──▶│ validate.yaml ││ │──workflow_call──▶│ build-app.yaml ││ │──workflow_call──▶│ deploy-cdk.yaml ││ │ │ ││ promote.yaml │──workflow_call──▶│ deploy-svc.yaml ││ │──workflow_call──▶│ publish.yaml │└─────────────────┘ └──────────────────┘Callback Types
Section titled “Callback Types”| Type | Purpose | Standard Inputs |
|---|---|---|
| Validate | Pre-build checks (lint, test) | environment, sha, dry_run |
| Build | Produce artifacts | environment, sha, dry_run |
| Deploy | Apply changes to an environment | environment, sha, dry_run, plus build outputs |
| Publish | Retag artifacts at the prerelease->release boundary | build_name, old_version, new_version, sha, artifact_id |
Standard Inputs
Section titled “Standard Inputs”The framework always passes these to validate/build/deploy callbacks:
| Input | Type | Description |
|---|---|---|
environment | string | Target environment (e.g., dev, test, prod) |
sha | string | Commit SHA being processed |
dry_run | boolean | If true, skip mutating operations |
Any other inputs your callback needs must come from one of:
- Static
inputs:in the manifest - Per-environment
env_inputs:in the manifest - Outputs declared by a
depends_on:callback (auto-discovered)
The framework parses your workflow files to discover declared outputs: and forwards them to dependents as inputs by name.
Build Workflow Contract
Section titled “Build Workflow Contract”Build workflows produce artifacts (Docker images, binaries).
Required Structure
Section titled “Required Structure”name: Build App
on: workflow_call: inputs: environment: type: string required: true sha: type: string required: true dry_run: type: boolean required: false default: false # Add custom inputs as needed outputs: artifact_id: description: Immutable artifact identifier (e.g., image digest) value: ${{ jobs.build.outputs.artifact_id }} # Add custom outputs as neededOutputs
Section titled “Outputs”| Output | Required | Description |
|---|---|---|
artifact_id | Recommended | Immutable artifact identifier (image digest, binary checksum). Captured to state and passed to publish callbacks. |
Additional declared outputs are forwarded to dependent deploys as inputs by name.
Example
Section titled “Example”name: Build App
on: workflow_call: inputs: environment: type: string required: true sha: type: string required: true dry_run: type: boolean required: false default: false dockerfile: type: string required: false default: ./Dockerfile outputs: artifact_id: description: Image digest value: ${{ jobs.build.outputs.digest }} image_tag: description: Built image tag value: ${{ jobs.build.outputs.tag }}
jobs: build: runs-on: ubuntu-latest outputs: digest: ${{ steps.push.outputs.digest }} tag: ${{ steps.meta.outputs.tag }} steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.sha }}
- uses: docker/setup-buildx-action@v3
- name: Generate tag id: meta run: | TAG="${{ github.sha }}-$(date +%s)" echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Build image uses: docker/build-push-action@v5 with: context: . file: ${{ inputs.dockerfile }} push: false load: true tags: myrepo/app:${{ steps.meta.outputs.tag }}
- name: Push image id: push if: ${{ !inputs.dry_run }} run: | docker push myrepo/app:${{ steps.meta.outputs.tag }} DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \ myrepo/app:${{ steps.meta.outputs.tag }} | cut -d@ -f2) echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"Deploy Workflow Contract
Section titled “Deploy Workflow Contract”Deploy workflows apply changes to an environment.
Required Structure
Section titled “Required Structure”name: Deploy Services
on: workflow_call: inputs: environment: type: string required: true sha: type: string required: true dry_run: type: boolean required: false default: false # Plus any outputs from depends_on builds (e.g., image_tag, artifact_id) outputs: # Add custom outputs as neededReceiving Build Outputs
Section titled “Receiving Build Outputs”When a deploy declares depends_on: [app] and the app build declares an image_tag output, the deploy callback receives image_tag as an input automatically:
on: workflow_call: inputs: environment: type: string required: true sha: type: string required: true image_tag: type: string required: true # Provided by the framework via output chainingExample
Section titled “Example”name: Deploy Services
on: workflow_call: inputs: environment: type: string required: true sha: type: string required: true image_tag: type: string required: true dry_run: type: boolean required: false default: false cluster: type: string required: false default: default
jobs: deploy: runs-on: ubuntu-latest environment: ${{ inputs.environment }} # Enables environment protection steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.sha }}
- name: Configure AWS uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/deploy-role aws-region: us-east-1
- name: Deploy if: ${{ !inputs.dry_run }} run: | aws ecs update-service \ --cluster ${{ inputs.cluster }} \ --service my-service \ --force-new-deployment \ --task-definition my-task:${{ inputs.image_tag }}Validate Workflow Contract
Section titled “Validate Workflow Contract”Optional pre-build validation.
Required Structure
Section titled “Required Structure”name: Validate
on: workflow_call: inputs: environment: type: string required: true sha: type: string required: true dry_run: type: boolean required: false default: falseExample
Section titled “Example”name: Validate
on: workflow_call: inputs: environment: type: string required: true sha: type: string required: true dry_run: type: boolean required: false default: false check_lint: type: boolean required: false default: true
jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.sha }}
- uses: actions/setup-go@v5 with: go-version: "1.25"
- name: Lint if: ${{ inputs.check_lint }} run: golangci-lint run ./...
- name: Test run: go test -v ./...Publish Workflow Contract
Section titled “Publish Workflow Contract”The publish callback runs once per build at the prerelease->release boundary (when a draft RC is published as a final semver release). Use it to retag artifacts that still carry their RC version.
Required Structure
Section titled “Required Structure”name: Publish
on: workflow_call: inputs: build_name: type: string required: true old_version: type: string required: true new_version: type: string required: true sha: type: string required: true artifact_id: type: string required: falseInputs
Section titled “Inputs”| Input | Description |
|---|---|
build_name | Which build’s artifacts to retag (matches a builds[].name) |
old_version | RC version currently in the registry (e.g., v1.0.0-rc.2) |
new_version | Final semver to apply (e.g., v1.0.0) |
sha | Git commit SHA |
artifact_id | Immutable digest from the build’s artifact_id output (empty if not declared) |
Example
Section titled “Example”name: Publish
on: workflow_call: inputs: build_name: type: string required: true old_version: type: string required: true new_version: type: string required: true sha: type: string required: true artifact_id: type: string required: false
jobs: retag: runs-on: ubuntu-latest steps: - name: Pull and retag run: | docker pull myrepo/${{ inputs.build_name }}:${{ inputs.old_version }} docker tag \ myrepo/${{ inputs.build_name }}:${{ inputs.old_version }} \ myrepo/${{ inputs.build_name }}:${{ inputs.new_version }} docker push myrepo/${{ inputs.build_name }}:${{ inputs.new_version }}The framework only carries metadata. The publish callback performs the registry operation. When artifact_id is present, use it instead of old_version so the target is unambiguous.
Custom Inputs
Section titled “Custom Inputs”Pass custom inputs via inputs and env_inputs in the manifest:
ci: config: builds: - name: app workflow: .github/workflows/build-app.yaml inputs: dockerfile: ./docker/Dockerfile.prod build_args: "VERSION=1.0.0" env_inputs: prod: sign_image: trueYour workflow receives them as inputs:
on: workflow_call: inputs: dockerfile: type: string build_args: type: string sign_image: type: booleanOutput Chaining
Section titled “Output Chaining”Outputs from one callback are passed to dependents:
ci: config: builds: - name: app workflow: .github/workflows/build-app.yaml # Outputs declared in the workflow: artifact_id, image_tag
deploys: - name: services workflow: .github/workflows/deploy-services.yaml depends_on: [app] # Receives: artifact_id, image_tag as inputsThe framework parses workflow files for outputs: and forwards them automatically.
State Capture
Section titled “State Capture”The framework automatically captures into per-environment state:
| Field | Source | Where |
|---|---|---|
artifact_id | Build’s artifact_id output | state.<env>.builds.<name>.artifact_id |
artifact_id is the canonical identifier passed to publish callbacks at release time.
ci: state: dev: builds: app: sha: abc123 built_at: "2026-01-15T10:25:00Z" artifact_id: "sha256:def456..."Environment Protection
Section titled “Environment Protection”Use GitHub Environment protection for approval gates. Because every deploy is a reusable workflow, declare the environment: key on the job inside your reusable workflow. GitHub Actions only allows a job-level environment: key on a steps job, never on a job that calls a reusable workflow with uses:, so the caller job cascade generates cannot carry it.
cascade passes the target environment name to your workflow as the environment input, so wire it through:
# your reusable deploy workflowjobs: deploy: runs-on: ubuntu-latest environment: ${{ inputs.environment }} # protection lives here steps: - run: ./deploy.shcascade cannot set environment: on the caller job it generates: GitHub Actions rejects a workflow that puts environment: on a uses: job. cascade therefore emits only the with: environment: input on the caller and relies on your reusable workflow to apply the protection rules. cascade prints a generate-time note when gha_environment is configured for an environment, reminding you to declare environment: inside the reusable workflow.
Configure protection in GitHub: Settings -> Environments -> Add required reviewers.
Dry Run Handling
Section titled “Dry Run Handling”All callbacks should respect dry_run:
- name: Deploy if: ${{ !inputs.dry_run }} run: | # Actual deployment
- name: Dry run preview if: ${{ inputs.dry_run }} run: | echo "Would deploy ${{ inputs.image_tag }}"Error Handling
Section titled “Error Handling”Callback failures are handled by the on_failure policy:
| Policy | Behavior |
|---|---|
abort | Fail the entire workflow |
continue | Other callbacks proceed |
With retries: N, failed callbacks retry up to N times before final failure.
Keep callbacks focused
Section titled “Keep callbacks focused”- Build -> produce artifacts
- Deploy -> apply to environment
- Validate -> check quality
- Publish -> retag
Consistent naming
Section titled “Consistent naming”build-{name}.yamldeploy-{name}.yamlvalidate.yamlpublish.yamlDeclare outputs explicitly
Section titled “Declare outputs explicitly”The framework discovers outputs by parsing your workflow files. Declare them under on.workflow_call.outputs:
outputs: artifact_id: description: Image digest value: ${{ jobs.build.outputs.digest }}Testing locally
Section titled “Testing locally”# Use act to invoke workflow_call locallyact workflow_call -j build \ --input environment=dev \ --input sha=$(git rev-parse HEAD) \ --input dry_run=true