You push v1.2.3 and expect a predictable sequence: tests pass → version is resolved → GitHub Release is created. In practice, teams usually pick one of two painful options:
-
One giant workflow — every stage in a single YAML file. It works until you need reuse,
workflow_call, or different triggers per stage. -
workflow_runchains — workflow A triggers workflow B. Passing outputs between runs is awkward, and renaming a workflow breaks the chain silently.
There is a middle path: keep small, focused stage workflows (the ones you already have), declare order and wiring in one pipeline file, and use a single orchestrator step on tag push.
This tutorial uses pipeline-compose-run — available on the GitHub Marketplace — and a copy-paste example you can drop into any repo.
Full example (copy .github/): examples/run-tag-release
What we are building
On git push origin v*:
release.yml ← one job, one action step
└─ pipeline.yml ← declares order + wiring
├─ ci.yml
├─ stage-version-sync.yml → exports version
└─ stage-release-publish.yml ← receives version
Enter fullscreen mode Exit fullscreen mode
No generated workflow to commit. No manual workflow_run graph.
Step 1 — Entry workflow
Create .github/workflows/release.yml:
name: Release
on:
push:
tags: ["v*"]
permissions:
contents: write
actions: write
jobs:
run-pipeline:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: aeswibon/[email protected]
with:
pipeline_file: .github/pipelines/pipeline.yml
github_token: ${{ github.token }}
Enter fullscreen mode Exit fullscreen mode
The actions: write permission is required because the action dispatches your stage workflows via workflow_dispatch.
Step 2 — Pipeline file (order only)
Create .github/pipelines/pipeline.yml:
name: pipeline
version: 1
stages:
- id: ci
workflow: .github/workflows/ci.yml
- id: version-sync
workflow: .github/workflows/stage-version-sync.yml
needs:
- ci
outputs:
- version
- id: release-publish
workflow: .github/workflows/stage-release-publish.yml
needs:
- version-sync
inputs:
version: ${{ context.version-sync.version }}
Enter fullscreen mode Exit fullscreen mode
This file is the source of truth for order. Stage implementations stay in normal workflow files you can also run manually or reuse via workflow_call.
The ${{ context.version-sync.version }} syntax resolves at runtime from the completed version-sync stage.
Step 3 — Stage workflows
Each stage must include workflow_dispatch. Downstream stages that receive values need matching workflow_dispatch inputs.
CI (.github/workflows/ci.yml)
name: CI
on:
workflow_dispatch:
push:
branches: [master]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: echo "Replace with your test/lint commands"
Enter fullscreen mode Exit fullscreen mode
Version sync — export outputs
GitHub’s API does not return job outputs for workflow_dispatch runs. pipeline-compose collects stage outputs from an artifact:
-
Artifact name:
pipeline-compose-<stage-id> -
File:
outputs.jsonwith your output keys
name: Version sync
on:
workflow_dispatch:
jobs:
version-sync:
runs-on: ubuntu-latest
steps:
- name: Resolve semver from ref
id: version
run: |
ref="${GITHUB_REF}"
if [[ "$ref" =~ ^refs/tags/v(.+)$ ]]; then
echo "value=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT"
else
echo "Expected a version tag ref, got: $ref" >&2
exit 1
fi
- name: Export outputs for pipeline-compose
if: success()
env:
VERSION: ${{ steps.version.outputs.value }}
run: |
mkdir -p pipeline-compose
jq -n --arg version "$VERSION" '{version: $version}' > pipeline-compose/outputs.json
- uses: actions/upload-artifact@v4
if: success()
with:
name: pipeline-compose-version-sync
path: pipeline-compose/outputs.json
retention-days: 1
Enter fullscreen mode Exit fullscreen mode
Note the artifact name matches the stage id: pipeline-compose-version-sync.
Release publish — consume version input
name: Release publish
on:
workflow_dispatch:
inputs:
version:
description: Semver without v prefix
type: string
required: true
permissions:
contents: write
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
tag="v${VERSION}"
if gh release view "$tag" >/dev/null 2>&1; then
echo "Release $tag already exists"
exit 0
fi
gh release create "$tag" --title "$tag" --generate-notes
Enter fullscreen mode Exit fullscreen mode
Step 4 — Ship it
git tag v1.0.0 && git push origin v1.0.0
Enter fullscreen mode Exit fullscreen mode
Open Actions → Release in your repo. Stages run in pipeline order; publish receives the version from sync.
Why not other approaches?
Approach Pain point Monolithic workflow Hard to reuse stages; noisy diffsworkflow_run chains
Fragile; outputs don’t flow cleanly
Generated workflow (compile)
Works, but you commit generated YAML
pipeline-compose-run
Ordered dispatch + context; one pipeline file
If you prefer committing a static graph with native GitHub needs:, see pipeline-compose-compile instead.
Troubleshooting
Symptom Fix403 on dispatch
Add actions: write on the entry job
Publish stage missing version
Artifact must be pipeline-compose-version-sync with outputs.json
Stage never runs
Add workflow_dispatch to that workflow
Wrong ref in stage
Pass ref: to the run action if needed
Try it now
- Marketplace: pipeline-compose-run
- Copy-paste example: examples/run-tag-release
- More examples: pipeline-compose/examples
Part of pipeline-compose. MIT License.
답글 남기기