Build a CI/CD Pipeline with GitLab
Deploy a Vite site to GitLab Pages with a multi-stage CI/CD pipeline.
Introduction
30 Second Summary
Every time you manually upload files to a server or copy build folders around, you risk breaking something. Automated pipelines take that manual work off your plate by building, testing, and deploying your code every time you push a change.
In this project, you will build a multi-stage CI/CD pipeline on GitLab that automatically lints, builds, and deploys a static site to a live URL whenever you push code.
What You'll Build
A Vite-powered static site deployed to GitLab Pages through a multi-stage CI/CD pipeline configured entirely in a single .gitlab-ci.yml file.
By the end of this project, you'll have:
- A GitLab repository containing a Vite project with linting, building, and deployment fully automated.
- A live GitLab Pages site accessible at a public URL you can share in your portfolio.
- A merge request workflow with conditional pipeline rules that run lint and build on branches but only deploy from main.
- Secret Mission: Add a staging environment with parallel Pages deployments so merge requests get their own preview URL.
Are there any prerequisites?
You should be comfortable with basic Git operations like commits, branches, and pushing to a remote. Basic familiarity with frontend development (HTML, CSS, JavaScript) is helpful but not required.
Set Up Your GitLab Project and Vite App
Every CI/CD pipeline needs two things: a repository to watch for changes, and code to build. In this step, you'll set up both.
You'll create a GitLab project, clone it locally, and scaffold a Vite app inside it. By the end, your code will be pushed to GitLab and ready for the pipeline you'll build in the next step.
In this step, get ready to:
- Create a GitLab project and clone it locally.
- Scaffold a Vite app and customize the project files.
- Push your code to GitLab and confirm no pipeline runs yet.
Create your GitLab project
You need a remote repository on GitLab to host your code and eventually run pipelines against it.
- Open your browser and go to gitlab.com. Sign in to your account (or create a free account if you don't have one).
- Click the + button in the top navigation bar, then select New project/repository.
- Select Create blank project.
- In the Project name field, enter vite-pipeline-demo.
- Under Visibility Level, select Public.
- Check the box for Initialize repository with a README.
- Click Create project.
Why Public visibility?
GitLab Pages requires Public visibility on the free tier to serve your deployed site to anyone on the internet. If you set the project to Private, your Pages site won't be accessible without authentication.
GitLab creates your project and drops you on the project homepage. You'll see a README.md file already in the repository.
Clone the repository and scaffold your Vite app
Now you'll clone the repository to your machine and scaffold a Vite project inside it. The . in the scaffold command tells Vite to generate files in the current directory instead of creating a new subfolder.
- On your GitLab project page, click the Code dropdown button and copy the HTTPS clone URL.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar, type Terminal (macOS) or Git Bash (Windows), and press Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Clone your GitLab repository by running this command:
git clone [[GITLAB_CLONE_URL="https://gitlab.com/YOUR_USERNAME/vite-pipeline-demo.git"]]
- Move into the cloned project folder by running:
cd vite-pipeline-demo
- Scaffold a Vite project in the current directory by running:
npm create vite@latest . -- --template vanilla
What prompts will I see?
You may be asked to confirm installing the create-vite package. Press y and Enter to proceed.
Because the directory already has a README.md from GitLab, Vite will ask how to handle the non-empty directory. Select Ignore files and continue so Vite scaffolds alongside the existing README.
- Install the project dependencies by running:
npm install
Seeing permission errors on install?
Make sure you are inside the vite-pipeline-demo folder (run pwd to check). If npm complains about permissions, avoid using sudo. Reinstall Node.js using a version manager instead.
help me fix npm permission errors on my machine
Now you'll customize the scaffolded files to match the project content. The default Vite scaffold includes some demo files you don't need.
- Remove the unnecessary scaffold files by running:
rm counter.js javascript.svg
- Open the vite-pipeline-demo folder in your code editor. Press Cmd+Space (macOS) or the Windows key (Windows), type VS Code, and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder, then select the vite-pipeline-demo folder on your Desktop and click Open.
- Open index.html in the editor, select all the content, delete it, and paste the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Pipeline Demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/main.js"></script>
</body>
</html>
What does this file do?
This is the entry point for your Vite app. The <div id="app"> is where your JavaScript will inject content. The <script type="module"> tag loads main.js using ES modules.
- Open main.js, select all, delete, and paste:
import "./style.css";
document.querySelector("#app").innerHTML = `
<div>
<h1>Vite Pipeline Demo</h1>
<p>Deployed automatically via GitLab CI/CD</p>
<p class="pipeline-info">Every push to main triggers: lint → build → deploy</p>
</div>
`;
What does this file do?
This imports your stylesheet and injects HTML into the #app div. The content describes your pipeline workflow so you'll see it on the deployed site later.
- Open style.css, select all, delete, and paste:
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color: #213547;
background-color: #ffffff;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.pipeline-info {
color: #666;
font-style: italic;
}
- Create a new file called vite.config.js in the project root (at the same level as index.html) and paste:
import { defineConfig } from "vite";
export default defineConfig({
build: {
outDir: "dist",
},
});
Why add a Vite config?
This explicitly sets the build output directory to dist. Your CI/CD pipeline will reference this directory when deploying to GitLab Pages. Being explicit avoids confusion if Vite ever changes its default.
- Save all files with Cmd+S (macOS) or Ctrl+S (Windows).
✔️ Awesome, I've got everything!
Great. Make sure all four files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the full content of each file you edited or created:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Pipeline Demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/main.js"></script>
</body>
</html>
import "./style.css";
document.querySelector("#app").innerHTML = `
<div>
<h1>Vite Pipeline Demo</h1>
<p>Deployed automatically via GitLab CI/CD</p>
<p class="pipeline-info">Every push to main triggers: lint → build → deploy</p>
</div>
`;
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color: #213547;
background-color: #ffffff;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.pipeline-info {
color: #666;
font-style: italic;
}
import { defineConfig } from "vite";
export default defineConfig({
build: {
outDir: "dist",
},
});
Verify the dev server, commit, and push
Before pushing to GitLab, confirm the app runs locally. This catches any typos before they reach your remote repository.
- Start the Vite dev server by running this command in your terminal (make sure you're in the vite-pipeline-demo folder):
npm run dev
Your terminal shows a local URL (typically http://localhost:5173). Open that URL in your browser. You should see a centered heading that says "Vite Pipeline Demo" with italicized pipeline info text below it.
Don't see the page?
Check that you saved all four files (index.html, main.js, style.css, vite.config.js). If you see a blank page, open the browser console (F12) and look for errors pointing to missing files.
help me debug why my Vite dev server shows a blank page
- Stop the dev server by pressing Ctrl+C in your terminal.
- Commit all files and push to GitLab by running these commands:
git add .
git commit -m "Add Vite project scaffold"
git push
What does this do?
These three commands stage all files, create a commit with a descriptive message, and push everything to your GitLab remote. Your code is now on the main branch in GitLab.
- Go back to your GitLab project in the browser.
- In the left sidebar, navigate to Build > Pipelines.
You should see an empty pipelines page with no pipeline runs listed. This is expected. GitLab only triggers a pipeline when it finds a .gitlab-ci.yml file in your repository, which you haven't created yet.
Why no pipeline yet?
GitLab CI/CD looks for a .gitlab-ci.yml file at the root of your repository to know what jobs to run. Without this file, pushes don't trigger anything. You'll create this file in the next step.
Seeing a pipeline that failed?
Some GitLab projects have Auto DevOps enabled by default, which creates an automatic pipeline. If you see a failed pipeline, go to Settings > CI/CD > Auto DevOps and uncheck Default to Auto DevOps pipeline. Then click Save changes.
help me disable Auto DevOps in my GitLab project
Your repository is live on GitLab with all your Vite project files. Next up, you'll create the .gitlab-ci.yml file that turns every push into an automated build and deployment.
Create Your First Pipeline
Your Vite project is pushed to GitLab, but nothing happens automatically when you push code. That's because GitLab needs a configuration file telling it what to do with your repository.
The file that controls automation in GitLab is called .gitlab-ci.yml. In this step, you'll create this file to build your Vite site and deploy it to GitLab Pages every time you push to main.
In this step, get ready to:
- Create a .gitlab-ci.yml pipeline configuration file.
- Push the file to trigger your first automated pipeline.
- Verify your site is live on GitLab Pages.
Write your pipeline configuration
A CI/CD pipeline is a set of automated steps that run every time you push code. GitLab reads a file called .gitlab-ci.yml in the root of your repository to know which steps to run.
- Create a new file called .gitlab-ci.yml in the root of your vite-pipeline-demo folder (the same level as package.json).
- Paste the following pipeline configuration into the file:
# Define the pipeline stages
stages:
- deploy
# Set the default Docker image for all jobs
default:
image: node:22
# Deploy the site to GitLab Pages
deploy-pages:
stage: deploy
script:
- npm ci
- npm run build
# Save the build output so Pages can publish it
artifacts:
paths:
- dist/
# Mark this job as a Pages deployment
pages:
publish: dist
# Only run this job on the main branch
rules:
- if: $CI_COMMIT_BRANCH == "main"
What does this configuration do?
- stages defines one stage called deploy. Stages control the order jobs run in.
- default: image: node:22 tells GitLab to run every job inside a Node.js 22 Docker container.
- npm ci installs dependencies from package-lock.json (faster and more reliable than npm install for CI environments).
- npm run build compiles your Vite project into production-ready files in the dist/ folder.
- pages: publish: dist tells GitLab this job deploys to Pages, publishing the dist directory as your live site.
- The rules section restricts this job to only run when code is pushed to the main branch.
- Save .gitlab-ci.yml.
File not saving with the dot prefix?
Files starting with a dot (like .gitlab-ci.yml) are hidden files on most operating systems. Make sure your editor is creating the file with the exact name .gitlab-ci.yml and not gitlab-ci.yml (missing the leading dot).
Help me create a .gitlab-ci.yml file in my project root
✔️ Awesome, I've got everything!
Great. Double check you've saved the file before moving on.
ⓧ I'd like to double check the full code
Here is the complete .gitlab-ci.yml file:
stages:
- deploy
default:
image: node:22
deploy-pages:
stage: deploy
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
pages:
publish: dist
rules:
- if: $CI_COMMIT_BRANCH == "main"
Push your pipeline to GitLab
Your pipeline configuration only takes effect once it reaches GitLab. Committing and pushing the file triggers GitLab to read it and start your first pipeline automatically.
- Stage, commit, and push your new file by running these commands:
git add .gitlab-ci.yml
git commit -m "Add CI/CD pipeline configuration"
git push
- In your browser, return to your vite-pipeline-demo project on GitLab.
- In the left sidebar, select Build then Pipelines.
- You should see a pipeline running (or already finished) with a green Passed status.
What is happening in the pipeline?
GitLab detected your .gitlab-ci.yml file and started a hosted runner (a temporary cloud server) to execute your job. The runner pulls the node:22 Docker image, installs your dependencies, builds your site, and hands the dist/ folder to GitLab Pages.
A special pages:deploy job may also appear in the pipeline view. GitLab runs this in the background to finalize your Pages deployment.
Pipeline failed or stuck on pending?
- Check that your file is named exactly .gitlab-ci.yml (not .yaml) and sits in the repository root.
- Click on the failed job name to see its log output. Look for YAML syntax errors near the top.
- Make sure your project visibility is set to Public. GitLab's shared runners are enabled by default for public projects.
My GitLab pipeline failed or is stuck. Help me debug it.
Find your live site URL
Once the pipeline passes, GitLab publishes your built site to a public URL on gitlab.io.
- In the left sidebar of your GitLab project, select Deploy then Pages.
- Under Deployments, find your active deployment URL.
- Click the URL to open your live site in a new tab.
You should see your Vite site with the heading "Vite Pipeline Demo" and the text "Deployed automatically via GitLab CI/CD".
What is my Pages URL?
Your site is hosted at https://<username>.gitlab.io/vite-pipeline-demo where <username> is your GitLab username. It might take a few minutes for the site to become available after the pipeline completes.
Seeing a 404 error on your Pages URL?
- Wait 2-3 minutes after the pipeline passes. Pages deployments need a moment to propagate.
- Verify your index.html exists in the project root. GitLab Pages requires a non-empty index.html in the published directory.
- Check that pages: publish: dist is in your .gitlab-ci.yml. Without it, GitLab looks for a public/ folder instead of dist/.
My GitLab Pages site shows a 404 after the pipeline passed. Help me fix it.
Your first pipeline is running and your site is live on the internet. Next up, you'll add a lint stage to catch code issues before deployment.
Add Multi-Stage Linting
Your site is live on GitLab Pages, deployed by a single-job pipeline. That works, but real-world pipelines split work into stages so problems are caught early and failures are isolated.
In this step, you will add a linting stage that checks your code quality before the build even starts. If a lint error exists, the pipeline fails fast instead of wasting time building broken code.
In this step, get ready to:
- Install ESLint and configure a lint script.
- Restructure the pipeline into three sequential stages.
- Push and verify the multi-stage pipeline graph in GitLab.
Install ESLint and create the configuration
ESLint is a static analysis tool that finds problems in JavaScript code without running it. You will install it as a dev dependency so it is available both locally and inside the CI pipeline.
- Install ESLint by running this command in your terminal:
npm install --save-dev eslint
- You should see ESLint added to your devDependencies in package.json.
Install failed or showing warnings?
Make sure you are running the command from inside your vite-pipeline-demo folder where package.json lives. If you see peer dependency warnings, they are safe to ignore for this project.
Help me troubleshoot my ESLint installation
Next, ESLint needs a configuration file to know which rules to apply. ESLint 9+ uses a flat config format stored in eslint.config.js.
- Create a new file called eslint.config.js in the root of your project and paste the following code:
export default [
{
files: ["**/*.js"],
rules: {
// Warn about variables that are declared but never used
"no-unused-vars": "warn",
// Allow console.log statements in this project
"no-console": "off",
},
},
{
// Tell ESLint to skip these directories entirely
ignores: ["dist/", "node_modules/"],
},
];
What does this configuration do?
The files array tells ESLint to check all .js files in the project. The two rules catch unused variables (as a warning) and allow console.log statements.
The ignores block prevents ESLint from wasting time on build output (dist/) and third-party code (node_modules/).
Now you need a script in package.json so the pipeline can run linting with a single command.
- In package.json, find the "scripts" section. It currently looks like this:
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
- Add a "lint" script after "preview", so the section now looks like this:
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint ."
},
- Save package.json.
- Verify the lint script works locally by running:
npm run lint
- You should see the command complete with no errors. If you have any unused variables, ESLint prints a warning but still exits successfully.
Seeing a configuration error?
If ESLint reports it cannot find a configuration file, make sure eslint.config.js is in the root of your project (the same folder as package.json). Also confirm your package.json has "type": "module" so Node.js treats the config as ESM.
Help me fix my ESLint configuration error
✔️ Awesome, I've got everything!
Great. Make sure both eslint.config.js and package.json are saved.
ⓧ I'd like to double check the full code
Here is the full eslint.config.js:
export default [
{
files: ["**/*.js"],
rules: {
"no-unused-vars": "warn",
"no-console": "off",
},
},
{
ignores: ["dist/", "node_modules/"],
},
];
And here is the full package.json:
{
"name": "vite-pipeline-demo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint ."
},
"devDependencies": {
"eslint": "^9.0.0",
"vite": "^8.1.2"
}
}
Restructure the pipeline into three stages
Your current .gitlab-ci.yml has one stage and one job. You will now split the pipeline into three stages: lint, build, and deploy. Each stage runs sequentially, so a lint failure stops the pipeline before it wastes time building.
Why split into stages?
Stages run in sequence. If lint fails, the build and deploy stages never start. This saves compute minutes and gives you faster feedback on code quality issues.
Splitting responsibilities also lets you pass artifacts between jobs. The build job produces dist/ and the deploy job consumes it without re-running the build.
- Open .gitlab-ci.yml and replace the entire file contents with the following:
stages:
- lint
- build
- deploy
default:
image: node:22
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/
lint:
stage: lint
script:
- npm ci
- npm run lint
rules:
- if: $CI_COMMIT_BRANCH == "main"
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy-pages:
stage: deploy
script:
- echo "Deploying to GitLab Pages"
artifacts:
paths:
- dist/
pages:
publish: dist
rules:
- if: $CI_COMMIT_BRANCH == "main"
What does this pipeline do?
- stages defines the execution order: lint first, then build, then deploy.
- default.cache shares the node_modules/ folder between jobs so npm ci runs faster on subsequent jobs. The key $CI_COMMIT_REF_SLUG scopes the cache to the current branch.
- The build job saves dist/ as an artifact that expires after 1 hour. GitLab automatically passes artifacts to later stages.
- The deploy-pages job receives dist/ from the build job and publishes it to GitLab Pages using pages: publish: dist.
- Save .gitlab-ci.yml.
✔️ Awesome, I've got everything!
Great. Double check you saved the file before committing.
ⓧ I'd like to double check the full code
Here is the complete .gitlab-ci.yml:
stages:
- lint
- build
- deploy
default:
image: node:22
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/
lint:
stage: lint
script:
- npm ci
- npm run lint
rules:
- if: $CI_COMMIT_BRANCH == "main"
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy-pages:
stage: deploy
script:
- echo "Deploying to GitLab Pages"
artifacts:
paths:
- dist/
pages:
publish: dist
rules:
- if: $CI_COMMIT_BRANCH == "main"
Push and verify the three-stage pipeline
- Commit and push all your changes by running these commands:
git add .
git commit -m "Add ESLint and restructure pipeline into three stages"
git push
- Switch back to your GitLab project in the browser.
- Navigate to Build in the left sidebar, then select Pipelines.
- Click on the latest pipeline to open the pipeline details page.
You should see a pipeline graph with three stages displayed left to right: lint, build, deploy. Each stage contains one job that runs sequentially.
What does the pipeline graph show?
The pipeline graph groups jobs by stage. Stages run in the order defined in your stages array. If all jobs in a stage succeed, the pipeline moves to the next stage. If any job fails, the pipeline stops early.
Pipeline failed at the lint stage?
Check the lint job logs in GitLab. If you see a message about ESLint not finding a configuration file, make sure eslint.config.js was committed. Run git status locally to confirm it is tracked.
If the error mentions node_modules, check that your .gitignore includes node_modules and that the pipeline runs npm ci before npm run lint.
Help me debug my failing lint pipeline job
Your pipeline now has proper separation of concerns. Each stage has a single responsibility, and failures surface at the earliest possible point. Next up, you will configure conditional rules so this pipeline behaves differently for merge requests versus the main branch.
Configure Merge Request Pipelines
Your pipeline now validates and deploys code automatically on every push to main. In real teams, developers work on feature branches and open merge requests before code reaches production.
In this step, you will configure conditional pipeline rules so that lint and build jobs run on merge request events, while the deploy job stays exclusive to main. You will also track deployments with GitLab Environments.
In this step, get ready to:
- Create a feature branch and open a Merge Request.
- Update pipeline rules for conditional execution.
- Track deployments with GitLab Environments.
Create a feature branch and open a Merge Request
Right now, every pipeline job only triggers on the main branch. To test merge request pipelines, you need a branch with changes and an open MR.
- Create a new branch called feature/add-styling by running this command:
git checkout -b feature/add-styling
- Open style.css in your editor.
- Find the background-color value in the :root selector. Change it from #ffffff to #f9f9f9.
- Save style.css.
- Commit and push the branch to GitLab by running these commands:
git add .
git commit -m "Add styling changes"
git push -u origin feature/add-styling
What does the push output show?
After pushing, GitLab prints a direct link in the terminal to create a merge request from your branch. You can use this link, or follow the UI steps below.
- Return to your GitLab project in the browser.
- In the left sidebar, select Code then Merge requests.
- Click New merge request.
- Select feature/add-styling as the source branch and main as the target branch.
- Click Compare branches and continue.
- Leave the title and description as defaults, then click Create merge request.
Your merge request is now open. No pipeline runs yet because the current rules only trigger on the main branch.
Update pipeline rules for merge request events
You want lint and build jobs to validate code on every merge request, catching errors before code reaches main. The deploy job should remain restricted to main only.
- In .gitlab-ci.yml, find the rules section under the lint job. It currently looks like this:
rules:
- if: $CI_COMMIT_BRANCH == "main"
- Add a merge request event rule above the existing rule, so the lint job's rules now look like this:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
What does this rule do?
Each rule in the list is evaluated independently. If any rule matches, the job runs.
The $CI_PIPELINE_SOURCE == "merge_request_event" rule triggers the job when a merge request is created or updated. The $CI_COMMIT_BRANCH == "main" rule triggers it on direct pushes to main.
- Make the same change to the build job's rules section. Update it to match:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
- Leave the deploy-pages job's rules unchanged. It should still only run on main.
Add environment tracking and verify the MR pipeline
GitLab can track where your code is deployed using environments. Adding the environment keyword lets you see deployment history in the GitLab UI.
- In the deploy-pages job, find this section:
pages:
publish: dist
rules:
- if: $CI_COMMIT_BRANCH == "main"
- Add environment: production between the pages block and the rules block, so it looks like this:
pages:
publish: dist
environment: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
What does the environment keyword do?
The environment keyword tells GitLab that this job deploys to a named environment. Every time the job runs successfully, GitLab records a deployment entry you can view under Operate then Environments in the left sidebar.
This gives you a history of what was deployed, when, and from which commit.
- Save .gitlab-ci.yml.
- Commit and push the changes to your feature branch by running:
git add .gitlab-ci.yml
git commit -m "Add MR pipeline rules and environment tracking"
git push
Because you pushed to a branch with an open merge request, GitLab triggers the MR pipeline automatically.
- Return to your merge request page in GitLab.
- Scroll down to the pipeline section. You should see a pipeline running with the lint and build stages only.
- Confirm that the deploy-pages job does NOT appear in the MR pipeline.
Why no deploy on the MR?
The deploy job's rule only matches $CI_COMMIT_BRANCH == "main". Since this pipeline was triggered by a merge request event on a feature branch, the deploy job is skipped entirely. This prevents accidental deployments from unreviewed code.
- Once the MR pipeline passes, click Merge on the merge request page to merge your branch into main.
- In the left sidebar, select Build then Pipelines.
- You should see a new pipeline running on main with all three stages: lint, build, and deploy.
- After the pipeline passes, select Operate then Environments in the left sidebar.
- You should see a production environment listed with your deployment recorded.
MR pipeline not appearing?
Make sure you pushed the .gitlab-ci.yml changes to the feature/add-styling branch, not to main. The MR pipeline uses the CI config from the source branch.
Also check that the merge request is still open. A closed or merged MR will not trigger a new pipeline.
Help me debug why my GitLab MR pipeline is not running.
✔️ Awesome, I've got everything!
Great. Double check you have saved .gitlab-ci.yml and your environment is showing in the GitLab UI.
ⓧ I'd like to double check the full code
Here is the complete .gitlab-ci.yml after this step:
stages:
- lint
- build
- deploy
default:
image: node:22
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/
lint:
stage: lint
script:
- npm ci
- npm run lint
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
deploy-pages:
stage: deploy
script:
- echo "Deploying to GitLab Pages"
artifacts:
paths:
- dist/
pages:
publish: dist
environment: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
Your pipeline now separates validation from deployment. Merge requests get lint and build checks without accidentally deploying unreviewed code. Next up, the Secret Mission challenges you to take this further with staging preview environments.
Secret mission
Add Staging Preview Environments
Your pipeline deploys to production on every push to main. But what if reviewers could preview changes from a merge request before merging? In this secret mission, you will configure parallel Pages deployments so every MR gets its own live preview URL that reviewers can trigger on demand.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on GitLab's free tier, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- GitLab project vite-pipeline-demo on gitlab.com (free, no cost).
- GitLab Pages site hosted at gitlab.io (free, no cost).
- Local vite-pipeline-demo folder on your Desktop.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep your live site accessible.
- Your GitLab Pages site stays live at no cost.
- Your pipeline continues to run on every push, using free-tier compute minutes.
- Your local project folder remains on your Desktop for future edits.
Pause - I'll come back to this later
No action needed for GitLab resources. Pages remains active until you delete the project, and there are no costs to pause.
- Your site stays live and your pipeline stays configured.
- You can come back anytime, push a change, and the pipeline picks up where you left off.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow these steps to clean everything up.
- Delete the GitLab project (this also removes the Pages site and all pipeline history):
- In GitLab, navigate to your vite-pipeline-demo project.
- Select Settings in the left sidebar, then General.
- Expand Advanced.
- In the Delete project section, select Delete.
- Enter the project name vite-pipeline-demo in the confirmation dialog and select Yes, delete project.
Delete the local project folder from your Desktop:
macOS
rm -rf ~/Desktop/vite-pipeline-demo
Windows
Remove-Item -Recurse -Force "$HOME\Desktop\vite-pipeline-demo"
Nice Work!
Nice Work!
Nice work! You just built a production-grade CI/CD pipeline on GitLab that automatically lints, builds, and deploys a Vite site to GitLab Pages on every push.
Here's what you learned:
- Built a multi-stage CI/CD pipeline from scratch using .gitlab-ci.yml with lint, build, and deploy stages.
- Deployed a Vite site to GitLab Pages with automated builds on every push and a live URL you can share.
- Configured conditional pipeline rules so merge requests run linting and builds without triggering production deployment.
- Secret Mission: Set up parallel Pages deployments with per-MR preview URLs so reviewers can see changes before merging.
Ready to quiz yourself?