A GitHub Action that generates a deployment matrix for multi-service, multi-environment deployments. It reads your repository's configuration to produce a GitHub Actions strategy.matrix JSON object, enabling parallel deployments across services and environments.
The action supports two configuration formats. Both can coexist in the same repository — their matrices are merged with deduplication by service_name + overlay.
The Skyhook format defines services and environments in a single YAML file at .skyhook/skyhook.yaml.
Environment discovery works in two ways depending on whether a service has a deploymentRepo:
- Without
deploymentRepo: environments are read from theenvironments[]array inskyhook.yaml(local path). - With
deploymentRepo: environments are discovered from the remote deployment repository — overlay directories are listed from{deploymentRepoPath}/overlays/, and environment details (cluster, cloud provider, account, location, namespace) are read fromskyhook/environments/{name}.yamlfiles in that repo.
This means different services can have different sets of environments — one service might deploy to dev and staging (from its deployment repo), while another deploys to dev, staging, and prod (from the local config or a different deployment repo).
services:
- name: api-gateway
path: apps/api-gateway
deploymentRepo: my-org/deployment-repo # environments discovered from remote repo
deploymentRepoPath: api-gateway # path within deployment repo (defaults to service name)
- name: worker
path: apps/worker
# no deploymentRepo — uses local environments[] below
environments: # used by services without deploymentRepo
- name: dev
clusterName: nonprod-cluster
cloudProvider: gcp
account: my-project-nonprod
location: us-east1-b
namespace: dev
- name: prod
clusterName: prod-cluster
cloudProvider: gcp
account: my-project-prod
location: us-east1-b
namespace: prodFor services with deploymentRepo, the action clones the repo (shallow, --depth 1) and reads:
deployment-repo/
├── api-gateway/
│ └── overlays/
│ ├── dev/ # each directory = one environment
│ ├── staging/
│ └── prod/
└── skyhook/
└── environments/
├── dev.yaml # environment details
├── staging.yaml
└── prod.yaml
Each environment file (skyhook/environments/{name}.yaml):
clusterName: my-cluster
cloudProvider: gcp
account: my-project-id
location: us-central1
namespace: default
autoDeploy: trueThe environment name comes from the filename, not from inside the file. If an environment YAML file is missing, the overlay is still included with only its name populated.
Multiple services can reference the same deployment repo — it is cloned once and shared. Clone and environment config caches are keyed by repo:branch and repo:branch:envName respectively, so different deployment repos with same-named environments never collide.
The Koala format uses .koala-monorepo.json at the repository root to list services and .koala.toml files per service for environment configuration. Processing is handled by the external workflow-utils CLI (installed automatically via npx).
- name: Create deployment matrix
id: matrix
uses: skyhook-io/generate-service-matrix@v1
with:
tag: v1.2.3
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy services
strategy:
matrix: ${{ fromJson(steps.matrix.outputs.matrix) }}
fail-fast: false
runs-on: ubuntu-latest
steps:
- run: echo "Deploying ${{ matrix.service_name }} (${{ matrix.service_tag }}) to ${{ matrix.overlay }}"- name: Deploy to production only
id: matrix
uses: skyhook-io/generate-service-matrix@v1
with:
overlay: prod
tag: ${{ github.ref_name }}
github-token: ${{ secrets.GITHUB_TOKEN }}- name: Deploy preview
id: matrix
uses: skyhook-io/generate-service-matrix@v1
with:
overlay: dev
branch: ${{ github.head_ref }}
tag: pr-${{ github.event.pull_request.number }}
github-token: ${{ secrets.GITHUB_TOKEN }}| Input | Description | Required | Default |
|---|---|---|---|
tag |
The image tag to deploy | Yes | - |
github-token |
GitHub token for API access and deployment repo cloning | Yes | - |
overlay |
Environment filter (e.g., dev, staging, prod). If omitted, all environments are included. |
No | (all) |
branch |
Branch for deployment context and deployment repo cloning. If omitted, uses the remote's default branch (HEAD). | No | (HEAD) |
repo-path |
Path to the repository root | No | . |
max-length |
Maximum length for the generated service_tag. When {service_name}_{tag}_{counter} would exceed this, the tag middle is truncated and any trailing -/_ is stripped so the service prefix and counter suffix are preserved. Default matches the Kubernetes label limit; raise to 128 if service_tag is only used as an image tag or GitHub release name. |
No | 63 |
| Output | Description |
|---|---|
matrix |
JSON string of the deployment matrix, ready for strategy.matrix via fromJson() |
{
"include": [
{
"service_name": "api-gateway",
"service_dir": "apps/api-gateway",
"service_repo": "my-org/my-app",
"service_tag": "api-gateway_v1.2.3_01",
"deployment_repo": "my-org/deployment-repo",
"deployment_folder_path": "api-gateway",
"overlay": "dev",
"cluster": "nonprod-cluster",
"cluster_location": "us-east1-b",
"cloud_provider": "gcp",
"namespace": "dev",
"account": "my-project-nonprod",
"auto_deploy": "false"
}
]
}| Field | Source |
|---|---|
service_name |
skyhook.yaml services[].name |
service_dir |
skyhook.yaml services[].path |
service_repo |
GITHUB_REPOSITORY env var |
service_tag |
Computed: {service_name}_{tag}_{counter} |
deployment_repo |
skyhook.yaml services[].deploymentRepo |
deployment_folder_path |
skyhook.yaml services[].deploymentRepoPath |
overlay |
Environment name |
cluster |
environments[].clusterName (local or remote) |
cluster_location |
environments[].location |
cloud_provider |
environments[].cloudProvider |
namespace |
environments[].namespace |
account |
environments[].account |
auto_deploy |
environments[].autoDeploy (default false) |
Each matrix entry gets a unique service_tag in the format {service_name}_{tag}_{counter} (e.g., api-gateway_v1.2.3_01). Counters are per-service and are seeded from two sources to prevent duplicate tags across multiple runs:
- Existing git tags — the action queries
git ls-remote --tags originfor tags matching{service_name}_{tag}_NNand starts after the highest existing counter. - Koala matrix output — if both Koala and Skyhook configs are present, counters from the Koala matrix carry forward into the Skyhook matrix.
When {service_name}_{tag}_{counter} would exceed max-length (default 63, matching the Kubernetes label limit), the middle {tag} portion is truncated from the right and any resulting trailing -/_ is stripped. The service prefix and counter suffix are preserved so each matrix entry stays unique and the tag never ends in a separator. Raise max-length to 128 if you only use service_tag as an image tag or GitHub release name.
permissions:
contents: readThe github-token must have read access to any deployment repos referenced by services[].deploymentRepo.
MIT