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.
- 1Give the portfolio something to check
- 2Your first workflow
- 3Watching it run
- 4Protect main with a ruleset
- 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:
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 testnode_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:
index.html
1:1 error DOCTYPE should be uppercase doctype-styleYour first workflow
GitHub Actions runs workflows defined in YAML files under .github/workflows/. Create one:
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 testRead it top to bottom:
onlists the events that start the workflow: any pull request intomain(opened, or updated with new commits), and pushes tomainafter merging.jobsholds one job,check. That id becomes the name of the status check GitHub shows on the pull request.runs-onpicks a runner, a fresh virtual machine GitHub provides. It starts empty every time.stepsrun in order.usesruns a published action:actions/checkoutcopies your repository onto the runner,actions/setup-nodeinstalls Node.js 24 and caches npm downloads.runexecutes a shell command:npm ciinstalls exactly whatpackage-lock.jsonlists, andnpm testruns 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.
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:
- Name it “Protect main” and set Enforcement status to Active.
- Under Target branches, add the default branch.
- Keep Restrict deletions and Block force pushes ticked.
- 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.
- Tick Require status checks to pass, add the check
check, and save.
Now try to skip the process:
git switch main
git commit --allow-empty -m "Sneak a commit onto main"
git push origin mainremote: 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/;onpicks the events,jobsrun on runners, andstepseitherusesan action orruns 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
maincan 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.