| ← 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.
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.
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 useruns.using: 'composite'with a list ofsteps.
Inputs and outputs let the action communicate with the calling workflow, making the action flexible and reusable across different contexts.
Let's create a composite action that sets up Python, installs dependencies, and seeds the test database.
-
In your codespace, open a terminal window by selecting Ctl+`.
-
Create the directory for the action by executing the following command in the terminal:
mkdir -p .github/actions/setup-python-env
-
In the newly created
setup-python-envfolder, create a new file namedaction.ymlto store your composite action. -
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-outputstep's output, making the database path available to the calling workflow. - Each
runstep explicitly declaresshell: bashas required by composite actions.
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.
-
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 thatazd pipeline configused 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. - Name:
-
Return to your codespace and open
.github/workflows/run-tests.yml. In thetest-apijob, 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 }}
-
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 }}
-
The
test-e2ejob 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 (nopython-versionoverride 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 }}
-
Here's the complete updated
run-tests.ymlfor 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 }}
-
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 -
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.
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/coreand@actions/githubpackages. - 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.
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.
- Creating a composite action
- About custom actions
- Metadata syntax for GitHub Actions
- GitHub Skills: Reusable workflows
| ← Deploy to Azure | Next: Reusable Workflows → |
|---|