Skip to content

Latest commit

 

History

History
268 lines (202 loc) · 12 KB

File metadata and controls

268 lines (202 loc) · 12 KB

Creating Custom Actions

← Deploy to Azure Next: Reusable Workflows →

Custom actions let you encapsulate reusable logic into a single step you can use across workflows. GitHub Actions supports three types of custom actions: composite (combines multiple steps), JavaScript (runs Node.js code), and Docker container (runs in a container). Composite actions are the most approachable and a great starting point for bundling common step patterns.

In this exercise you'll create a composite action that sets up the Python environment and seeds the test database, then use it in your CI workflow.

Scenario

The pet shelter's test workflows need to seed the database before running tests. This involves setting up Python, installing dependencies, and running seed_database.py. Rather than duplicating these steps in every workflow, we'll create a custom composite action that any workflow can reference in a single step.

Background

The great advantage to a composite action is it builds upon the knowledge you already have. You've defined actions already, and a custom action uses a very similar syntax, all defined in YAML.

Every custom action is defined by an action.yml file. This file describes the action's interface and behavior:

  • name: A human-readable name for the action.
  • description: A short summary of what the action does.
  • inputs: Parameters the caller can pass to the action.
  • outputs: Values the action makes available to subsequent steps.
  • runs: Defines how the action executes. Composite actions use runs.using: 'composite' with a list of steps.

Inputs and outputs let the action communicate with the calling workflow, making the action flexible and reusable across different contexts.

Create the setup-python-env action

Let's create a composite action that sets up Python, installs dependencies, and seeds the test database.

  1. In your codespace, open a terminal window by selecting Ctl+`.

  2. Create the directory for the action by executing the following command in the terminal:

    mkdir -p .github/actions/setup-python-env
  3. In the newly created setup-python-env folder, create a new file named action.yml to store your composite action.

  4. Add the following YAML to the file to define your composite action:

    name: 'Setup Python Environment'
    description: 'Sets up Python, installs dependencies, and seeds the test database'
    
    inputs:
      python-version:
        description: 'Python version to use'
        required: false
        default: '3.14'
      database-path:
        description: 'Path to the test database file'
        required: false
        default: './test_dogshelter.db'
    
    outputs:
      database-file:
        description: 'Path to the seeded database file'
        value: ${{ steps.set-output.outputs.database-file }}
    
    runs:
      using: 'composite'
      steps:
        - name: Set up Python
          uses: actions/setup-python@v5
          with:
            python-version: ${{ inputs.python-version }}
    
        - name: Install dependencies
          run: pip install -r server/requirements.txt
          shell: bash
    
        - name: Seed the database
          id: seed
          run: python server/utils/seed_database.py
          shell: bash
          env:
            DATABASE_PATH: ${{ inputs.database-path }}
    
        - name: Set output
          run: echo "database-file=${{ inputs.database-path }}" >> $GITHUB_OUTPUT
          shell: bash
          id: set-output

Note

Composite action steps must include shell: bash for every run step — this is required even though it seems redundant. Without it, the workflow will fail with a validation error.

Review the key parts of the action:

  • Inputs provide sensible defaults so callers only need to override what's different.
  • Outputs reference the set-output step's output, making the database path available to the calling workflow.
  • Each run step explicitly declares shell: bash as required by composite actions.

Use the action in the CI workflow

Now let's update the CI workflow to use the custom action instead of the individual setup and install steps. We'll also store the test database path as a repository variable — configured once in your repository settings and available to every workflow.

  1. Navigate to your repository on GitHub and go to Settings > Secrets and variables > Actions > Variables tab. Select New repository variable and create:

    • Name: TEST_DATABASE_PATH
    • Value: ./test_dogshelter.db

    This is the same vars.* mechanism that azd pipeline config used in the deploy lesson for Azure credentials. Repository variables keep configuration out of your workflow files, making them easier to change without a code commit.

  2. Return to your codespace and open .github/workflows/run-tests.yml. In the test-api job, replace the Set up Python and Install dependencies steps (lines 23–32) with a single call to the composite action:

          - name: Setup Python environment
            id: seed
            uses: ./.github/actions/setup-python-env
            with:
              python-version: ${{ matrix.python-version }}
              database-path: ${{ vars.TEST_DATABASE_PATH }}
  3. Update the Run tests step in test-api (line 34) to pass the database path from the action's output:

          - name: Run tests
            run: python -m unittest test_app -v
            working-directory: ./server
            env:
              DATABASE_PATH: ${{ steps.seed.outputs.database-file }}
  4. The test-e2e job has the same Set up Python and Install Python dependencies steps — a perfect chance to reuse the action. Replace those two steps with the same composite action call (no python-version override needed since the action defaults to 3.14):

          - name: Setup Python environment
            id: seed
            uses: ./.github/actions/setup-python-env
            with:
              database-path: ${{ vars.TEST_DATABASE_PATH }}

    Then update the Run e2e tests step to pass the database path so the Flask server started by Playwright can find the seeded database:

          - name: Run e2e tests
            working-directory: ./client
            run: npx playwright test
            env:
              DATABASE_PATH: ${{ steps.seed.outputs.database-file }}
  5. Here's the complete updated run-tests.yml for reference:

    name: Run Tests
    
    on:
      push:
        branches: [main]
      pull_request:
        branches: [main]
    
    permissions:
      contents: read
    
    jobs:
      test-api:
        runs-on: ubuntu-latest
        strategy:
          fail-fast: false
          matrix:
            python-version: ['3.12', '3.13', '3.14']
    
        steps:
          - uses: actions/checkout@v4
    
          - name: Setup Python environment
            id: seed
            uses: ./.github/actions/setup-python-env
            with:
              python-version: ${{ matrix.python-version }}
              database-path: ${{ vars.TEST_DATABASE_PATH }}
    
          - name: Run tests
            run: python -m unittest test_app -v
            working-directory: ./server
            env:
              DATABASE_PATH: ${{ steps.seed.outputs.database-file }}
    
      test-e2e:
        runs-on: ubuntu-latest
    
        steps:
          - uses: actions/checkout@v4
    
          - name: Setup Python environment
            id: seed
            uses: ./.github/actions/setup-python-env
            with:
              database-path: ${{ vars.TEST_DATABASE_PATH }}
    
          - name: Set up Node.js
            uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'npm'
              cache-dependency-path: 'client/package-lock.json'
    
          - name: Install Node dependencies
            working-directory: ./client
            run: npm ci
    
          - name: Install Playwright browsers
            working-directory: ./client
            run: npx playwright install --with-deps chromium
    
          - name: Run e2e tests
            working-directory: ./client
            run: npx playwright test
            env:
              DATABASE_PATH: ${{ steps.seed.outputs.database-file }}
  6. In the terminal (Ctl+` to toggle), commit and push your changes:

    git add .github/actions/setup-python-env/action.yml .github/workflows/run-tests.yml
    git commit -m "Add setup-python-env composite action"
    git push
  7. Navigate to the Actions tab on GitHub and verify the workflow runs successfully with the new action.

Tip

When developing custom actions, you can test them by pushing to a branch and triggering a workflow run. Check the workflow logs to ensure each step in your composite action executes as expected.

Types of custom actions

GitHub Actions supports three types of custom actions, each suited to different use cases:

Type Best for Runs on Complexity
Composite Bundling multiple existing steps into one Directly on the runner Easiest to create
JavaScript Complex logic, API calls, or custom computations Node.js runtime Moderate
Docker container Actions that need specific tools or environments Inside a container Most involved
  • Composite actions are ideal when you want to combine several existing steps (like we did with setup, install, and seed) into a single reusable unit. They're the fastest to create because they use the same step syntax you already know.
  • JavaScript actions are best when you need custom logic, such as making API calls, processing data, or interacting with the GitHub API. They run on Node.js and have access to the @actions/core and @actions/github packages.
  • Docker container actions are best when your action requires specific tools, operating system libraries, or a particular runtime environment. They run in a Docker container, giving you full control over the execution environment.

Summary and next steps

Custom actions reduce duplication and make workflows cleaner. You've created a composite action that encapsulates Python setup and database seeding into a single reusable step. Any workflow in the repository can now prepare the Python environment with a single uses reference.

Next, we'll take reusability to the next level by exploring reusable workflows for sharing entire workflow patterns across your CI/CD pipeline.

Resources

← Deploy to Azure Next: Reusable Workflows →