Product
CI/CD integration
One more job in your pipeline, with no change to your habits
Contents
Three steps in your pipeline
Grammage runs in a Docker container, so it takes its place in your pipeline like any other step, and nobody has to think about it afterwards since it starts by itself on every merge request.
Every merge request is audited
Grammage measures the preview of the request, or the site it builds itself in the job, and publishes its report right inside the request, so that everyone sees what the change costs before approving it.
The pipeline fails below your threshold
You set the minimum grade, a letter like C or a score out of 100, and a request that would bring the site below it flags it, or does not go to production at all if you decide so.
Production is measured and attested
After the deployment, a second job measures the public address, signs its report and sends it to Grammage, which checks it and then keeps your badge alive.
Install it in GitLab CI
The ci/grammage.gitlab-ci.yml template carries three ready-made jobs, which you take in with an include and then tune with a few variables. The first audits the preview of the merge request, the second reads the source code of the repository, and the third, .grammage-certify, has production verified further down this page.
include:
- project: solyzon/products/grammage/grammage
ref: prod
file: ci/grammage.gitlab-ci.yml
grammage:
extends: .grammage
needs: [preview]
variables:
GRAMMAGE_CATEGORY: auto
GRAMMAGE_FAIL_UNDER: C
grammage-source:
extends: .grammage-sourceneeds: [preview] brings in PREVIEW_URL, the address of the preview published by the job that deploys it. To audit another address, GRAMMAGE_URL replaces it, and your licence arrives through the masked variable GRAMMAGE_LICENSE, which you set once for the whole group.
Without waiting for the preview
The job can also build your site and start it itself, which avoids waiting for the preview and its password. GRAMMAGE_SERVE holds the command that builds then starts the site, and the job waits for it to answer before placing a small proxy in front of it, which compresses responses in zstd or gzip as your production server would. Without this proxy, a Node server that compresses nothing would make the site look much heavier than it really is online.
grammage:
extends: .grammage
variables:
GRAMMAGE_SERVE: pnpm install --frozen-lockfile && pnpm exec astro build && HOST=127.0.0.1 PORT=4321 node dist/server/entry.mjs
GRAMMAGE_SITE: '1'
GRAMMAGE_FAIL_UNDER: AIf the site never answers, the job fails and shows the log of the command, so that you see right away what got stuck.
The whole site and the duration of the job
With GRAMMAGE_SITE, the job no longer measures one page but the whole site, three pages per template and fifty at most. A pass takes about thirty seconds on average on a real site, twenty-four passes in a little over twelve minutes in our September 2026 tests, so the duration of the job follows directly the number of pages, the number of passes and the number of pages measured at the same time. Translations of the same page are measured only once, and the fast mode also skips the return visit, which saves 10 to 15% per page without changing the score.
| Mode | Passes | Used for |
|---|---|---|
fast | 1 | a first look, and the job of every merge request |
normal | 2 | the default setting |
full | 3 | the most stable measurement, for a scheduled job or a result you publish |
The calculation is therefore simple, about thirty seconds times the number of pages times the number of passes, divided by GRAMMAGE_PARALLEL, plus the discovery of the site. These are estimates drawn from that average, and a very heavy page or a slow server makes them longer.
| Setting | Pages | Passes | At the same time | Estimated duration |
|---|---|---|---|---|
| Every merge request | 10 | 1 | 3 | ≈ 2 min |
| Default setting | 50 | 2 | 2 | ≈ 25 min |
| Full scheduled job | 50 | 3 | 4 | ≈ 20 min |
| Full, page after page | 50 | 3 | 1 | ≈ 75 min |
grammage:
extends: .grammage
needs: [preview]
variables:
GRAMMAGE_SITE: '1'
GRAMMAGE_MODE: fast
GRAMMAGE_PARALLEL: '3'
GRAMMAGE_MAX_PAGES: '10'The runner
The image starts from Node 24 and only ships a windowless Chromium, to stay light on runners. Each page measured at the same time opens its own Chromium, so GRAMMAGE_PARALLEL must never exceed the number of cores the runner gives to the job, and it is capped at 8 anyway. Beyond that, the browsers fight over the processor, which changes neither the weight, nor the requests, nor the score, but distorts the computing time shown in the report. For a measurement you publish, the full mode with few pages at the same time therefore remains the most reliable, and it is better kept for a scheduled job.
What the merge request shows
The job stores its results where GitLab already knows how to display them, so you read the audit without leaving the merge request, and each finding points to the file of the repository to fix, an image public/images/hero.png served under an address transformed by the framework for example.
| Where | File | What you read there |
|---|---|---|
| Tests tab | grammage-junit.xml | one rule per case and one page per group |
| Code Quality widget | gl-code-quality-report.json | each rule that is not followed, with its fix, annotated on the file of the repository when Grammage finds it |
| Metrics widget | metrics.txt | the score, the weight, the requests, the DOM, the carbon and the unused code, compared with the target branch |
| Artifacts | grammage/ | the HTML and PDF report, the JSON, the grammage-agent.md brief and the RGESN helper |
Block or warn
The job exits with 1 when the threshold or the budget is not met, and with 2 when a page could not be measured. By default, it warns without blocking on a missed threshold, whereas an impossible measurement does make the pipeline fail, since nothing is known about the site any more. For a failed audit to really prevent a release to production, you only need to make it mandatory and make the deployment wait for it.
grammage:
allow_failure: false
deploy:
needs: [grammage]Restricted pages
A preview closed by an HTTP password is audited with GRAMMAGE_HTTP_AUTH. A page behind a login form is audited with the session of a test account without special rights, which you record once on your machine with npx playwright codegen --save-storage=session.json, then paste into a masked file-type variable, GRAMMAGE_STORAGE_STATE. The session eventually expires, and a page that then redirects to the login is measured as such, so you redo it the same way as soon as the audited pages are no longer the right ones.
Only the audit receives the session. The attestation only covers public pages, because Grammage then measures them again without any session to check your report.
Attest production
The .grammage-certify job runs right after the deployment. It measures the public address, signs the report with the key carried by your licence and sends it to the Grammage API, then waits for the verdict, twenty minutes at most, while Solyzon measures the entry page and two randomly chosen pages again. The domain must be part of your licence, and the licence must include the badge.
grammage:certify:
extends: .grammage-certify
needs: [deploy]
rules:
- if: $CI_COMMIT_BRANCH == "prod"
variables:
GRAMMAGE_URL: https://example.com/
GRAMMAGE_SITE: '1'
GRAMMAGE_CATEGORY: autoWhen the Solyzon figures confirm your report, the job shows the end date of the attestation and the badge is updated. When a page weighs more than 20% more than declared, or when a chosen page could not be measured again, the job fails and says why, and the badge keeps the previous attestation as long as it is valid. Without GRAMMAGE_FAIL_UNDER or a budget, the site is verified whatever its score, which makes it possible to display an honest badge even when the site is still heavy. The detail of the verification is on the attestation page.
The variables of the template
The audit and attestation jobs read the same variables. The verification always covers a page or the whole site, never a list, since the attestation is valid for a page or for a domain, so GRAMMAGE_URLS_FILE and GRAMMAGE_SERVE are only used for the audit.
| Variable | Default | Role |
|---|---|---|
GRAMMAGE_URL | PREVIEW_URL | the starting page, the preview of the merge request by default |
GRAMMAGE_SITE | any non-empty value measures the whole site from the address | |
GRAMMAGE_MAX_PAGES | 50 | the number of pages at most when GRAMMAGE_SITE is set |
GRAMMAGE_CATEGORY | showcase | the grading category, or auto to infer it from each page |
GRAMMAGE_FAIL_UNDER | the minimum grade, a letter from A to F or a score out of 100 | |
GRAMMAGE_BUDGET | a budget.json file of the repository, held like the threshold | |
GRAMMAGE_DEVICE | mobile | mobile or desktop |
GRAMMAGE_MODE | normal | fast, normal or full, that is 1, 2 or 3 passes per page |
GRAMMAGE_RUNS | a number of passes per page, instead of the mode | |
GRAMMAGE_PARALLEL | 2 | the number of pages measured at the same time, from 1 to 8 |
GRAMMAGE_SERVE | the command that builds and starts the site in the job, audit only | |
GRAMMAGE_SERVE_URL | http://127.0.0.1:4321 | the address where the site started by GRAMMAGE_SERVE answers |
GRAMMAGE_SERVE_TIMEOUT | 300 | the seconds given to the started site to answer |
GRAMMAGE_URLS_FILE | a file of the repository that lists other pages, one per line, audit only | |
GRAMMAGE_HTTP_AUTH | user:password of a protected preview, as a masked variable | |
GRAMMAGE_STORAGE_STATE | a Playwright session, as a file-type variable, for restricted pages | |
GRAMMAGE_COOKIES | cookies in Playwright format, instead of the session | |
GRAMMAGE_LICENSE | your licence, as a masked variable set once for the whole group | |
GRAMMAGE_REGISTRY | registry.solyzon.net | the registry of the image |
GRAMMAGE_VERSION | the template’s own | the version of the image, or the tag in your name that follows the updates |
The command line
Everything the jobs do goes through the grammage command, which you run the same way on a machine, in the Docker image. grammage audit measures, grammage certify measures then records the result, grammage source reads the code of the repository, and grammage license verify tells what your licence covers, which is read from GRAMMAGE_LICENSE. Finally grammage proxy serves a site started in the CI by compressing it in zstd or gzip, as a real server would, so that the measurement sees the same bytes as in production, and this is in fact what the GitLab template does with GRAMMAGE_SERVE.
grammage proxy http://127.0.0.1:4321 --port 8080| Option | Role |
|---|---|
--site <url> | the whole site, through its sitemap, or by following its links otherwise |
--urls <file> | one address per line, # to comment |
--max-pages <n> | the pages of the site at most, 50 by default |
--per-template <n> | the pages measured per template, 3 by default |
--exclude <pattern> | the pages left out, /admin/* for example, repeatable option |
--device mobile|desktop | the simulated device, mobile by default |
--mode <mode> | fast, normal or full, full by default on a machine |
--runs <n> | a number of passes per page, instead of the mode |
--parallel <n> | the pages measured at the same time, from 1 to 8 |
--category <category> | presentation, documentation, showcase, editorial, ecommerce, application or auto |
--page-type home|inner | the type of page, home or inner page, inferred from the address by default |
--audience <country> | the country of the audience for the local estimate, FR by default |
--idle <seconds> | an idle period after the measurements, to see what a page left open costs |
--format <list> | json, html, pdf, agent, rgesn, markdown, junit, codequality, metrics or gitlab |
--out <folder> | where to write the reports, the current folder by default |
--source <folder> | the repository of the site, to link each offending file to its source |
--budget <file> | caps per page in JSON, bytes, requests, dom, co2PerView, score and rules |
--fail-under <grade> | the minimum grade, from A to F, or a score from 0 to 100 |
--http-user, --http-password | the credentials of a protected preview |
--cookies, --storage-state | a Playwright session, for a restricted page |
| Exit code | What it means |
|---|---|
0 | everything is fine |
1 | the budget or the minimum grade is not met |
2 | a page could not be measured, or an argument is invalid |
Other CI platforms
The ready-to-include template is written for GitLab CI, which also displays the reports in the merge request. Elsewhere, Grammage remains a Docker image and a command, so the job is rewritten in a few lines, and the reports are collected in the output folder.
- GitLab CIready-to-include template
- GitHub Actions
- Bitbucket Pipelines
- Jenkins
- Docker
jobs:
grammage:
runs-on: ubuntu-latest
container:
image: registry.solyzon.net/solyzon/products/grammage/grammage:<your-tag>
credentials:
username: ${{ secrets.GRAMMAGE_USER }}
password: ${{ secrets.GRAMMAGE_LICENSE }}
env:
GRAMMAGE_LICENSE: ${{ secrets.GRAMMAGE_LICENSE }}
steps:
- run: grammage audit --site https://example.com/ --mode fast --format json,html,agent --out grammagedocker login registry.solyzon.net
docker run --rm -e GRAMMAGE_LICENSE -v "$PWD/grammage:/out" \
registry.solyzon.net/solyzon/products/grammage/grammage:<your-tag> \
grammage audit --site https://example.com/ --format json,html --out /outThe image and the licence
The image is pulled from registry.solyzon.net, with your user name and your licence as the password, which goes into GitLab through the masked variable DOCKER_AUTH_CONFIG of your project. The registry only serves you the tag in your name and the versions your licence covers, and this tag moves forward by itself with every new version, never backwards, so a project that follows it receives the updates without touching anything.
{ "auths": { "registry.solyzon.net": { "auth": "<base64 of user:licence>" } } }Without a valid licence, the image still audits one page at a time, in JSON and HTML, which only excludes the whole site, the GitLab reports and the badge.
See what your site really weighs
Grammage brings together in a single tool the audit in a real browser, the code analysis in the CI/CD and a signed result that anyone can verify.
Free trial, no commitment.
Try it for free