The pages job is green and the URL is a 404
pages: success in the pipeline. https://group.gitlab.io/project/
returns 404 Not Found.
Work down this list — the first two account for most of them.
1. The report is not in public/
GitLab Pages publishes one directory: public/ at the repository
root, declared as the job's artifact. Not dist/, not htmlcov/, not
public/ nested inside a subproject.
pages:
stage: deploy
script:
- coverage html # writes htmlcov/
- mv htmlcov public # ← the step people omit
artifacts:
paths:
- public # ← must be exactly this
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Miss the mv and the job still passes. Miss the artifacts: paths and
it also passes. Pages just has nothing to publish.
2. It has not deployed yet
The Pages deploy runs after the job, as a separate step. A minute or three between green pipeline and live site is normal. Check Deploy → Pages for the deployment's own status before assuming breakage.
3. index.html is not at the root of public/
Pages serves public/index.html at /. A report that lands as
public/htmlcov/index.html needs /htmlcov/ in the URL. Directory
listings are off by default, so the root is a 404 rather than a helpful
index.
4. Another branch overwrote it
One project, one Pages site, published by whichever pipeline ran the
pages job last. Two branches both running it means the second wins
silently. The usual workaround is a per-branch subdirectory under
public/, which then needs a cleanup job nobody writes — so the site
grows until someone notices.
5. Access control
If the project is private, the site is private only when Pages access control is enabled — a per-instance setting on self-managed GitLab that is often off, in which case the "internal" report is public. And it is all-or-nothing: project members or the world. There is no "this one report, these three people".
6. .gitlab-ci.yml rules skipped the job
rules: or only: restricted to the default branch means feature
branches never publish, which is exactly when a preview is wanted.
The artifact browser is not the fallback
GitLab's artifact browse view can serve a single HTML file, but multi-file reports — coverage trees, Allure, Playwright — break on its path handling and policy rules. And artifacts expire: 30 days by default, sooner when "keep artifacts from most recent successful jobs" deletes superseded pipelines regardless of age. Every link you paste has an expiry date attached.
What Pages is actually good for
One canonical site per project, published from the default branch, public. Docs sites fit that perfectly.
Per-pipeline reports do not: they are many, they are per-branch, they are frequently private, and they exist to be argued about. Publishing each run to a report URL removes the whole list above —
publish_report:
stage: deploy
when: always # a red pipeline needs it more
script:
- >
curl -sS -X PATCH "https://commareports.com/api/v1/reports/$COMMA_REPORT_ID"
-H "Authorization: Bearer $COMMA_API_TOKEN"
-H "Content-Type: application/json"
--data "$(jq -Rs '{html: .}' < htmlcov/index.html)"
— with one id per report, a revision appended per pipeline, per-report visibility, and no expiry. Full setup in GitLab CI HTML reports and publish from CI.
Try it
Comma is free — unlimited reports, unlimited commenters, unlimited revision history.
Publish a report from GitLab CI →