Tag Tests and Add Manual CI/CD Triggers

Add test tags and a manual trigger to your Playwright CI/CD pipeline.

Introduction

30 Second Summary

When your test suite grows, running every single test on every change wastes time and slows your team down. Being able to pick and choose which tests to run from anywhere is a game-changer.

In this project, you will add tag-based filtering to your existing Playwright tests and create a manual trigger on your GitHub Actions workflow. After this, anyone on your team can run specific test groups directly from the browser.

What You'll Build

An enhanced Playwright CI/CD pipeline with tagged tests and a workflow_dispatch trigger that lets you run smoke tests, regression suites, or individual tests on demand from GitHub's Actions UI.

By the end of this project, you'll have:

  • Tests tagged with @smoke, @regression, and @slow using Playwright's built-in tag syntax.
  • Local npm scripts for running tagged groups without memorizing CLI flags.
  • A workflow_dispatch trigger with a choice dropdown and text input on your GitHub Actions workflow.
  • Secret Mission: A job summary that posts test results and the filter used directly on the workflow run page.

Are there any prerequisites?

This project builds on an existing Playwright project in TypeScript with tests and a GitHub Actions workflow that has 2 jobs. If you do not have this set up yet, complete the prerequisite Playwright project first.

Tag Your Existing Tests

As your Playwright test suite grows, running every test for every code change becomes slow and wasteful. You need a way to categorize tests so you can choose which ones to run depending on the situation.

Playwright supports test tags through a details object passed directly to test(). In this step, you'll tag your tests with @smoke and @regression categories so you can filter them from the command line later.

In this step, get ready to:
  • Tag critical tests with @smoke for fast feedback.
  • Tag thorough tests with @regression on an entire test group.
  • Use array syntax to assign multiple tags to a single test.
Tag your smoke tests

Smoke tests are your fastest, most critical tests. They verify that the core paths of your application still work after a change.

  • In VS Code, create a new file called example-smoke.spec.ts inside your tests folder.
  • Paste the following code into tests/example-smoke.spec.ts:
import { test, expect } from '@playwright/test';

test('homepage loads successfully', {
  tag: '@smoke',
}, async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveTitle(/./);
});

test('navigation links are visible', {
  tag: '@smoke',
}, async ({ page }) => {
  await page.goto('/');
  await expect(page.getByRole('navigation')).toBeVisible();
});

What does this code do?

Each test() call receives a details object { tag: '@smoke' } as its second argument. This attaches the @smoke tag as structured metadata on the test.

Tags must start with the @ symbol. Later, you'll use these tags to filter which tests run from the command line or from CI/CD.

  • Save tests/example-smoke.spec.ts.
  • Confirm the file exists by running:
ls tests/example-smoke.spec.ts
  • You should see tests/example-smoke.spec.ts in the output.

File not found?

Make sure you created the file inside the tests folder, not in the project root. The file name must end in .spec.ts for Playwright to pick it up.

Help me find where my test file should go.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

import { test, expect } from '@playwright/test';

test('homepage loads successfully', {
  tag: '@smoke',
}, async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveTitle(/./);
});

test('navigation links are visible', {
  tag: '@smoke',
}, async ({ page }) => {
  await page.goto('/');
  await expect(page.getByRole('navigation')).toBeVisible();
});
Tag a test group with @regression

Regression tests are more thorough and typically slower. Instead of tagging each test individually, you can tag an entire test.describe block so every test inside inherits the tag.

  • Create a new file called example-regression.spec.ts inside your tests folder.
  • Paste the following code into tests/example-regression.spec.ts:
import { test, expect } from '@playwright/test';

test.describe('form validation', {
  tag: '@regression',
}, () => {
  test('shows error for empty required field', async ({ page }) => {
    await page.goto('/');
    // Replace with your actual form interaction
    await expect(page).toHaveTitle(/./);
  });
});

What does this code do?

The details object { tag: '@regression' } is passed as the second argument to test.describe(). Every test inside this describe block automatically inherits the @regression tag.

This saves you from repeating the same tag on each individual test when they all belong to the same category.

  • Save tests/example-regression.spec.ts.

Seeing a syntax error?

Check that the details object { tag: '@regression' } sits between the describe name string and the arrow function. A missing comma after the closing brace is the most common mistake.

Help me fix a syntax error in my test.describe tag.

Add multiple tags to a single test

Some tests belong to more than one category. A test can be both a regression test and a slow test. Playwright supports this with array syntax.

  • In tests/example-regression.spec.ts, find the closing }); of the first test (after await expect(page).toHaveTitle(/./);).
  • Add the following test below it, still inside the describe block:
  test('submits form with valid data', {
    tag: ['@regression', '@slow'],
  }, async ({ page }) => {
    await page.goto('/');
    // Replace with your actual form submission test
    await expect(page).toHaveTitle(/./);
  });

What does the array syntax do?

The tag: ['@regression', '@slow'] array assigns two tags to this single test. You can later filter by either tag independently.

This test also inherits the @regression tag from the describe block. Playwright merges them, so this test is discoverable by any of its tags.

  • Save tests/example-regression.spec.ts.
  • Verify both test files exist by running:
ls tests/
  • You should see both example-smoke.spec.ts and example-regression.spec.ts listed alongside your existing test files.

Missing a file?

Double check that both files are saved inside the tests folder. If you accidentally saved them to the project root, move them into tests/ now.

Help me figure out where my test files ended up.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

import { test, expect } from '@playwright/test';

test.describe('form validation', {
  tag: '@regression',
}, () => {
  test('shows error for empty required field', async ({ page }) => {
    await page.goto('/');
    // Replace with your actual form interaction
    await expect(page).toHaveTitle(/./);
  });

  test('submits form with valid data', {
    tag: ['@regression', '@slow'],
  }, async ({ page }) => {
    await page.goto('/');
    // Replace with your actual form submission test
    await expect(page).toHaveTitle(/./);
  });
});

Your tests are now organized into clear categories. Next up, you'll learn how to run only the tests you want from the command line using Playwright's filtering options.

Run Tagged Tests Locally

Your tests are now tagged with @smoke, @regression, and @slow categories. The next question is: how do you actually run just one group instead of the entire suite?

Playwright's CLI has a --grep flag that filters tests by their tags. In this step, you'll use it to run specific tag groups from the terminal, then save those commands as npm scripts so your team never has to memorize the syntax.

In this step, get ready to:
  • Run filtered test groups using the --grep and --grep-invert flags.
  • Combine multiple tag groups with OR logic.
  • Add reusable npm scripts to package.json for quick filtered runs.
Run only smoke-tagged tests

The --grep flag tells Playwright to run only the tests whose tags match the pattern you provide. This is how you isolate your fastest, most critical tests for a quick sanity check.

  • Run only your smoke-tagged tests by running this command:
npx playwright test --grep @smoke

You should see output showing that only 2 tests ran (the two tests in example-smoke.spec.ts). The regression and slow tests are skipped entirely.

Seeing 0 tests found?

Make sure the @ symbol is included in the grep pattern. Tags must start with @ in Playwright. Also check that you saved your test files from the previous step.

Why aren't my tagged tests being found by --grep?

Exclude slow tests and combine tags

Sometimes you want to run everything except the slow tests. The --grep-invert flag does the opposite of --grep. It excludes tests matching the pattern.

  • Exclude all slow-tagged tests by running this command:
npx playwright test --grep-invert @slow

You should see 3 tests run. The "submits form with valid data" test is excluded because it carries the @slow tag.

What does grep-invert do?

Think of --grep as "only run these" and --grep-invert as "run everything except these." This is useful when you have a handful of slow tests you want to skip during development without removing them from the suite.

  • Run both smoke and regression tests together using OR logic by running this command:
npx playwright test --grep "@smoke|@regression"

You should see all 4 tests run. The pipe character | acts as OR, matching tests that have either tag.

Seeing a shell error with the pipe character?

Make sure the pattern is wrapped in double quotes: "@smoke|@regression". Without quotes, your shell interprets the | as a pipe operator between two separate commands.

Why is my grep OR pattern causing a shell error?

Add npm scripts for filtered runs

Remembering grep syntax every time you want a quick smoke run is tedious. Adding named scripts to package.json gives your team memorable shortcuts like npm run test:smoke.

  • In VS Code, open package.json from the Explorer sidebar.
  • Find the "scripts" section. It should already have a "test" entry.
  • Replace your entire "scripts" block with the following:
"scripts": {
  "test": "npx playwright test",
  "test:smoke": "npx playwright test --grep @smoke",
  "test:regression": "npx playwright test --grep @regression",
  "test:exclude-slow": "npx playwright test --grep-invert @slow"
}

What do these scripts do?

  • test runs the full suite with no filters.
  • test:smoke runs only your fastest, most critical tests.
  • test:regression runs thorough tests that cover edge cases.
  • test:exclude-slow runs everything except slow tests, perfect for quick development feedback.
  • Save package.json.
  • Verify your new scripts work by running this command:
npm run test:smoke

You should see only 2 smoke-tagged tests execute. This confirms your npm script correctly passes the --grep flag to Playwright.

npm run test:smoke not working?

Check for trailing commas in your JSON. The last script entry should not have a comma after its closing quote. Also make sure the "scripts" block is valid JSON by looking for red squiggly lines in VS Code.

Why is my npm script failing to run Playwright tests?

✔️ Awesome, I've got everything!

Great. Double check you've saved package.json before moving on.

ⓧ I'd like to double check the full code

Here is the complete scripts section for package.json:

{
  "scripts": {
    "test": "npx playwright test",
    "test:smoke": "npx playwright test --grep @smoke",
    "test:regression": "npx playwright test --grep @regression",
    "test:exclude-slow": "npx playwright test --grep-invert @slow"
  }
}

Your local workflow is now set up with quick shortcuts for running any test group. Next, you'll bring this filtering power to your CI/CD pipeline so anyone on your team can trigger targeted test runs from GitHub's UI.

Add workflow_dispatch with Inputs to Your Workflow

In the last step, you set up npm scripts so your team can run filtered tests locally without memorizing --grep syntax. Now it's time to bring that same power to your CI/CD pipeline.

GitHub Actions supports a trigger called workflow_dispatch that adds a "Run workflow" button to your Actions tab. You can attach inputs to this trigger so anyone on your team can pick a tag group from a dropdown or type a specific test name, all without touching code.

In this step, get ready to:
  • Add a workflow_dispatch trigger with choice and string inputs to your workflow file.
  • Update the test run step with conditional --grep logic.
  • Push your changes and verify the manual trigger appears in GitHub.
Add the workflow_dispatch trigger with inputs

The workflow_dispatch trigger lets you define inputs that appear as form fields in the GitHub UI. You will add two inputs: a dropdown for selecting a tag group and a free-text field for targeting a specific test by name.

  • In VS Code, open .github/workflows/playwright.yml.
  • Find the on: block at the top of your file. It currently has push and pull_request triggers.
  • Add the following workflow_dispatch block below the existing pull_request trigger:
  workflow_dispatch:
    inputs:
      test_tag:
        description: 'Tag group to run'
        required: true
        default: 'all'
        type: choice
        options:
          - all
          - '@smoke'
          - '@regression'
      specific_test:
        description: 'Specific test title (overrides tag if provided)'
        required: false
        type: string

What do these inputs do?

The test_tag input uses type: choice, which renders as a dropdown in the GitHub UI. The learner picks from a fixed list of options: all, @smoke, or @regression. This prevents typos.

The specific_test input uses type: string, which renders as a free-text field. This allows someone to type in an exact test title to run just one test. When provided, it overrides the tag selection.

Why is default set to 'all'?

Setting default: 'all' means that clicking "Run workflow" without changing anything runs all tests. This is the safest default because it matches existing behavior.

Update the test run step with conditional grep logic

Your workflow needs logic to decide which tests to run based on the inputs. A bash if/elif/else block checks whether a specific test was requested, whether a tag was selected, or whether to run everything.

  • Find the step named "Run Playwright tests" in your workflow file.
  • Replace the existing run: command with the following conditional logic:
      - name: Run Playwright tests
        run: |
          if [ -n "${{ inputs.specific_test }}" ]; then
            npx playwright test --grep "${{ inputs.specific_test }}"
          elif [ "${{ inputs.test_tag }}" != "all" ] && [ -n "${{ inputs.test_tag }}" ]; then
            npx playwright test --grep "${{ inputs.test_tag }}"
          else
            npx playwright test
          fi

What does this conditional logic do?

  • Line 1: If specific_test is not empty (-n tests for non-empty string), use it as the grep pattern. This takes priority over everything.
  • Line 2: Otherwise, if test_tag is not "all" and is not empty, use the tag as the grep pattern.
  • Line 3: Otherwise, run all tests with no filter. This is the fallback.
  • Make sure your complete on: block now includes all three triggers: push, pull_request, and workflow_dispatch.
  • Save .github/workflows/playwright.yml.

YAML indentation errors?

YAML is whitespace-sensitive. The workflow_dispatch block must be indented at the same level as push and pull_request (two spaces in from on:). Check that the inputs block is indented four spaces from the left edge.

help me fix YAML indentation in my workflow_dispatch trigger

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete .github/workflows/playwright.yml file. Compare yours against it:

name: Playwright Tests

on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]
  workflow_dispatch:
    inputs:
      test_tag:
        description: 'Tag group to run'
        required: true
        default: 'all'
        type: choice
        options:
          - all
          - '@smoke'
          - '@regression'
      specific_test:
        description: 'Specific test title (overrides tag if provided)'
        required: false
        type: string

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      - name: Run Playwright tests
        run: |
          if [ -n "${{ inputs.specific_test }}" ]; then
            npx playwright test --grep "${{ inputs.specific_test }}"
          elif [ "${{ inputs.test_tag }}" != "all" ] && [ -n "${{ inputs.test_tag }}" ]; then
            npx playwright test --grep "${{ inputs.test_tag }}"
          else
            npx playwright test
          fi
      - uses: actions/upload-artifact@v5
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
Push your changes and verify the Run workflow button

The workflow_dispatch trigger only activates when the workflow file is on the default branch. You need to push your changes to main before the "Run workflow" button appears.

  • Commit and push your workflow file by running these commands:
git add .github/workflows/playwright.yml
git commit -m "Add workflow_dispatch trigger with inputs"
git push origin main
  • Open your repository on GitHub in your browser.
  • Click the Actions tab at the top of your repository page.
  • In the left sidebar, click the name of your workflow (Playwright Tests).
  • Look for the Run workflow button in the upper area of the workflow page.
  • Click Run workflow to expand the form. You should see a dropdown for Tag group to run and a text field for Specific test title (overrides tag if provided).

Why does it need to be on the default branch?

GitHub only reads workflow_dispatch triggers from the workflow file on your repository's default branch. If you push to a feature branch first, the "Run workflow" button will not appear until you merge to main.

Don't see the Run workflow button?

Make sure you pushed to your default branch (main or master). Check under Settings > General to confirm which branch is set as default. Also verify you selected the correct workflow name in the left sidebar of the Actions tab.

help me troubleshoot why the Run workflow button is missing in GitHub Actions

Your workflow now has a manual trigger with interactive inputs. Next up, you will actually use this trigger to run tagged tests from the GitHub UI and verify that the filtering works end to end.

Test the Manual Trigger from GitHub

Your workflow file is pushed and the manual trigger is live. Now it is time to prove it works by firing off targeted runs from GitHub Actions and checking the results.

You will trigger two manual runs: one using the tag dropdown, and one using the free-text field for a specific test title. By reviewing the logs, you will confirm that the conditional grep logic routes to the correct tests each time.

In this step, get ready to:
  • Trigger a smoke-only run using the tag dropdown.
  • Trigger a single-test run using the specific test title field.
  • Review workflow logs to confirm the correct grep filter was applied.
Trigger a smoke test run from the dropdown

The workflow_dispatch trigger gives you a Run workflow button directly in the GitHub Actions UI. Your first test is to select @smoke from the dropdown and verify only your smoke-tagged tests execute.

  • Switch back to your repository on GitHub in your browser.
  • Click the Actions tab at the top of your repository.
  • In the left sidebar, click Playwright Tests to select your workflow.
  • Click the Run workflow dropdown button on the right side.
  • In the Tag group to run dropdown, select @smoke.
  • Leave the Specific test title field empty.
  • Click the green Run workflow button.

A new workflow run appears in the list with a yellow spinner. Wait for it to complete (this typically takes 1-2 minutes while browsers install in CI).

  • Once the run finishes with a green checkmark, click on the run name to open it.
  • Click the test job in the left sidebar.
  • Expand the Run Playwright tests step.

You should see that only your two smoke-tagged tests ran: homepage loads successfully and navigation links are visible. The regression and slow tests were excluded.

What should I see in the logs?

The log output for the Run Playwright tests step shows the command that was executed. You should see npx playwright test --grep "@smoke" at the top, followed by output showing only 2 tests passed.

Run failed or all tests ran?

If you see all tests running instead of just smoke tests, double-check that your workflow file on the main branch has the conditional if logic in the Run Playwright tests step. The workflow_dispatch trigger only reads inputs from the workflow file on the default branch.

If the run fails entirely, expand the Install Playwright Browsers step to check for errors there. Browser install occasionally times out on the first run.

Help me debug my manual workflow trigger

Trigger a run with a specific test title

The tag dropdown covers groups of tests, but sometimes you need to run just one specific test. The specific_test input field handles that case by overriding the tag selection entirely.

  • Click the Run workflow dropdown button again.
  • Leave the Tag group to run dropdown set to all.
  • In the Specific test title field, type homepage loads successfully.
  • Click the green Run workflow button.

Wait for the run to complete. Once it shows a green checkmark, click into it and expand the Run Playwright tests step.

You should see only one test executed: homepage loads successfully. The grep pattern matched the test title exactly, so every other test was excluded.

Why does specific_test override the tag?

Your workflow's shell script checks specific_test first with the if [ -n ... ] condition. If it is non-empty, the script uses that value for --grep and never reaches the elif branch that checks the tag. This priority logic means a specific test name always wins.

Test not found or no tests ran?

If the run shows 0 tests, the grep pattern did not match any test title. Check that you typed the title exactly as it appears in your test file (case-sensitive). The --grep flag uses a regex match, so partial titles work too.

Help me figure out why my specific test is not being found

Review the workflow run logs

Now that both runs are complete, confirm the correct filtering happened by reviewing the command that was executed in each run.

  • Return to the Actions tab and find your first run (the @smoke one).
  • Click into the run and expand the Run Playwright tests step.
  • Confirm the log shows npx playwright test --grep "@smoke" as the executed command.
  • Go back and open the second run (the specific test title one).
  • Expand the Run Playwright tests step and confirm it shows npx playwright test --grep "homepage loads successfully".

What are these logs proving?

By checking the exact command in the logs, you are verifying that your shell script's conditional logic correctly translated the UI inputs into the right --grep arguments. This is the same verification you would do in a real CI/CD pipeline to confirm filters are applied correctly before trusting the results.

Your manual trigger is fully working. Anyone on your team can now go to the Actions tab and run exactly the tests they need without editing code or pushing commits. Next up, you will take this further with a job summary that posts results directly on the workflow run page.

Secret mission

Add a Job Summary with Test Results

Your workflow runs tests, but after the run finishes, you have to dig through logs to see what happened. What if the results appeared right on the workflow run page with a clean markdown summary showing which filter was used and whether tests passed or failed?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses GitHub Actions, which is free for public repositories. There are no ongoing costs to worry about.

Resources you used:

  • Test files: tests/example-smoke.spec.ts and tests/example-regression.spec.ts.
  • npm scripts added to package.json (test:smoke, test:regression, test:exclude-slow).
  • GitHub Actions workflow changes in .github/workflows/playwright.yml (workflow_dispatch trigger, job summary, PR comment steps).
  • GitHub Actions workflow run artifacts (auto-expire after 30 days).

Keep everything running

No action needed. Choose this if you want to keep your tagged tests and manual workflow trigger active. Your workflow will continue to run on pushes and pull requests as before, with the added ability to trigger manual runs from the Actions tab.

  • Your tagged test files stay in your repository.
  • The workflow_dispatch trigger remains available in your Actions tab.
  • Workflow run artifacts auto-delete after 30 days based on the retention-days: 30 setting.

Pause - I'll come back to this later

Since this project is all local files and a GitHub workflow, there is nothing to pause. Your workflow only runs when triggered (by a push, PR, or manual dispatch). It does not consume resources while idle.

  • If you want to temporarily disable the workflow, go to your repository's Actions tab, click on Playwright Tests, then click the ... menu and select Disable workflow.
  • You can re-enable it from the same menu when you return.

Delete - I don't want to use this again

Remove the test files, revert your workflow changes, and clean up the npm scripts you added.

  • Delete the tagged test files by running this in your project folder:
rm tests/example-smoke.spec.ts tests/example-regression.spec.ts
  • Open package.json and remove the test:smoke, test:regression, and test:exclude-slow scripts you added.
  • Open .github/workflows/playwright.yml and remove the workflow_dispatch trigger block, the conditional grep logic in the Run Playwright tests step, the Write job summary step, and the Comment on PR if tests fail step.
  • Revert the Run Playwright tests step back to a simple npx playwright test command without the id or conditional logic.
  • Commit and push the reverted changes to remove the workflow_dispatch trigger from your Actions tab.

Nice Work!

Nice Work!

You just leveled up your Playwright project with tagged tests and a manual CI/CD trigger. Anyone on your team can now run targeted test groups directly from GitHub's UI without touching code or a terminal.

You've learned how to:

  • Tag tests with @smoke, @regression, and @slow categories using Playwright's built-in tag syntax on individual tests and describe groups.
  • Filter test runs locally with --grep and --grep-invert CLI flags, and create npm scripts so your team never has to remember grep syntax.
  • Add a workflow_dispatch trigger to your GitHub Actions workflow with a choice dropdown and free-text input for on-demand targeted test execution from the Actions UI.
  • Build a job summary that posts test results and the filter used directly on the workflow run page, plus automatic PR failure comments.

Ready to quiz yourself?