Browse the Knowledge Hub211 resources
GitHub Actions review: pricing, alternatives and setup
GitHub Actions is the CI/CD service built into GitHub. Workflows are YAML files in the repository, triggered by events such as pushes, pull requests and schedules, and run on GitHub-hosted Linux, Windows and macOS runners or on self-hosted machines. For test automation it offers matrix jobs, artefacts and caching, while test reporting and retries come from the test tools and marketplace actions.
QAble runs this tool on client engagements. The assessment below is from that experience.
- last reviewed
- 13 October 2026
- version covered
- GitHub Actions (runner 2.338.0)
- licence
- Freemium
At a glance
GitHub Actions at a glance
- Vendor
- GitHub / Microsoft
- Category
- CI/CD
- Licence
- Freemium
- Platforms
- Cloud, Self-hosted runners
- Languages
- YAML
- Integrates with
- Docker, Playwright, Selenium, SonarQube
- First released
- 2019
- Version reviewed
- GitHub Actions (runner 2.338.0)
Verdict
Our verdict on GitHub Actions
Strengths
- Workflows live in the repository and trigger on pull requests with no server, webhook or plugin to configure
- A matrix expresses browsers, operating systems or test shards in a few lines, up to 256 jobs per run
- Hosted Linux, Windows and macOS runners, so Safari and iOS work is possible without owning Macs
- Free for public repositories on standard runners, and free on self-hosted runners for any repository
- Artefacts, dependency caching and job summaries are built in and need no extra service
- A large marketplace of actions for browsers, reporting, cloud logins and notifications
Weaknesses
- No built-in test results view or per-test history; a marketplace action or external service provides it
- No retry keyword: flaky tests are retried by the test framework or by re-running failed jobs
- Matrix fail-fast is on by default, so one failing shard cancels the rest unless you turn it off
- Fork pull requests get a read-only token, so check-run reports need a second workflow
- macOS runners cost about ten times Linux per minute at list price
Best for
Any team whose code is already on GitHub and whose tests can run on a clean virtual machine. Workflows sit beside the code, pull requests show results without webhooks or plugins, and a matrix expresses browsers, shards or Java versions in a few lines. It is the default we reach for on Playwright, Cypress and Java Selenium suites that test a deployed environment or start the application inside the job. Public repositories run on standard runners at no cost, which makes it the obvious choice for open source test tooling.
Not for
Organisations whose code is not on GitHub, and suites that must reach systems inside a private network or a physical device lab, where self-hosted runners bring back the server maintenance you were avoiding. It is also a weaker fit for teams that want rich test analytics in the CI tool itself: Actions has no built-in test results tab, no per-test history and no flaky test detection, so those come from a marketplace action or a separate reporting service.
Setup cost
Very low. One YAML file under .github/workflows runs on the next push, and setup-java or setup-node with caching turned on handles most dependency time. The decisions worth making on day one are the permissions block, a timeout on every job, which reporting action to use, and naming artefacts per matrix job. A sharded browser suite with a merged report is an afternoon’s work.
Maintenance burden
Low. There is no infrastructure to patch on hosted runners. The ongoing work is keeping action versions current, since major versions of checkout, setup-java and the artefact actions move every year or so and old ones are retired; pinning third-party actions to commit SHAs and updating them deliberately; and stopping workflow files from drifting apart across repositories, which reusable workflows solve. Self-hosted runners add operating system, browser and runner updates.
Honest limitations
No test report UI, no test-level retry keyword and no flaky detection out of the box. Hosted jobs stop after 6 hours. Debugging a failing workflow means pushing and waiting. Free plans run 20 concurrent standard jobs and 5 macOS jobs. macOS minutes cost about ten times Linux at list price. Third-party actions are code from strangers running with your token, so permissions and pinning matter. And workflows triggered by forks get a read-only token, which breaks naive reporting set-ups.
How it works
How GitHub Actions works
Events, workflows, jobs and steps
A workflow is a YAML file in .github/workflows. It names the events that start it, such as push, pull_request, schedule or a manual workflow_dispatch, and contains jobs. Jobs run in parallel by default, each on its own runner, unless needs makes one wait for another. Inside a job, steps run in order on the same machine and share its file system, so a build step’s output is available to the test step that follows. Between jobs nothing is shared unless you pass it explicitly as an artefact, a cache entry or an output. That separation is what makes parallel test jobs easy and passing results between them deliberate.
Runners are disposable machines
A GitHub-hosted runner is a fresh virtual machine for each job, with Ubuntu, Windows or macOS images that come with common tools and browsers installed, and it is discarded afterwards. Nothing leaks between runs, which removes a whole class of flaky test causes, but every job starts cold, so dependency caching matters. Hosted jobs can run for up to 6 hours. Self-hosted runners are machines you register yourself, inside your network if needed, where jobs can run for up to 5 days. Concurrency is capped by plan: 20 standard jobs at once on Free, 60 on Team and 500 on Enterprise, with lower caps for macOS.
Actions are versioned code from repositories
A step either runs a shell command or uses an action, referenced as owner/repo@ref. Actions are JavaScript, Docker or composite steps published in ordinary repositories, so actions/checkout@v7 resolves to whatever the v7 tag points at today. That is convenient and also a supply chain risk, because a tag can be moved. GitHub’s own actions follow major versions that change behaviour: checkout v7 now refuses to check out fork pull request code under pull_request_target unless you opt in, and v3 of the artefact actions stopped working on 30 January 2025. Pinning to a full commit SHA, with the version in a comment, is the safe default for third-party actions.
Matrix jobs and fail-fast
strategy.matrix generates one job per combination of the values you list, up to 256 jobs per workflow run, and include or exclude adjust the combinations. This is how a suite fans out across browsers, Java versions or shard numbers. Two defaults matter for testing. fail-fast is true, so the first failing job cancels every queued and running sibling, which is right for a quick pull request gate and wrong for a regression run where you want every failure. max-parallel limits how many matrix jobs run at once, which protects a shared test environment from being flooded by its own pipeline.
Strengths
What it does well
Test reports through check runs and summaries
Actions does not parse test results itself, but job summaries accept Markdown written to GITHUB_STEP_SUMMARY, and marketplace actions turn JUnit XML into a check run with annotations on the failing lines. mikepenz/action-junit-report, for example, reads Surefire, Gradle or any JUnit XML, adds a check to the pull request, and exposes total, failed and retried counts as outputs for later steps.
Artefacts for evidence
upload-artifact v7 stores traces, screenshots, videos and HTML reports with the run, with a retention period you can shorten per upload, a compression level and an option to upload a single file without zipping it. Artefacts are immutable and names must be unique within a run, so matrix jobs add the shard or browser to the name. download-artifact can then merge them in a later job.
Dependency caching built into the setup actions
setup-java and setup-node accept a cache input that stores the Maven, Gradle or npm cache keyed on the lock or build files. On a cold hosted runner that is often the largest saving available, and for matrix fan-out the cache can be made read-only on branches so only main writes it.
Service containers for test dependencies
A job can declare services, such as PostgreSQL, Redis or a mock server, that start as containers beside the job and are reachable on localhost or by name. API and integration suites get a clean database for every run without shared test environments, which removes test data collisions between pull requests.
Reusable workflows for consistent pipelines
A workflow triggered by workflow_call can be called from other workflows and repositories with inputs and secrets. A QA team can own one workflow that sets up browsers, runs a sharded suite and publishes the report, and product repositories call it in a few lines. Fixing the reporting step once fixes it everywhere.
Cancelling superseded runs
A concurrency group with cancel-in-progress: true cancels a running workflow when a newer commit arrives on the same branch. For long browser suites on busy pull requests this saves more minutes than any tuning of the tests, and it keeps queues short for everyone else.
Pricing
GitHub Actions pricing and licensing
GitHub Actions is free on standard GitHub-hosted runners for public repositories, and free on self-hosted runners for any repository. Private repositories get a monthly allowance of minutes and storage with each GitHub plan, and usage beyond it is billed per minute at a rate set by the runner’s operating system and size. Without a payment method, jobs are blocked once the allowance runs out. Larger runners are always billed, even in public repositories.
- Included minutes per month for private repositories: 2,000 on GitHub Free, 3,000 on Pro and Team, and 50,000 on Enterprise Cloud, per GitHub’s billing documentation on 13 October 2026.
- List prices per minute on 13 October 2026: Linux 2-core $0.006, Windows 2-core $0.010 and macOS $0.062, with larger runners priced by core count. Partial minutes round up per job.
- Artefact storage is included at 500 MB on Free, 1 GB on Pro, 2 GB on Team and 50 GB on Enterprise Cloud, shared with GitHub Packages and billed hourly beyond that. Cache storage is a separate 10 GB per repository.
- Self-hosted runners cost nothing in Actions minutes, but you pay for the machines, their upkeep and their security.
- How cost scales: by minutes multiplied by the runner rate. A sharded suite costs roughly the same minutes as an unsharded one, just sooner; macOS jobs and retries are what inflate bills.
Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.
Getting started
GitHub Actions tutorial: getting started
Setup, step by step
- Make sure the suite runs headless from one command, such as mvn verify or npx playwright test.
- Create .github/workflows/tests.yml with checkout, the language setup action and caching.
- Set a permissions block and a timeout-minutes on the job before the first run.
- Add a reporting step that runs even when tests fail, and upload screenshots or traces only on failure.
- Push a branch, open a pull request and check the run, the check run and the artefacts.
- Break a test on purpose to confirm the report and artefacts appear where reviewers will look.
Install
# No install: commit a workflow file and push
mkdir -p .github/workflows
# add tests.yml, then
git add .github/workflows/tests.yml && git commit -m "Add test workflow" && git pushA first example
# .github/workflows/tests.yml
name: tests
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
checks: write # lets the report action create a check run
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30 # the default is 360
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '21'
cache: maven
- name: Run tests
run: mvn -B verify
- name: Publish test report
if: ${{ !cancelled() }} # run even when tests failed
uses: mikepenz/action-junit-report@v6
with:
report_paths: '**/target/*-reports/TEST-*.xml'
- name: Keep screenshots
if: failure()
uses: actions/upload-artifact@v7
with:
name: screenshots
path: target/screenshots
if-no-files-found: ignoreWhat trips people up first
Reports that never appear. Steps run only while the job is succeeding, so when the test step fails, every later step is skipped, including the one that publishes results, and reviewers see a red cross with a log to scroll. Add if: ${{ !cancelled() }} or if: always() to the reporting and upload steps. The second surprise follows on pull requests from forks: the token is read-only and secrets are withheld, so an action that creates check runs fails there. Use annotate-only mode for forks, or upload the XML as an artefact and publish it from a workflow_run workflow on the default branch.
In practice
Using GitHub Actions in practice
Laying out workflows for a test suite
Split by purpose rather than putting everything in one file. A pull request workflow runs fast checks and a smoke subset with fail-fast on and a concurrency group that cancels superseded runs. A main branch or scheduled workflow runs the full regression across browsers with fail-fast off. A manual workflow_dispatch workflow takes inputs such as environment and tag filter so testers can run a targeted suite against staging without editing YAML. Shared setup belongs in a composite action or a reusable workflow in one place. Keep environment URLs and credentials in GitHub environments and secrets, and give each workflow the narrowest permissions block it needs.
Sharding a browser suite and merging the report
Test runners with a shard option, such as Playwright, split cleanly across a matrix: each job runs one shard and uploads a partial report named after its shard, and a final job downloads them all and merges. fail-fast is off so every shard finishes, the merge job runs unless the workflow was cancelled, and the partial artefacts get a one-day retention because only the merged report matters. The same shape works for any runner that accepts a shard index.
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false # let every shard finish
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: lts/*
- run: npm ci
- run: npx playwright install --with-deps
# playwright.config sets reporter: 'blob' when CI is set
- run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: blob-report-${{ matrix.shardIndex }} # names must be unique
path: blob-report
retention-days: 1
merge-reports:
if: ${{ !cancelled() }}
needs: [e2e]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: lts/*
- run: npm ci
- uses: actions/download-artifact@v8
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- run: npx playwright merge-reports --reporter html ./all-blob-reports
- uses: actions/upload-artifact@v7
with:
name: html-report
path: playwright-reportReporting Java suites on the pull request
Surefire, Failsafe and Gradle all write JUnit XML, so one reporting action covers every JVM project. Point it at the report directories, run it with !cancelled(), and grant checks: write so it can create a check run. Failures then appear as annotations on the lines that failed and in the pull request’s checks list, which reviewers read far more often than logs. If Surefire’s rerunFailingTestsCount is set, a test that passes on a rerun is written to the XML with flakyFailure entries rather than as a plain pass, so the evidence of flakiness survives in the artefact even when the build goes green. Upload the raw XML too, so a reporting service can track it over time.
- name: Run API tests, rerunning failures twice
run: mvn -B -Dsurefire.rerunFailingTestsCount=2 verify
- name: Publish results
if: ${{ !cancelled() }}
uses: mikepenz/action-junit-report@v6
with:
report_paths: '**/target/surefire-reports/TEST-*.xml'
check_name: api-tests
- name: Keep raw results for trend reporting
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: api-test-xml
path: '**/target/surefire-reports/*.xml'Flaky tests, reruns and debugging
Actions has no retry keyword for steps or jobs, so retries belong in the test runner: Playwright and Cypress retries, Surefire’s rerunFailingTestsCount or Gradle’s test-retry plugin. Those record the retry, which a re-run does not. The Re-run failed jobs button is for infrastructure failures, and it works for up to 30 days after the original run; enabling debug logging on the re-run adds runner diagnostic and step debug output. Avoid a culture of re-running until green: track flaky tests in the report, quarantine them with a tag that a separate non-blocking job runs, and fix or delete them on a schedule. Upload traces or videos on failure so most flakes can be diagnosed without a re-run at all.
Security and self-hosted runners
Test workflows often hold credentials for staging environments, so treat them as production code. Set top-level permissions to read and grant writes per job. Pin third-party actions to a full commit SHA. Avoid pull_request_target with fork code: checkout v7 refuses it by default for this reason. When tests must reach a private network or a device lab, use self-hosted runners, but keep them off public repositories, where anyone can open a pull request that runs code on them, and prefer ephemeral runners that are destroyed after one job, so one test run cannot leave state or credentials for the next.
Pitfalls
Common pitfalls
One failing shard cancels the whole matrix
Why it happens: strategy.fail-fast defaults to true, so the first failure cancels every queued and running job in the matrix. Regression runs report one failure and hide the rest.
The fix: Set fail-fast: false on regression and sharded jobs. Keep it on only where the first failure is all you need, such as a quick pull request gate.
Artefact upload conflicts in matrix jobs
Why it happens: Since v4, artefacts are immutable and names must be unique within a run. Matrix jobs that all upload test-results collide, and every job after the first fails at the upload step.
The fix: Include the matrix values in the name, such as results-${{ matrix.browser }}, then download with a pattern and merge-multiple in a later job.
Reports and screenshots skipped on failure
Why it happens: Later steps run only while the job succeeds, so a failing test step skips the report and upload steps that follow it.
The fix: Use if: ${{ !cancelled() }} for reports and if: failure() for failure-only evidence such as screenshots and traces.
Copying workflows from old tutorials
Why it happens: Older examples use actions/upload-artifact@v3, which stopped working on 30 January 2025, and action majors written for Node 20, which GitHub removed from runners on 23 September 2026. JavaScript actions now run on Node 24 whatever they declare.
The fix: Use current majors: checkout v7, setup-java v6, setup-node v7, upload-artifact v7, download-artifact v8. Let Dependabot keep action versions current.
Jobs hanging for six hours
Why it happens: timeout-minutes defaults to 360. A stuck browser or a test waiting on a dead environment burns paid minutes until the limit and blocks a concurrency slot.
The fix: Set timeout-minutes on every test job, a little above its normal duration, and set test-level timeouts in the runner too.
Check-run reports failing on fork pull requests
Why it happens: Workflows triggered from forks get a read-only GITHUB_TOKEN and no secrets, so actions that create check runs or comment on the pull request are refused.
The fix: Use the reporter’s annotate-only mode, or upload results as an artefact and publish them from a workflow_run workflow on the default branch.
Compared
GitHub Actions alternatives compared
| Tool | Hosting | Free allowance | Built-in test results view | Retry options | Parallel test runs | macOS runners |
|---|---|---|---|---|---|---|
| GitHub Actions | GitHub-hosted or self-hosted runners | 2,000 minutes a month on Free | No; marketplace actions add check runs | Re-run failed jobs; no retry keyword | Matrix, up to 256 jobs per run | Hosted, billed per minute |
| GitLab CI/CD | GitLab.com, self-managed or Dedicated | 400 compute minutes a month on Free | Yes, in merge requests and pipelines | retry: up to 2, by failure type | parallel: 1 to 200, plus matrix | Hosted, beta, Premium and Ultimate |
| Jenkins | Self-hosted only | All free; you pay for servers | Yes, with history and trends | retry step, with agent conditions | Matrix, parallel stages, timing-based splitting | Your own Mac agents |
| Azure DevOps | Microsoft-hosted or self-hosted agents | One hosted job, 1,800 minutes, enable first | Yes, plus flaky test management | Flaky detection with VSTest reruns | parallel strategy, up to 99 agents | Microsoft-hosted macOS images |
Head-to-head comparisons
Jenkins vs GitHub ActionsFAQ
Frequently asked questions
What are the best GitHub Actions alternatives?
The alternatives most often weighed against GitHub Actions are GitLab CI/CD, Jenkins, Azure DevOps and CircleCI. The comparison on this page sets them side by side on hosting, free allowance, built-in test results view, retry options, parallel test runs and macos runners, and the verdict above says where GitHub Actions is the wrong choice and which of them to pick instead.
Is GitHub Actions free?
Free for public repositories on standard GitHub-hosted runners, and free on self-hosted runners for any repository. Private repositories get a monthly allowance with the account’s plan, 2,000 minutes on GitHub Free and 3,000 on Pro and Team, plus artefact storage. Beyond that, minutes are billed per minute at a rate that depends on the runner: at list price Windows costs more than Linux and macOS about ten times Linux. Larger runners are always billed.
GitHub Actions or Jenkins for test automation?
If the code is on GitHub and the tests can run on a clean hosted machine, Actions is less work: no server to run, results on the pull request, and matrix sharding in a few lines. Jenkins is the better fit when builds must stay inside a private network, when you want per-test history in the CI tool without extra services, or when an existing Jenkins estate already does the job well.
How do I run tests in parallel in GitHub Actions?
Use a matrix. Give each job a shard number and pass it to a runner that can split itself, such as Playwright’s --shard option, or split spec files yourself for runners that cannot. Set fail-fast: false so all shards finish, name artefacts per shard, and merge the reports in a final job that needs the matrix. Within each job, the test runner’s own workers use the runner’s cores.
Can GitHub Actions show test results like Jenkins or GitLab?
Not on its own. Actions shows logs, annotations and job summaries, but has no test results tab or per-test history. Marketplace actions such as mikepenz/action-junit-report or dorny/test-reporter parse JUnit XML and publish a check run with failures annotated on the code, and can write a summary. For trends across runs, use a reporting service or a test management tool that ingests the XML.
How do I retry flaky tests in GitHub Actions?
In the test runner, not the workflow. There is no retry keyword for steps or jobs. Configure retries in Playwright, Cypress, Surefire with rerunFailingTestsCount or Gradle’s test-retry plugin, which record which tests passed only on retry. Re-run failed jobs, available for 30 days after a run, is for infrastructure failures. Track the flaky list and fix it, since retries hide problems as well as noise.
Can GitHub Actions run Safari or iOS tests?
Yes, on macOS runners, which include Xcode and Safari. Standard macOS runners are available on every plan, but their per-minute list price is roughly ten times Linux and concurrency is capped at 5 macOS jobs on Free, Pro and Team. Keep macOS jobs to the tests that genuinely need Apple platforms, and run everything else on Linux.
Why did my workflow stop working after copying an old example?
Usually because it uses retired action versions. actions/upload-artifact and download-artifact v3 stopped working on 30 January 2025, and on 23 September 2026 GitHub removed Node 20 from runners, so JavaScript actions now run on Node 24 with no opt-out. Move to current majors, check each action’s release notes for behaviour changes, such as checkout v7 refusing fork code under pull_request_target, and let Dependabot raise future updates.
Alternatives
GitHub Actions alternatives worth weighing
GitLab CI/CD
CI/CD integrated into GitLab, covering the full lifecycle from source through security scanning to deployment.
Jenkins
Self-hosted open source automation server with pipelines as code in a Jenkinsfile and more than 2,300 community plugins.
CircleCI
Hosted CI/CD known for fast builds, flexible caching and reusable configuration through orbs.
Resources
Where to go next
Picking the wrong tool costs more than picking late.
QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.