Project 14 — CI/CD with GitHub Actions · Level beginner → intermediate · Estimated duration: 1 h 30 to 2 h
You receive a static mini-site already wired to two working GitHub Actions workflows. You clone, you push, your site is online. You change a color in
config.json, you push again, the public page updates by itself. You finish by repairing three broken workflows that illustrate the classic CI/CD mistakes.
You have just been hired as a junior in a web agency. Your first job: a mini “portfolio” site hosted for free. The lead’s instruction:
“I want to change the site color by editing a single file, run a
git push, and have it update by itself. No FTP, no server to administer.”
The senior on the team has already written the two GitHub Actions workflows and the build script. Your work:
git clone + push + Pages activation).config.json with your information and your colors.push → Pages cycle works by changing the color at least twice.This project is not a lab where you write YAML from scratch. It is a lab where you take ownership of a CI/CD chain already in place, as in a real team: you do not reinvent the workflows, you understand, customize, and debug them.
This document is self-contained. You do not need to open any external lesson to finish it.
GitHub Actions is an execution engine built into GitHub. On every event (push, pull_request, a timer, a manual button), it launches workflows described in YAML, on runners (free Ubuntu / Windows / macOS VMs for public repositories).
name: My first workflow # displayed in the UI
on: # WHEN to run
push:
branches: [main]
jobs: # WHAT to do (>= 1 job)
construire: # free job name
runs-on: ubuntu-latest # WHERE to run (VM image)
steps: # the job steps
- uses: actions/checkout@v4 # "reusable action" step
- run: echo "Hello" # "shell command" stepTwo kinds of steps:
uses: calls a published action (actions/checkout@v4, actions/setup-python@v5, and so on).run: executes a shell command on the runner.on: | Triggered when… |
|---|---|
push: { branches: [main] } | You push a commit to main |
pull_request: { branches: [main] } | Someone opens / updates a PR targeting main |
schedule: [{ cron: "0 6 * * *" }] | Every day at 06:00 UTC |
workflow_dispatch: | Manual button in the Actions tab |
A single workflow can have several triggers at once — that is the case for deployer-pages.yml (push + manual).
GitHub Pages is static hosting included with every public repository. You publish HTML/CSS/JS through one of these methods:
gh-pages branch (older method)./docs folder on main (another method).The public URL is https://<utilisateur>.github.io/<nom-du-depot>/ — reachable from any browser.
To publish from Actions, deployer-pages.yml chains three official actions:
Critical constraint: the job that publishes needs specific permissions, otherwise you get Resource not accessible by integration. The provided workflow already declares them:
permissions:
contents: read
pages: write
id-token: writeThat is also outage 1 of mission 3 — remember it.
On every run, GitHub Actions exposes useful environment variables:
| Variable | Content |
|---|---|
GITHUB_SHA | The SHA of the commit that triggered the run |
GITHUB_REF_NAME | The branch name (main, feature-x, and so on) |
GITHUB_RUN_NUMBER | A counter that increments on every run |
In this project, outils/build.py reads these three variables and displays them on the published page. Visual proof that the deployment really comes from Actions and not from a local python build.py.
Expected end-to-end loop — you must reproduce it at least 2 times:
site/config.json in VS Code.couleur_fond (for example #0f172a → #7c3aed).git add site/config.json && git commit -m "changement de fond" && git push.https://<vous>.github.io/<depot>/ → the background is purple.projet14-github-actions-pages-tp/
├── 00-ENONCE.md <- this document
├── 02-CORRECTION.md <- detailed solutions (read AFTER trying)
├── README.md
│
├── site/ <- THE SITE (source)
│ ├── config.json <- YOU EDIT: colors, title, author
│ └── src/
│ ├── index.html.template <- HTML template ({{...}} markers)
│ └── css/
│ └── style.css.template <- CSS template ({{...}} markers)
│
├── outils/
│ └── build.py <- PROVIDED — do not modify
│
├── .github/ <- ALREADY WORKING WORKFLOWS
│ └── workflows/
│ ├── deployer-pages.yml <- build + publish on Pages
│ └── verifier-config.yml <- validates config.json on PRs
│
├── casses/ <- 3 defective workflows (Mission 3)
│ ├── casse-1-permissions-manquantes.yml
│ ├── casse-2-declencheur-errone.yml
│ └── casse-3-chemin-artefact.yml
│
└── .gitignore <- ignores dist/Important point: both workflows are already in .github/workflows/. You have nothing to write for the deployment part — only to understand what happens there and to customize config.json.
outils/build.py — it is the contract between config.json and the final HTML/CSS.dist/ folder: it is listed in .gitignore and rebuilt on every run.git --version).python --version — to test build.py locally).projet14-github-actions-pages-tp/ folder on your disk.projet14-actions-pages..gitignore (already provided).From the project root:
python .\outils\build.pyYou must see:
OK dist/index.html genere
OK dist/css/style.css genere
--- Substitutions appliquees ---
{{TITRE}} -> Mon premier site pilote par GitHub Actions
{{COULEUR_FOND}} -> #0f172a
...Open dist/index.html in a browser: you see the site with the default dark background. If it works locally, it will work on Actions.
git init
git branch -M main
git add .
git commit -m "point de depart projet14"
git remote add origin https://github.com/<votre-utilisateur>/projet14-actions-pages.git
git push -u origin mainActions tab of your repository → a run titled Deployer sur GitHub Pages appears. Wait until it turns green (~40 seconds after the push).
At the end of the deployer job, a message announces:
Your site is live at https://<vous>.github.io/projet14-actions-pages/Click → you see your published site.
config.json (20 points)Open site/config.json and change at least:
titre — put your name or that of a fictional project.auteur — your name.couleur_fond — in #RRGGBB format (for example #7c3aed, #dc2626, #0891b2).Check with python .\outils\build.py that the build still passes. A bad format (rouge, #ff, RGB(255,0,0)) is detected by build.py and will block the workflow.
Constraint: do not break the JSON. An extra comma, a missing brace → red workflow on GitHub.
This is the heart of the project. You must demonstrate that the CI/CD cycle works by triggering it at least twice:
site/config.json (change couleur_fond for example to #dc2626 — red).git add site/config.json && git commit -m "fond en rouge" && git push.https://<vous>.github.io/<votre-depot>/ → the background is red.#16a34a, blue #0891b2, purple #7c3aed, your choice).Proof to include in the report:
git log --oneline).Key point: on the public page, a tile displays Commit SHA : <7 caractères>. That SHA matches the latest commit on main. It is the visual proof that the page really comes from Actions.
The casses/ folder contains three defective workflows, each illustrating a classic mistake. For each one:
.github/workflows/ (next to the official workflows)..github/workflows/ (not in casses/ — the original stays broken).| File | Expected symptom |
|---|---|
casse-1-permissions-manquantes.yml | The deployer job fails with Resource not accessible by integration |
casse-2-declencheur-errone.yml | No run is triggered: the workflow is silently ignored |
casse-3-chemin-artefact.yml | The construire job fails with Error: Path does not exist: ./public |
Important tip: you can launch each workflow manually via Run workflow in the Actions tab, without making a real commit for every test. The defective workflows all have a workflow_dispatch: or you can add one temporarily.
Proof to provide for each outage: screenshot of the red run before, diff of the fix, screenshot of the green run after.
Pick one of the following:
a) Add a status badge in a README.md at the root of your GitHub repository (not the lab one):
It displays passing (green) or failing (red) live.
b) Create a test PR with a deliberately invalid config.json (for example "couleur_fond": "rouge"). The Verifier la configuration workflow must fail and block the merge. Provide the screenshot of the block.
c) Create a third manual workflow (workflow_dispatch:) that sends a curl notification to a webhook (URL provided via Settings → Secrets, never in clear text in the YAML).
A RAPPORT.md at the root of your repository, containing:
https://<vous>.github.io/<depot>/).on: push without specifying branches: — when does it trigger? Is that a problem?id-token: write required to publish on GitHub Pages? What is the OIDC token for?git push on main at the same time? What does the concurrency block change in the behavior?API_KEY in Settings → Secrets. The workflow code can read it via ${{ secrets.API_KEY }}. Can it be printed in the logs? Why does GitHub mask some values?deployer-pages.yml and verifier-config.yml. Is the second useless because the first does the same build? Justify why they are complementary.build.py created a historique.log file? Where should it be saved?| Item | Points |
|---|---|
Mission 1 — customized config.json and local build OK | 20 |
| Mission 2 — proof of the push → Pages cycle (2 colors + screenshots + SHAs) | 30 |
| Mission 3 — 3 outages diagnosed and repaired | 30 |
| Report quality (structure, screenshots, explanations) | 20 |
| Bonus — Mission 4 | +10 |
| Total | 100 (+10) |
Penalties:
.github/workflows/ (therefore ignored by GitHub).dist/ folder (must stay in .gitignore).outils/build.py.# Reference skeleton of a workflow
name: Human-readable description
on: # WHEN
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: # manual button
permissions: # ONLY what is necessary
contents: read
pages: write
concurrency: # do not stack runs
group: pages
cancel-in-progress: false
jobs:
mon-job:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: A command
run: python outils/build.pyThe 5 diagnostic moves that save you:
Re-run failed jobs: restarts only the failed steps, faster.workflow_dispatch: add it to your workflows so you can relaunch them manually without an empty commit.echo "::debug::mon message": prints a debug message in the logs.site/config.jsonThis is the only file you need to edit in normal operation.
{
"titre": "Mon premier site pilote par GitHub Actions",
"sous_titre": "Change une couleur, fais un push, et regarde GitHub Pages se mettre a jour tout seul.",
"couleur_fond": "#0f172a",
"couleur_texte": "#f1f5f9",
"couleur_accent": "#38bdf8",
"auteur": "Etudiant du cours AOA-DEVOPS-101",
"version": "1.0.0"
}File constraints:
couleur_fond, couleur_texte, couleur_accent) must follow the #RRGGBB format — otherwise build.py rejects them.site/src/index.html.templateHTML template where each {{MARQUEUR}} is replaced by build.py. The page will display:
config.json).config.json).GITHUB_* variables).The complete file is provided in site/src/index.html.template.
site/src/css/style.css.templateCSS template that uses {{COULEUR_FOND}}, {{COULEUR_TEXTE}}, and {{COULEUR_ACCENT}} in a :root block.
Excerpt:
:root {
--fond: {{COULEUR_FOND}};
--texte: {{COULEUR_TEXTE}};
--accent: {{COULEUR_ACCENT}};
}
body {
background: var(--fond);
color: var(--texte);
}.github/workflows/deployer-pages.ymlname: Deployer sur GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
construire:
name: Construire le site
runs-on: ubuntu-latest
steps:
- name: Recuperer le code
uses: actions/checkout@v4
- name: Installer Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Generer dist/ a partir de config.json
run: python outils/build.py
- name: Preparer le dossier a publier
uses: actions/upload-pages-artifact@v3
with:
path: dist
deployer:
name: Publier sur GitHub Pages
needs: construire
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.publication.outputs.page_url }}
steps:
- name: Publier l'artefact
id: publication
uses: actions/deploy-pages@v4Line-by-line reading: see 02-CORRECTION.md → Mission 2.
.github/workflows/verifier-config.ymlname: Verifier la configuration
on:
pull_request:
branches: [main]
push:
branches-ignore: [main]
jobs:
linter:
name: Valider config.json
runs-on: ubuntu-latest
steps:
- name: Recuperer le code
uses: actions/checkout@v4
- name: Installer Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Construire en dry-run
run: python outils/build.py
- name: Verifier que dist/ est bien genere
run: |
test -f dist/index.html
test -f dist/css/style.css
echo "OK - site construit sans erreur"outils/build.py does four things:
site/config.json.#RRGGBB format (regex).{{MARQUEUR}} in the two templates and writes the result into dist/.On GitHub Actions, the variables GITHUB_SHA, GITHUB_REF_NAME, GITHUB_RUN_NUMBER are available automatically. Locally they do not exist → the script displays local-* instead. That is how you distinguish a page really deployed by Actions from a page built locally.
casses/casse-1-permissions-manquantes.ymlThe workflow runs but the deployer job fails with:
Error: Resource not accessible by integrationQuestion to ask yourself: which entire section is missing, at the very top of the file, next to on:?
casses/casse-2-declencheur-errone.ymlA push on main never triggers this workflow. No run appears. No error.
Question to ask yourself: read the key under on:. GitHub expects push, but what is written?
casses/casse-3-chemin-artefact.ymlbuild.py succeeds and prints OK dist/index.html genere. But upload-pages-artifact fails with:
Error: Path does not exist: ./publicQuestion to ask yourself: where does build.py write the site (look at the Python code)? Which path: should therefore be set in the workflow?
Course created by Dr. Haythem REHOUMA — Development and Deployment of Data Solutions