GitHub Actions Mission: Publish a Site on GitHub Pages with One Push

14 min

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.


Table of contents


The context

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:

  1. Take ownership of the delivered project (git clone + push + Pages activation).
  2. Customize config.json with your information and your colors.
  3. Prove that the push → Pages cycle works by changing the color at least twice.
  4. Repair three broken workflows that do not start from scratch but illustrate the 3 most common CI/CD mistakes.

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.


Essential concepts before you start

This document is self-contained. You do not need to open any external lesson to finish it.

1. What is GitHub Actions?

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).

2. Anatomy of a YAML workflow

yaml
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" step

Two 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.

3. The 4 triggers you must know

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).

4. What is GitHub Pages?

GitHub Pages is static hosting included with every public repository. You publish HTML/CSS/JS through one of these methods:

  • From a gh-pages branch (older method).
  • From a /docs folder on main (another method).
  • From a GitHub Actions workflow (the modern method, the one used in this project).

The public URL is https://<utilisateur>.github.io/<nom-du-depot>/ — reachable from any browser.

5. The official trio of actions for Pages

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:

yaml
permissions:
  contents: read
  pages: write
  id-token: write

That is also outage 1 of mission 3 — remember it.

6. Automatic variables provided by the runner

On every run, GitHub Actions exposes useful environment variables:

VariableContent
GITHUB_SHAThe SHA of the commit that triggered the run
GITHUB_REF_NAMEThe branch name (main, feature-x, and so on)
GITHUB_RUN_NUMBERA 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.


Target architecture

Expected end-to-end loop — you must reproduce it at least 2 times:

  1. Open site/config.json in VS Code.
  2. Change couleur_fond (for example #0f172a#7c3aed).
  3. git add site/config.json && git commit -m "changement de fond" && git push.
  4. Go to the repository Actions tab → watch the workflow run live (~40 s).
  5. Once everything is green, open https://<vous>.github.io/<depot>/the background is purple.

File layout

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.


The rules of the game

  1. You do not modify outils/build.py — it is the contract between config.json and the final HTML/CSS.
  2. You do not push the dist/ folder: it is listed in .gitignore and rebuilt on every run.
  3. Any sensitive data (passwords, API keys) goes in Settings → Secrets and variables → Actions, never in a YAML file.
  4. Every color change must go through a commit — no manual edit of the published site.
  5. The repository must be public (GitHub Pages on private repositories requires a paid plan).

Preparation

Prerequisites

  1. Git installed (git --version).
  2. Python 3.10+ installed (python --version — to test build.py locally).
  3. A GitHub account.
  4. This projet14-github-actions-pages-tp/ folder on your disk.

Step 0 — Create your GitHub repository

  1. On GitHub, click New repository.
  2. Suggested name: projet14-actions-pages.
  3. Visibility: Public (required for free GitHub Pages).
  4. Without a README or .gitignore (already provided).

Step 1 — Test the build locally

From the project root:

powershell
python .\outils\build.py

You 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.

Step 2 — Initialize the local repository and push

powershell
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 main

Step 3 — Enable GitHub Pages in Actions mode

  1. On GitHub, open the repository → SettingsPages.
  2. Under Source, choose GitHub Actions (and not Deploy from a branch).
  3. Save.

Step 4 — Watch the first deployment

Actions 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.


The missions

Mission 1 — Customize 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.


Mission 2 — Prove the push → Pages loop (30 points)

This is the heart of the project. You must demonstrate that the CI/CD cycle works by triggering it at least twice:

  1. Edit site/config.json (change couleur_fond for example to #dc2626 — red).
  2. git add site/config.json && git commit -m "fond en rouge" && git push.
  3. Wait until the Deployer sur GitHub Pages workflow turns green in the Actions tab (~40 s).
  4. Open https://<vous>.github.io/<votre-depot>/the background is red.
  5. Repeat the operation with another color (green #16a34a, blue #0891b2, purple #7c3aed, your choice).

Proof to include in the report:

  • Two screenshots of the public page with two different colors.
  • The two matching commit SHAs (output of git log --oneline).
  • One screenshot of the Actions tab showing the two green runs.

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.


Mission 3 — Investigation: repair 3 defective workflows (30 points)

The casses/ folder contains three defective workflows, each illustrating a classic mistake. For each one:

  1. Copy the file into .github/workflows/ (next to the official workflows).
  2. Push — observe that the workflow does not behave as expected (failure or no trigger).
  3. Diagnose by reading the logs (Actions tab → run → step).
  4. Repair in the copy under .github/workflows/ (not in casses/ — the original stays broken).
  5. Push again — the workflow must now turn green.
FileExpected symptom
casse-1-permissions-manquantes.ymlThe deployer job fails with Resource not accessible by integration
casse-2-declencheur-errone.ymlNo run is triggered: the workflow is silently ignored
casse-3-chemin-artefact.ymlThe 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.


Mission 4 — Bonus (+10 points)

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):

    markdown
    ![Deploy](https://github.com/<vous>/<depot>/actions/workflows/deployer-pages.yml/badge.svg)

    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).


Deliverables

A RAPPORT.md at the root of your repository, containing:

  1. The public URL of your site (https://<vous>.github.io/<depot>/).
  2. For Mission 2: two screenshots of the page with two different colors + the two SHAs + a screenshot of the Actions tab with the green runs.
  3. For Mission 3: for each outage, screenshot of the red run + written diagnosis + diff of the fix + screenshot of the green run.
  4. Your answers to the reflection questions.
  5. (Bonus) The proof of mission 4 if you completed it.

Reflection questions

  1. A workflow that contains on: push without specifying branches: — when does it trigger? Is that a problem?
  2. Why is id-token: write required to publish on GitHub Pages? What is the OIDC token for?
  3. What happens if two colleagues run git push on main at the same time? What does the concurrency block change in the behavior?
  4. You add a secret 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?
  5. You have two workflows: deployer-pages.yml and verifier-config.yml. Is the second useless because the first does the same build? Justify why they are complementary.
  6. A GitHub Actions runner is an ephemeral VM: nothing persists between two runs. What consequence does that have if your build.py created a historique.log file? Where should it be saved?

Grading

ItemPoints
Mission 1 — customized config.json and local build OK20
Mission 2 — proof of the push → Pages cycle (2 colors + screenshots + SHAs)30
Mission 3 — 3 outages diagnosed and repaired30
Report quality (structure, screenshots, explanations)20
Bonus — Mission 4+10
Total100 (+10)

Penalties:

  • −10 per workflow located outside .github/workflows/ (therefore ignored by GitHub).
  • −15 for any secret in clear text in a committed YAML file.
  • −5 per committed dist/ folder (must stay in .gitignore).
  • −10 for any modification of outils/build.py.

GitHub Actions toolbox

yaml
# 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.py

The 5 diagnostic moves that save you:

  1. Actions tab → red run → failed step: the error message is at the top, in red. Reading it literally avoids 90 % of mistakes.
  2. Re-run failed jobs: restarts only the failed steps, faster.
  3. workflow_dispatch: add it to your workflows so you can relaunch them manually without an empty commit.
  4. echo "::debug::mon message": prints a debug message in the logs.
  5. Read the logs all the way through: sometimes the meaningful error is in the middle, not at the end.


ANNEX A — The static site

File: site/config.json

This is the only file you need to edit in normal operation.

json
{
  "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:

  • The 7 keys are required.
  • The 3 colors (couleur_fond, couleur_texte, couleur_accent) must follow the #RRGGBB format — otherwise build.py rejects them.
  • Valid JSON: double quotes, a comma after each field except the last.

File: site/src/index.html.template

HTML template where each {{MARQUEUR}} is replaced by build.py. The page will display:

  • The title and the subtitle (from config.json).
  • The author and the version (from config.json).
  • The commit SHA, the run number, and the branch (from the runner GITHUB_* variables).
  • The build date.

The complete file is provided in site/src/index.html.template.

File: site/src/css/style.css.template

CSS template that uses {{COULEUR_FOND}}, {{COULEUR_TEXTE}}, and {{COULEUR_ACCENT}} in a :root block.

Excerpt:

css
:root {
  --fond: {{COULEUR_FOND}};
  --texte: {{COULEUR_TEXTE}};
  --accent: {{COULEUR_ACCENT}};
}

body {
  background: var(--fond);
  color: var(--texte);
}


ANNEX B — The provided workflows

File: .github/workflows/deployer-pages.yml

yaml
name: 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@v4

Line-by-line reading: see 02-CORRECTION.md → Mission 2.

File: .github/workflows/verifier-config.yml

yaml
name: 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"


ANNEX C — The build script

outils/build.py does four things:

  1. Reads site/config.json.
  2. Validates that the 7 expected keys are present.
  3. Validates that the 3 colors are in #RRGGBB format (regex).
  4. Substitutes every {{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.



ANNEX D — The three outages to repair

File: casses/casse-1-permissions-manquantes.yml

The workflow runs but the deployer job fails with:

Error: Resource not accessible by integration

Question to ask yourself: which entire section is missing, at the very top of the file, next to on:?

File: casses/casse-2-declencheur-errone.yml

A 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?

File: casses/casse-3-chemin-artefact.yml

build.py succeeds and prints OK dist/index.html genere. But upload-pages-artifact fails with:

Error: Path does not exist: ./public

Question 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