DEV Community

Abhiuday
Abhiuday

Posted on

Tag release pipelines without a 400-line GitHub Actions workflow

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:

  1. One giant workflow — every stage in a single YAML file. It works until you need reuse, workflow_call, or different triggers per stage.
  2. workflow_run chains — 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/pipeline-compose-run@v0.3.0
        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.json with 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 diffs
workflow_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 Fix
403 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

  1. Marketplace: pipeline-compose-run
  2. Copy-paste example: examples/run-tag-release
  3. More examples: pipeline-compose/examples

Part of pipeline-compose. MIT License.

Top comments (0)