Skip to content

Latest commit

 

History

History
358 lines (273 loc) · 15.4 KB

File metadata and controls

358 lines (273 loc) · 15.4 KB

Exercise 3 — Release Pipeline: Dispatch, Reusable Workflows, Matrix & Composite Actions

TL;DR — In ~30 minutes you'll build a release pipeline that ties together five core Actions concepts: a composite action, a reusable workflow with a matrix, a release.yml triggered by workflow_dispatch and git tags, all wired up using third-party uses: actions to publish a GitHub Release.

Duration: ~30 minutes Prerequisites: Exercise 2 (ci.yml) merged to main. Goal: A release workflow you can fire manually with a version input (or by pushing a v* tag) that validates → builds → publishes a GitHub Release with attached artifacts.


What you'll build

┌──────────────────────────────────────────────────────────────────┐
│  release.yml          (workflow_dispatch + push tags 'v*')       │
│                                                                  │
│   ┌───────────────────┐     ┌───────────────┐    ┌────────────┐  │
│   │ validate (calls   │ ──▶ │ build         │──▶ │ publish    │  │
│   │ reusable-         │     │ (uses         │    │ (creates   │  │
│   │ validate.yml)     │     │ composite     │    │ GitHub     │  │
│   │  matrix: 3 OS ×   │     │ action)       │    │ Release)   │  │
│   │          3 Python │     │               │    │            │  │
│   └───────────────────┘     └───────────────┘    └────────────┘  │
└──────────────────────────────────────────────────────────────────┘

Files you'll create:

.github/
├── actions/
│   └── setup-python-project/
│       └── action.yml          ← composite action (Part A)
└── workflows/
    ├── reusable-validate.yml   ← callable workflow w/ matrix (Part B)
    └── release.yml             ← dispatch + tag trigger (Part C)

Setup

Bash / macOS / Linux / Git Bash:

git checkout main && git pull
BRANCH="feature/$(whoami)/release-workflow"
git checkout -b "$BRANCH"
mkdir -p .github/actions/setup-python-project
mkdir -p .github/workflows

Windows PowerShell:

git checkout main; git pull
$BRANCH = "feature/$env:USERNAME/release-workflow"
git checkout -b $BRANCH
New-Item -ItemType Directory -Force -Path .github/actions/setup-python-project | Out-Null
New-Item -ItemType Directory -Force -Path .github/workflows | Out-Null

💡 Keep the $BRANCH variable in the same shell session for the rest of the exercise — later commands (git push -u origin "$BRANCH", gh workflow run release.yml --ref "$BRANCH" ...) reference it. If you open a new terminal, re-run the BRANCH=... (bash) or $BRANCH = ... (PowerShell) line first.


Part A — Composite Action: setup-python-project (5 min)

A composite action bundles repeated steps into one reusable uses: block. We'll wrap "checkout + setup-python + cache + install deps" so every job in every workflow gets it for free.

Create .github/actions/setup-python-project/action.yml:

name: Setup Python Project
description: Checkout, install Python, restore pip cache, install requirements

inputs:
  python-version:
    description: Python version to install
    required: false
    default: "3.11"

# TODO ①: Wrap the steps below in a `runs:` block that declares this as a
#         composite action. (See "Composite Actions" in the guide.)

steps:
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4

  # TODO ②: Set up Python using inputs.python-version

  - name: Cache pip
    uses: actions/cache@v4
    with:
      path: ~/.cache/pip
      key: ${{ runner.os }}-py${{ inputs.python-version }}-${{ hashFiles('requirements.txt') }}
      restore-keys: ${{ runner.os }}-py${{ inputs.python-version }}-

  # TODO ③: Add a step that runs `pip install -r requirements.txt`

💡 Why composite actions? They live in your repo (no marketplace publish needed), are referenced as uses: ./.github/actions/setup-python-project, and DRY up workflows. Read more: reusable-workflows.md → "Composite actions".

💡 Part A — Hints & Solution

# TODO Concept Read this
runs.using: "composite" Composite action declaration reusable-workflows.md → "Composite Actions"
Use input in composite step ${{ inputs.<name> }} in actions reusable-workflows.md → composite example
shell: bash for run steps Composite action requirement reusable-workflows.md → composite example

→ Solution: solutions/03-release-workflow/.github/actions/setup-python-project/action.yml


Part B — Reusable Workflow with Matrix (10 min)

A reusable workflow is a full workflow file that other workflows can uses:. Unlike a composite action (which is a step), a reusable workflow is a job. Perfect for "run our full validation suite from anywhere."

Create .github/workflows/reusable-validate.yml:

name: Reusable Validate

# TODO ④: Make this workflow callable from other workflows.
#         It should accept a `python-versions` input (string, default '["3.11"]').

permissions:
  contents: read

jobs:
  validate:
    name: Validate (${{ matrix.os }} / py${{ matrix.python-version }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      # TODO ⑤: Build a matrix that fans out across:
      #   - os: ubuntu-latest, windows-latest, macos-latest
      #   - python-version: expanded from the `python-versions` input

    steps:
      # TODO ⑥a: Add `actions/checkout@v4` as the FIRST step.
      #          This is required so the runner has the local composite action
      #          (`./.github/actions/setup-python-project`) on disk before the
      #          next step tries to load its `action.yml`. Without checkout,
      #          you'll see: "Can't find 'action.yml' under
      #          '.github/actions/setup-python-project'."

      # TODO ⑥b: Use the composite action from Part A,
      #          passing the matrix Python version through.
      #          The composite does: setup-python + pip cache
      #          + `pip install -r requirements.txt`. After it runs,
      #          `pytest` and `ruff` are on PATH (provided they're listed
      #          in requirements.txt — see note below).

      - name: Lint
        run: |
          pip install ruff
          ruff check src/

      - name: Test
        run: pytest src/ --junitxml=test-results-${{ matrix.os }}-${{ matrix.python-version }}.xml

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results-${{ matrix.os }}-${{ matrix.python-version }}
          path: test-results-*.xml

⚠️ actions/checkout must precede a local composite action. Even if your composite includes a checkout step internally, the runner can't load the composite's action.yml until the repo is on disk. So the caller workflow (this one) must run actions/checkout first. This is the most common stumble with local composites.

⚠️ Make sure requirements.txt lists pytest (and ideally ruff). The composite installs from requirements.txt; if pytest isn't there, the Test step fails with pytest: command not found (exit code 127).

💡 Composite action vs reusable workflow:

Composite action Reusable workflow
Granularity A single step A whole job (or many)
Runs on Caller's runner Its own runner(s)
Can use matrix
Called via uses: ./path (in steps:) uses: ./path (under jobs.<id>.uses:)

💡 Part B — Hints & Solution

# TODO Concept Read this
on: workflow_call: with python-versions input Reusable workflow trigger + the exact input shape caching-and-matrix.md → workflow_call inputs block (copy this)
fromJSON() to expand matrix Dynamic matrix from input string caching-and-matrix.md → "Dynamic matrix from a workflow input"
⑥a actions/checkout@v4 first Required before any local uses: ./... core-concepts.md → "Run on Pushes & PRs" / first checkout step
⑥b Local action via uses: ./path Calling a composite action reusable-workflows.md → "Use the composite action"

→ Solution: solutions/03-release-workflow/.github/workflows/reusable-validate.yml


Part C — Release Workflow: Dispatch + Tags (15 min)

Now the headline workflow. It triggers two ways:

  1. Manually via workflow_dispatch with a version input — operator-driven release.
  2. Automatically when someone pushes a v* git tag — release-by-tagging.

Create .github/workflows/release.yml:

name: Release

on:
  push:
    tags:
      - "v*"
  # TODO ⑦: Add a `workflow_dispatch` trigger with two inputs:
  #   - version: required string (e.g., 'v1.2.3')
  #   - prerelease: boolean, default false

permissions:
  contents: write   # required for creating releases

jobs:
  # ── Job 1: Resolve version (from tag OR dispatch input) ─────────────
  resolve:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.v.outputs.version }}
    steps:
      - id: v
        run: |
          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
            echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
          else
            echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
          fi

  # ── Job 2: Validate via the reusable workflow ───────────────────────
  validate:
    needs: resolve
    # TODO ⑧: Call the reusable workflow from Part B,
    #         passing python-versions: '["3.10","3.11","3.12"]'.

  # ── Job 3: Build distribution ───────────────────────────────────────
  build:
    needs: [resolve, validate]
    runs-on: ubuntu-latest
    steps:
      # TODO ⑨: Use the composite action (default Python is fine).

      - name: Build wheel + sdist
        run: |
          pip install build
          python -m build

      - name: Upload dist artifact
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  # ── Job 4: Publish GitHub Release ───────────────────────────────────
  publish:
    needs: [resolve, build]
    runs-on: ubuntu-latest
    steps:
      - name: Download dist
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/

      # TODO ⑩: Create a GitHub Release using a third-party action
      #         (search the marketplace for one that publishes releases).
      #         It must:
      #           - tag the release with the resolved version
      #           - attach everything in `dist/` as release assets
      #           - auto-generate release notes
      #           - honor the `prerelease` dispatch input

💡 Part C — Hints & Solution

# TODO Concept Read this
workflow_dispatch inputs Manual triggers manual-triggers.md → "Inputs"
jobs.<id>.uses: Calling a reusable workflow reusable-workflows.md → "Call the reusable workflow"
Reusing the composite action DRY principle reusable-workflows.md → "Use the composite action"
Third-party action via version-pinned uses: Marketplace actions security-best-practices.md → Create GitHub Release step (copy this block)

→ Solution: solutions/03-release-workflow/.github/workflows/release.yml


Validate It

Run via dispatch (no tag needed)

git add .github/
git commit -m "ci: add release pipeline (dispatch + tags + reusable + matrix)"
git push -u origin "$BRANCH"
gh pr create --title "ci: add release workflow" --body "Adds release.yml with dispatch + reusable + composite."

# Merge the PR (via the GitHub UI, or with the CLI below):
gh pr merge --squash --delete-branch
git checkout main && git pull

# Now the workflow exists on the default branch — dispatch it:
gh workflow run release.yml -f version=v0.1.0 -f prerelease=true
gh run watch

Run via tag

git tag v0.1.1
git push origin v0.1.1

✅ Checklist

  • gh workflow run release.yml shows the version and prerelease inputs in the UI
  • validate job fans out to 9 matrix combinations (3 OS × 3 Python) — visible as separate jobs
  • build waits for all matrix combos
  • A new entry appears under the repo's Releases page with dist/* files attached
  • Auto-generated release notes list the commits since the previous tag

Bonus Challenges

  1. Concurrency — add concurrency: { group: release-${{ github.ref }}, cancel-in-progress: false } so two release runs can't race.
  2. SHA-pin everything — replace every @v4/@v2 in release.yml with full commit SHAs.
  3. Environment + approval — gate the publish job behind environment: production with required reviewers.
  4. Skip matrix on dispatch — accept a boolean quick input that, when true, narrows the reusable's matrix to ["3.11"] only. (Pass it through with with: python-versions: ${{ inputs.quick && '["3.11"]' || '["3.10","3.11","3.12"]' }}.)
  5. Reuse from another repo — move reusable-validate.yml to a shared .github org repo and call it as uses: my-org/.github/.github/workflows/reusable-validate.yml@main.

Concept Recap

Concept Where used in this exercise
Workflow file structure All three workflow files
workflow_dispatch with inputs release.yml Part C
Tag-triggered releases release.yml on: push: tags:
Matrix builds reusable-validate.yml (OS × Python)
Reusable workflows reusable-validate.yml + release.yml Job 2
Composite actions setup-python-project/action.yml
Third-party actions via uses: softprops/action-gh-release, actions/checkout, actions/setup-python, actions/cache, actions/upload-artifact, actions/download-artifact
GitHub Releases publish job

Solution Reference

Per-part solutions are linked under each part above. Full solution tree: solutions/03-release-workflow/.