EduFlow
Git & GitHub
0%

Section 4 · Professional Habits

Guarding main: CI with Actions and Rulesets

17 min4 min readSpot the bug

Lesson at a glance

Add a GitHub Actions workflow that checks every pull request, then protect main with a ruleset so changes can only land through a pull request whose checks pass.

  1. 1Give the portfolio something to check
  2. 2Your first workflow
  3. 3Watching it run
  4. 4Protect main with a ruleset
  5. 5What you can do now

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Everything so far has relied on you remembering. Remember to run the HTML check, to open a pull request rather than pushing to main, to never force-push. People forget, especially at 11 p.m. before a job application. This lesson moves those habits out of your head and into GitHub: a machine runs the checks on every pull request, and rules make main impossible to change any other way.

Give the portfolio something to check

Continuous integration needs a command that fails when something is wrong. For an HTML site, a validator is a good start. In your portfolio, on a new branch:

Terminal
git switch -c add-ci
npm init -y
npm install --save-dev html-validate
npm pkg set scripts.test="html-validate index.html projects.html"
echo '{ "extends": ["html-validate:recommended"] }' > .htmlvalidate.json
npm test

node_modules/ is already in your .gitignore from two lessons ago; commit package.json, package-lock.json and .htmlvalidate.json. If npm test reports problems, that’s the point: fix them, or note them for the first CI run. On a fresh portfolio it often finds this:

Text
index.html
  1:1  error  DOCTYPE should be uppercase  doctype-style

Your first workflow

GitHub Actions runs workflows defined in YAML files under .github/workflows/. Create one:

.github/workflows/ci.yml
name: CI

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm test

Read it top to bottom:

  • on lists the events that start the workflow: any pull request into main (opened, or updated with new commits), and pushes to main after merging.
  • jobs holds one job, check. That id becomes the name of the status check GitHub shows on the pull request.
  • runs-on picks a runner, a fresh virtual machine GitHub provides. It starts empty every time.
  • steps run in order. uses runs a published action: actions/checkout copies your repository onto the runner, actions/setup-node installs Node.js 24 and caches npm downloads. run executes a shell command: npm ci installs exactly what package-lock.json lists, and npm test runs the validator.

Any step that exits with an error fails the job. Commit the workflow, push the branch and open a pull request.

Watching it run

Within seconds the pull request shows a check, “CI / check (pull_request)”, with a yellow dot while it runs. Then a green tick, or a red cross. Click Details to see the full log of every step; the failing one is expanded, showing the same validator output you’d get locally.

To fix a red check, do what you’d do for review feedback: commit on the same branch and push. The workflow runs again on the new commit. Get used to reading these logs. CI isn’t judging you; it’s a colleague who reruns the checks every single time without getting bored.

A left-to-right flow: feature branch, push, pull request, then two gates side by side, CI check passes and review approves, then merge into main. A blocked arrow shows direct pushes to main being rejected by the ruleset. branch pull request CI: check review merge main direct push: rejected
The protected path to main

Protect main with a ruleset

A passing check is only useful if merging waits for it. That’s what a ruleset enforces. On GitHub, open the repository’s Settings, then Rules, then Rulesets, and choose New ruleset, New branch ruleset:

  1. Name it “Protect main” and set Enforcement status to Active.
  2. Under Target branches, add the default branch.
  3. Keep Restrict deletions and Block force pushes ticked.
  4. Tick Require a pull request before merging. On a solo repository set required approvals to 0 (you can’t approve your own pull request); on a team, 1 or more.
  5. Tick Require status checks to pass, add the check check, and save.

Now try to skip the process:

Terminal
git switch main
git commit --allow-empty -m "Sneak a commit onto main"
git push origin main
Text
remote: error: GH013: Repository rule violations found for refs/heads/main.

The push is refused, and your local commit is untouched. Every change to main now goes through a pull request, and the merge button stays disabled until the check is green. Run git reset --hard origin/main to drop the empty commit.

Rulesets are free on public repositories; on private ones they need a paid plan such as GitHub Pro or Team. You may also see the older branch protection rules in settings. They do a similar job; rulesets are newer, can be layered, and anyone with read access can see which rules apply.

What you can do now

Look at where the portfolio started: a folder with index.html. It’s now a repository with readable history, branches that merge or rebase cleanly, a GitHub remote you reach with proper authentication, issues that explain why work exists, tagged releases, and a main that only accepts reviewed, automatically checked pull requests. You can undo almost any mistake, and you know which undo fits.

More than the commands, you have the model: commits are snapshots linked to parents, branches and tags are labels, HEAD is where you stand, and remotes are other copies of the same graph. When Git surprises you in future, run git log --oneline --graph --all, draw what you see, and the next command will usually be obvious.

Key takeaways

  • A workflow is a YAML file in .github/workflows/; on picks the events, jobs run on runners, and steps either uses an action or runs a command.
  • Each job reports a status check on the pull request, shown as a green tick or a red cross with full logs.
  • A branch ruleset on main can block force pushes and deletions, require a pull request, and require named status checks to pass.
  • A required status check is matched by job name, and GitHub only suggests checks that have already reported, so run the workflow before requiring it.
  • The finished loop is branch, push, pull request, automated check, review, merge, and nobody can skip a step.

Further reading

Check your understanding

4 questions

Question 1 of 4

Your workflow starts with on: pull_request. When does it run?

Question 2 of 4

Your CI job fails at npm ci with an error that package.json can’t be found. The first step is run: npm ci. What’s wrong?

Question 3 of 4

You’ve added a ruleset on main requiring a pull request. What happens when you run git push origin main with a local commit?

Question 4 of 4

You renamed your workflow job from check to html. Now every pull request shows the required check check as “Expected — Waiting for status to be reported”, and nothing can merge. Why?

Practice

Spot the bug

Debug the CI workflow

A teammate wrote .github/workflows/ci.yml to run the portfolio’s HTML check on every pull request into main. It never appears on pull requests, and once that’s fixed it would still fail. Select the three lines that are wrong.

Select the line(s) you think are wrong, then check.