Deployment¶
Maintainer material
This page describes how the course website is published. Students do not need it — to read the course, just visit the site.
How the site reaches GitHub Pages, what has to pass first, how to verify a deployment, and what to do when something fails.
How publishing works¶
flowchart LR
accTitle: Deployment pipeline from an edit to the published site
accDescr {
Editing a page under docs/ leads to a branch push or a pull request, which triggers the Validate
docs workflow. If the checks fail, the editor returns to the edit. If they pass, the change is
merged to main, the Deploy docs workflow builds the site, and GitHub Pages publishes it.
}
A[Edit docs/] --> B[Push branch<br/>or open a PR]
B --> C[Validate docs<br/>workflow runs checks]
C --> D{Pass?}
D -->|No| A
D -->|Yes| E[Merge to main]
E --> F[Deploy docs<br/>workflow builds]
F --> G[GitHub Pages<br/>publishes the site]
The site is built from docs/ by MkDocs and published to GitHub Pages. Nobody uploads files by hand,
no built output is committed (site/ is git-ignored), and the deployed artifact is produced by the same
build command that contributors run locally.
| Live URL | https://nadeem-majeedch.github.io/Software-Project-Management/ |
| Publishing branch | main — the workflows trigger on main only |
| Source directory | docs/ (docs_dir) |
| Output directory | site/ (site_dir, the MkDocs default) |
| Base path | /Software-Project-Management/ — set by site_url, a project-site path |
The base path is not cosmetic. site_url is what makes every canonical link, every sitemap.xml entry
and every asset path in the generated 404.html resolve under /Software-Project-Management/ rather than
at the domain root. Change it and the site still builds, but link previews and the 404 page break.
One-time repository setting¶
Publishing requires a single setting, which only a repository administrator can change:
Settings → Pages → Build and deployment → Source = "GitHub Actions"
Without it the build job succeeds and the deploy job fails, because there is no Pages site for the
artifact to be published to. If the site is not appearing, check this setting first. No workflow file can
set it, and actions/configure-pages is deliberately run without enablement: true, which would require a
token with repository-administration scope and would modify repository settings from CI.
The deployment contract¶
A deployment is a contract between four things that live in different files. Each can be edited on its
own, and a mismatch shows up as a failed run minutes after a push. tools/check-workflows.py verifies the
whole contract from the repository alone, and runs first in both workflows.
| Part of the contract | Where it lives | What breaks if it drifts |
|---|---|---|
Triggers (push: main, manual dispatch) |
deploy-docs.yml |
No deployment, or a deployment from the wrong branch |
Permissions (contents: read, pages: write, id-token: write) |
deploy-docs.yml |
The deploy job cannot authenticate to the Pages service |
Concurrency (group: pages, cancel-in-progress: false) |
deploy-docs.yml |
Concurrent runs publish out of order, or a run is cut off mid-publish |
Artifact path (path: site) |
deploy-docs.yml |
An empty or stale site is published, or the upload fails |
Output directory (site_dir) |
mkdocs.yml |
The uploaded directory is not where MkDocs wrote |
Base path (site_url) |
mkdocs.yml |
Canonical links, sitemap and 404 assets point at the wrong root |
Two further properties are checked because they are easy to lose in an edit: the build step must pass
--strict, and the output must be verified by check-build-output.py before it is uploaded. MkDocs
excludes a top-level templates/ directory by default and logs that at INFO level, so a strict build can
still be missing a whole section of the course.
Action versions¶
The workflows use first-party GitHub actions, pinned to a major version tag:
| Action | Pin | Used for |
|---|---|---|
actions/checkout |
@v7 |
Check out the repository |
actions/setup-python |
@v7 |
Python 3.12, with pip caching keyed on requirements-docs.txt |
actions/configure-pages |
@v6 |
Read the Pages configuration and expose its outputs |
actions/upload-pages-artifact |
@v5 |
Package site/ as the Pages artifact |
actions/deploy-pages |
@v5 |
Publish the artifact to Pages |
upload-pages-artifact and deploy-pages are released as a pair and should be upgraded together;
check-workflows.py warns if they drift onto different majors. A major tag is a deliberate choice: it
accepts patch and minor fixes from GitHub, which is what you want for the deployment plumbing itself.
Where a workflow is not the right place to make that trade, the repository pins strictly instead —
requirements-docs.txt bounds MkDocs below 2.0, because the Material build reports MkDocs 2.0 as
backwards-incompatible with no migration path.
If your own policy requires immutable pins, replace a major tag with the full commit SHA of the release
you want (actions/checkout@<40-hex-sha>) — check-workflows.py accepts either, and rejects a branch
reference such as @main, which can change without warning.
The build itself is verified locally with MkDocs 1.6.1 / Material for MkDocs 9.7.7. CI uses Python 3.12; 3.13 also builds the site cleanly, so a patch-level interpreter change is not a risk.
Why there are no secrets¶
Pages deployment authenticates with a short-lived OIDC token: the deploy job requests id-token: write
and the Pages service verifies the workflow's identity. No personal access token, deploy key or other
credential is needed, and none is stored. check-workflows.py fails the build if any workflow
references secrets., and check-public-content.py scans the documentation for credential-shaped
strings.
The two workflows¶
| Workflow | Trigger | Purpose |
|---|---|---|
.github/workflows/validate-docs.yml |
Pull requests, and pushes to non-main branches |
Runs every check, a strict build, and the build-output check. Catches problems before merge |
.github/workflows/deploy-docs.yml |
Pushes to main, or manual dispatch |
Runs the same checks, builds the site, and publishes it |
Both run the same steps, in the same order, so a change that passes on a branch will build on main.
There is no "works locally, fails in CI" class of problem, provided you ran the local checks described in
Local Development. Both jobs also carry a timeout-minutes bound, so a hung step
fails the run instead of occupying a runner indefinitely.
What must pass before publishing¶
The deploy workflow stops if any of these fail:
python tools/check-workflows.py— the deployment contract above, including the artifact path andsite_url.python tools/validate-structure.py— no broken internal links, no missingnaventries, no naming violations.python tools/check-curriculum.py,check-lessons.py,check-labs.py,check-templates.py,check-capstone.py— the content still meets its standards and the session plan is intact.python tools/spm-calculators.py --check— every formula still reproduces the worked example published in the course text.python tools/check-public-content.py— instructor pages labelled, no credentials committed, and the published status of the answer keys stated accurately.python -m mkdocs build --strict— the site builds with zero warnings.python tools/check-build-output.py— every source page is present in the built site.
Step 7 is not decoration. MkDocs silently excludes a top-level templates/ directory inside the docs
directory and logs that as an INFO message, so --strict can pass while the published site is missing a
whole section. Reading the output directory is the only way to catch it. See
Local Development.
A failed step simply does not publish. The previous version of the site stays live, which is the intended safety property: the published site is never broken, only out of date.
Deploying manually¶
The deploy workflow can be started by hand from Actions → Deploy docs → Run workflow. This is useful
after changing a repository setting, or to republish without a new commit. A manual run publishes the
current state of main, so commit first if you want a change included.
Verifying a deployment¶
A green workflow is necessary but not sufficient, and a workflow file cannot tell you whether the published page renders. After a deployment, check the live URL in this order:
| # | Check | What a failure looks like |
|---|---|---|
| 1 | The homepage loads at the live URL | A 404 page, or a directory listing |
| 2 | A stylesheet is applied | Unstyled text — check the page source for a 200 response from assets/stylesheets/main.*.min.css |
| 3 | The sidebar shows all six top-level sections, including Reference and Instructors | A truncated nav means the build published stale output |
| 4 | A deep page opens directly, e.g. /course-content/01-foundations/lessons/01-what-is-software-project-management/ |
A 404 on a deep path usually means the wrong base path |
| 5 | A template page opens, e.g. /templates/project-charter/ |
This is the section MkDocs excludes by default — if it 404s, exclude_docs has been removed from mkdocs.yml |
| 6 | An internal link from a lab to a lesson works | A dead link that the local build reported as fine |
| 7 | The search box returns results | A missing search/search_index.json |
| 8 | A deliberately wrong path (e.g. /no-such-page/) shows the themed 404 page |
A bare GitHub 404 means 404.html was not published with base-prefixed assets |
What cannot be verified from a local checkout: whether a real deployment succeeded. That requires
either repository access or the workflow to have run on GitHub. The workflows here are validated for
syntax and internal consistency (check-workflows.py), and the build they run is reproducible locally
with the same commands and pinned dependencies — but "the site is live and correct" is a claim only the
live URL can support. Do not state that deployment works until those checks have been performed against
the published site.
External runtime dependencies¶
The built site is static, but two things are fetched from third-party CDNs at page-view time:
| Dependency | Source | Consequence if blocked |
|---|---|---|
| Roboto and Roboto Mono fonts | fonts.googleapis.com |
The site renders in the fallback system font — readable, just not styled as designed |
| Mermaid, for the 19 diagrams | unpkg.com/mermaid@11 |
Diagrams do not render; the surrounding prose still reads correctly, but the visual explanation disappears |
That is a real limitation on restricted or offline networks, and it is the one part of the student
experience this repository does not control. If the course is likely to be used on such a network, the fix
is to self-host both: place the font files under docs/assets/ and disable theme.font, and add a local
Mermaid bundle through extra_javascript with a matching custom_fences entry. That change adds roughly a
megabyte to the site and needs testing against the theme's version, so it should be made deliberately
rather than as an aside.
Everything else is self-contained: icons are inlined as SVG, the search index is built into the site, and there are no analytics, tracking scripts or runtime services.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Workflow succeeds but the site 404s | Pages source is not set to GitHub Actions | Settings → Pages → Source = GitHub Actions |
| Deploy job fails with a permissions error | Missing pages: write or id-token: write |
Check the permissions block in deploy-docs.yml |
| Upload step fails: no files found | The artifact path does not match site_dir |
Run python tools/check-workflows.py |
| Deploy job fails with "artifact not found" | The upload step did not run, or built with a different job | The deploy job must needs: the build job that uploads |
| Site publishes with unstyled pages or 404ing assets | site_url does not match the published path |
Correct site_url in mkdocs.yml; it must be the project-site URL |
| Build fails on a link | A relative link points at a page that does not exist or was renamed | Fix the link, or add the file to nav |
| A page exists in the source but is not on the site | It is not listed in nav, or it is excluded by MkDocs' default patterns |
Add it to mkdocs.yml; check exclude_docs if it lives under a templates/ directory |
| A runner warning about a deprecated Node runtime | An action pinned to an older major | Update the pin to the current major and re-run check-workflows.py |
| Site did not update after merging | The deploy workflow did not run | Check the Actions tab; re-run it manually if needed |
| Site shows stale content | Browser or CDN cache | Hard-refresh; the site is rebuilt on every push to main |
| Curriculum check fails after editing the roadmap | Master calendar and coverage index disagree | See the tips below |
When the curriculum check fails¶
check-curriculum.py protects the 32-session plan. The usual causes:
- A session number changed. Session numbers are derived from position — two per week for 16 weeks. Renumber the master calendar and update the coverage index together.
- A topic moved. Update both the master calendar and the coverage index row; the check compares them.
- A new module was inserted. Module sessions must stay contiguous and in ascending order.
- An objective is not observable. Rewrite it to start with an action verb such as explain, apply, construct, evaluate, or justify.
- A prerequisite points forward. A module may only depend on earlier modules — if it needs later material, the sequence is wrong.
Rolling back¶
To undo a published change, revert the commit on main. The deploy workflow re-runs and republishes the
previous content. Because site/ is not committed, there is nothing else to clean up.
Keeping the site lightweight¶
The site is a static documentation build on purpose — no application, no runtime services, and no analytics. It does load fonts and the Mermaid renderer from CDNs (see above), which is the one exception and is documented rather than hidden. If you propose a change that adds a further dependency, justify it: the course is meant to load quickly on a slow connection and be readable by anyone.
See Contributing for the contribution process and the accuracy rules.