So far, we have reviewed how to review code with ES Lint + Prettier and Typecheck, how to set up and run smoke tests, how to run all Playwright + TypeScript tests, and reviewed the HTML report of results.
In this post, we are going to set up a three stage GitLab CI/ CD pipeline that will run against every merge request:
- Quality (lint, typecheck, prettier) --> Test ( smoke + regression ) --> Report ( Downloadable )
Stages run one after another, and jobs within a stage run in parallel. If one stage fails, the stages after it are skipped.
This begs the question: What is GitLab? What is CI/ CD? Or a pipeline? Or a merge request?
What is GitLab?
GitLab began in 2011 as a side project by Ukrainian programmer Dmytro Zaporozhets, built to help developers work together on code more easily. Over time, it grew into a large open-source DevOps platform. More than just storing code, it handles code reviews, CI/CD (continuous integration and continuous delivery), and package registries. A company called GitLab Inc. was eventually created to turn the project into a commercial product.
What makes GitLab different from tools that only host Git repositories (like a basic Git server) is that it bundles the entire software delivery process into one application. This includes:
- Merge requests – GitLab's term for proposing and reviewing code changes before they're merged into the main codebase (similar to a "pull request" in GitHub)
- Issue tracking – a built-in system for logging bugs, tasks, and feature requests
- Pipelines – automated workflows that build, test, and deploy code whenever changes are pushed
So instead of stitching together separate tools for version control, testing, and deployment, GitLab gives you all of it in one place.
Isn't That Just GitHub?
According to GitLab's "What are the fundamental differences between GitLab and GitHub", "GitHub's security scanning, AI, and advanced CI/CD are native but sold as separate paid add-ons on top of the base platform price [...] GitLab puts planning, source code, CI/CD, security, and deployment into a single application with one permission model, one audit trail, and shared analytics. That means fewer tools to run, fewer integrations to maintain, and one place to apply policies and AI across the whole lifecycle".GitLab calls all the extra stuff you need to purchase from GitHub a "toolchain tax".
What is CI/ CD?
"CI/CD stands for Continuous Integration and Continuous Delivery (or Deployment). It's a DevOps practice that automates building, testing, and deploying code changes, enabling faster and more reliable software releases.
"CI/CD is best explained as an automated workflow that replaces manual steps with pipelines that build, test, and deploy software reliably.
"CI/CD falls under DevOps (the joining of development and operations teams) and combines the practices of continuous integration and continuous delivery. CI/CD automates much or all of the manual human intervention traditionally needed to get new code from a commit into production, encompassing the build, test (including integration tests, unit tests, and regression tests), and deploy phases, as well as infrastructure provisioning.
"With a CI/CD pipeline, development teams can make changes to code that are then automatically tested and pushed out for delivery and deployment. Get CI/CD right and downtime is minimized and code releases happen faster" - GitLab.com / What is CI / CD?
"CI/CD is best explained as an automated workflow that replaces manual steps with pipelines that build, test, and deploy software reliably.
"CI/CD falls under DevOps (the joining of development and operations teams) and combines the practices of continuous integration and continuous delivery. CI/CD automates much or all of the manual human intervention traditionally needed to get new code from a commit into production, encompassing the build, test (including integration tests, unit tests, and regression tests), and deploy phases, as well as infrastructure provisioning.
"With a CI/CD pipeline, development teams can make changes to code that are then automatically tested and pushed out for delivery and deployment. Get CI/CD right and downtime is minimized and code releases happen faster" - GitLab.com / What is CI / CD?
What are CI/ CD Pipelines?
"A CI/CD pipeline is an automated process utilized by software development teams to streamline the creation, testing and deployment of applications.
"CI represents continuous integration, where developers frequently merge code changes into a central repository, allowing early detection of issues. CD refers to continuous deployment or continuous delivery, which automates the application's release to its intended environment, ensuring that it is readily available to users. This pipeline is vital for teams aiming to improve software quality and speed up delivery through regular, reliable updates.
"Integrating a CI/CD pipeline into your workflow significantly reduces the risk of errors in the deployment process. Automating builds and tests ensures that bugs are caught early and fixed promptly, maintaining high-quality software". - GitLab.com / What is CI / CD?
Just like GitHub, GitLab is free to join, hoping people will use the platform and decide to subscribe for more of its services.
With GitLab you can take your changes you want to make to the codebase of the code repository, try to commit the changes, and make a request to merge the changes into the main branch.
You can set up the CI/ CD pipeline that before a senior developer looks at your merge request to see if it can be approved, the merge request gets tested right when it gets created.
How Do You Create a Merge Request?
Let's say you want to make a change to a code repository to bun-create-playwright.
First, if you haven't already, you need to pull down the code repo:
- Do you have Git? Open PowerShell or the Terminal, whatever is your Command Line Interface for the PC or the Mac, and run: git --install.
- If you do not have git, install it on you Windows machine or MacBook.
- Get the code repository: git clone git@gitlab.com:tjmaher/bun-create-playwright.git
Already downloaded the repo, and want to make sure you have the latest version?
- git fetch origin
- git checkout main
- git pull origin main
Ready to make some updates?
- git checkout -b feature/my-new-change
Then make your changes. After that is done, add your changes and create a commit for your changes:
- git status // All your files you made changes to are listed in red, since they are unstaged.
- git .add
- git status // All your changes are now listed in green. They are staged.
- git commit -m "This is my commit!"
Changes all set? Push the changes to your remote repo:
- git push -u origin features/my-new-change
Now, you will see a URL pointing exactly to the link where you can see your commit in GitLab. Select the button, "Create Merge Request".
Once you that, the Merge Request will be created, and will be run automatically on the GitLab CI/ CD pipeline.. once we create one.
Create the Configuration File For a CI/ CD Pipeline
- Want to practice the basics? See Tutorial: Create and run your first GitLab CI/CD pipeline
- This walks you through creating a public project, setting up a test runner, and creating a .gitlab-ci.yml file in your root directory, and running a job.
Here, we are going to start reviewing the sample .gitlab-ci.yml file I created for Bun-Create-Playwright, the configuration file used when creating a new pipeline that will process the changes we have made to the code base.
bun-create-playwright / .gitlab-ci.yml
stages:
- quality
- test
- report
For any CI / CD pipeline created when a merge request is pushed, we are setting up four stages:
- Quality: Where we check the format using Prettier, run the linter allowing no warnings to sneak by, and run the TypeScript compiler to do type checking. This stage takes seconds to run.
- Test:
- We run just the one smoke test on Chromium, checking that we can log into the Login page.
- Is the smoke test successful? We then will run ALL the tests, positive and negative, on every platform, on Chrome, Firefox, and WebKit / Safari.
- Report: Create a report we can download and examine.
Hidden Jobs: The Quality Template
If we take a GitLab job, and add a dot to the name, it becomes a "hidden" job. Let's say that you have a stage called QUALITY, and you have a substage which checks the format of the code, a substage that runs a linter on the code, and a substage that type checks the code.
Each substage might need to have the same things added to it, such as using the same pre-configured Docker image for bun, version 1.3, so you don't have to manually install bun, its version managers or system libraries.
Or you could take the lockfile as is, produced when dependencies are installed, and use that same exact version on all three substages.
That ,bun_quality could act as a template that could be extended into all three substages.
.bun_quality:
image: oven/bun:1.3
before_script:
- bun --version
- bun install --frozen-lockfile
Docker is a tool that packages your code and everything it needs -- like libraries and system tools -- into a single box called a container. This container runs the exact same way on your laptop, your teammate's computer, and the cloud server, fixing the classic "it works on my machine" problem
Docker Hub is a public cloud-based registry service and community library of Docker images.
Bun, the package manager, has a Docker Hub it has dubbed "oven". Here, we are going to be using the Docker image of Bun version 1.3.
Before we run any of the substages -- format, lint, or typecheck -- we will in all stages check the version of bun used, print it out to the log file, then install bun using the Bun 1.3 Docker image, from a frozen lockfile.
- A lockfile is a "file that lists the direct and indirect dependencies of an application and their version numbers. Its purpose is reproducibility, to ensure anyone installing the application’s dependencies gets the exact same versions". ( GitLab / Terminology Lockfile )
- By installing a frozen lockfile, we are locking down all package dependencies in our jobs so we will use the same exact version in all three cases. If the lock file is out of sync from package.json, we will have it fail instead of it silently updating.
By having it frozen, we can guarantee reproducible builds and catch cases where a dependency was added/changed without committing the updated lockfile.
The Quality Stage
Now that we set up the quality template, we can use it for the substages: format, lint, and typecheck.
format:
extends: .bun_quality
stage: quality
script:
- bun run format:check
lint:
extends: .bun_quality
stage: quality
script:
- bun run lint:ci
typecheck:
extends: .bun_quality
stage: quality
script:
- bun run typecheck
Need to add another part to the quality stage? You can just declare it, extending the same template, designating the stage, and adding the script you want to run.
Back in the post How to Configure Playwright Test to run smoke tests, headed tests, and debug versions through scripts in package.json we set up the following scripts:
- Format:check throws an error when Prettier finds a formatting style error while running the script: "prettier --check ."
- Lint:ci uses ES Lint to see if the code has an errors with the script: "eslint . --max-warnings 0"
- Typecheck uses the TypeScript compiler to see if there an errors, running: "tsc --noEmit"
This CI Pipeline will execute these three stages. Any errors detected? The build pipeline will be halted.
Introducing: The Official Playwright Docker Image
This GitLab pipeline when running tests, will use the official Docker image that Microsoft has released to run Playwright tests: mcr.microsoft.com/playwright:v1.62.1-noble
Microsoft stores its Docker images in the Microsoft Artifact Registry under the Playwright subfolder at at https://mcr.microsoft.com/en-us/artifact/mar/playwright
Microsoft stores its Docker images in the Microsoft Artifact Registry under the Playwright subfolder at at https://mcr.microsoft.com/en-us/artifact/mar/playwright
The Docker image uses the Ubuntu 24.04 LTS (Noble Numbat) operating system.
Hidden Jobs: The Test Template, Run on Playwright Docker
Just like the Quality stage, the Test stage will receive its own template to be used in its substages.
And, just like the Quality stage, to make sure that each substage is on the same page, we will be using a Docker environment. The Docker image we will be using is Microsoft's official Docker environment for Playwright.
.bun_playwright:
image: mcr.microsoft.com/playwright:v1.62.1-noble
before_script:
- apt-get update && apt-get install -y unzip curl
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun --version
- bun install --frozen-lockfile
From Playwright.dev / Docker: "Dockerfile.noble can be used to run Playwright scripts in Docker environment. This image includes the Playwright browsers and browser system dependencies".
Using this image in your CI/CD pipeline guarantees a stable, pre-configured, and reproducible environment for running your automated tests
After that, we are using APT, the Advanced Package Tool to refresh the container's package index, then install unzip and curl.
Once curl is installed, we download Bun's official install script (See Bun.sh / Installation ) and pipes it directly into bash to execute. The flags:
- -f fails silently on server errors instead of printing an HTML error page
- -s runs silently (no progress meter)
- -S re-enables error messages even in silent mode
- -L follows redirects.
Then we export bun's install directory to the shell's PATH for the current job session, since the install script doesn't automatically make bun available in a fresh shell (it normally edits .bashrc/.zshrc for interactive shells, which CI runners don't source the same way).
Did everything work?
- Print the installed Bun version to the job log. This confirms the install succeeded (the job fails fast here if
bunisn't onPATH), and leaves a version record in the CI logs for debugging.
Finally we install the project's dependencies using Bun's package manager. --frozen-lockfile tells Bun to install exactly what's in bun.lock (or bun.lockb) and fail the build if the lockfile is out of sync with package.json, rather than silently updating it, which is the standard practice for CI to guarantee reproducible builds.
Whew! And this will happen on every substage in Test.
The Test Stage
Now, we set up the test stage, first running all tests we have tagged with "@smoke". If that passes, we run ALL the tests.
Everything we set up in .bun_playwright gets pulled right into this GitLab CI job.
playwright:
extends: .bun_playwright
stage: test
timeout: 60 minutes
script:
- bun run test:smoke
- bun run test
artifacts:
when: always
paths:
- playwright-report/
- test-results/
- reports/
reports:
junit: reports/junit/results.xml
expire_in: 30 days
- After the clock starts, we are giving it one full hour for all the tests to run.
Assemble the Artifacts!
- playwright-report/: This directory holds the main interactive HTML report. It includes the index.html file that lets you visually click through test suites, read error logs, and see execution timelines.
- test-results/: This folder stores the raw individual assets captured during test execution. If a test fails, Playwright drops failure screenshots, video recordings of the browser screen, and .zip trace files (which allow you to step through the test execution like a video) into this directory
- reports/: This is a catch-all or customized directory often used for alternative report formats. It holds JUnit XML files meant to be parsed directly by your CI platform to display a quick test summary on your pull/merge request page
The Report Stage
On the report stage, we are using a lightweight Linux container called "alpine" to execute the commands quickly. We don't need pre-installed browsers or a JavaScript runtime environment. These reports will be generated whether the tests pass or fail.
This job will be started right after the "playwright" job finishes in the Test stage.
report:
stage: report
image: alpine:latest
when: always
needs:
- job: playwright
artifacts: true
script:
- ls -la playwright-report/ test-results/ reports/junit/ || true
artifacts:
when: always
paths:
- playwright-report/
- test-results/
- reports/
reports:
junit: reports/junit/results.xml
expire_in: 30 days
Next, we have the script, where we are going to list the contents of our test results directory. The "-la" will even list the hidden directories that might exist.
Even if one of the directories do not exist, we still want to carry on, which is why we are appending "OR TRUE" to the script.
Finally, we add the reports and XML file to GitLab's servers.
View The Results
There you have it! Now, when you push changes, your code can be checked for quality, and you can make sure the tests all actually run.
Each code change, each merge request, generates a new pipeline. You can view it at https://gitlab.com/tjmaher/bun-create-playwright/-/pipelines
Test Report Artifacts
- report:archive: Contains the full, human-readable HTML test reports (like Allure, if set up, or Playwright's native HTML report) compressed into a zip archive.
- report:junit: Contains a structured XML file in the JUnit format used by CI/CD platforms to parse, count, and display test success rates directly in the pipeline UI.
Playwright Specific Artifacts
- playwright:archive: Stores heavy debugging assets generated during test failures, such as recorded videos, browser screenshots, and .zip trace files that you can load into the Playwright Trace Viewer.
- playwright:junit: Holds the specific JUnit-formatted XML results generated directly by the Playwright test runner (@playwright/test) during execution.
You can also drill down into the pipeline to see each stage's results.
And, last of all, you can download an HTML version of the report, in the archive of artifacts.
Next, we are going to look into manually kicking off a job, viewing a report on GitLab Pages.
Bun Create Playwright:
- Part One: Introducing bun, a new package manager and JavaScript runtime environment
- Part Two: What happens when you scaffold a Playwright framework and run installed tests using bun?
- Part Three: Add Type Checking and TSConfig to Bun-Create-Playwright
- Part Four: Checking code with lint, formatting it with prettier
- Part Five: Running Tests with Playwright Test Explorer and Generating Tests with Codegen
- Part Six: How Playwright Frameworks get configured with playwright.config.ts
- Part Seven: Implementing Page Objects in Playwright
- Part Eight: How to Configure Playwright Test to run smoke tests, headed tests, and debug versions through scripts in package.json
- Part Nine: Setting up a CI/ CD pipeline with GitLab: Quality, Test and Report
- GitLab: https://gitlab.com/tjmaher/bun-create-playwright
Until then, Happy Testing!
-T.J. Maher
Software Engineer in Test
BlueSky | YouTube | LinkedIn | Articles