Any CI system
apigenie runs the same three checks on a pull request whatever system runs the pipeline: lint
for the issues in the files, format --check for the files that are not formatted, and diff for
the changes that break clients. Each command exits with status 1 when it finds something, which
fails the job, and writes a report in a format the system shows next to the pull request. This page
has the pieces every system needs: the three commands, the reports each system reads, installing the
CLI in a job, the comparison with the base branch, and one script that runs the checks. The pages of
GitLab CI, Azure Pipelines, Jenkins,
CircleCI and Bitbucket Pipelines put them into
one pipeline file each. On GitHub, the GitHub Action installs the CLI,
finds the base of the pull request and turns the reports into annotations.
The three checks
| Check | Command | Fails when |
|---|---|---|
| Lint | apigenie lint api/openapi.yaml |
An issue of severity error is reported. Warnings alone do not fail the check. |
| Formatting | apigenie format --check api/openapi.yaml |
A file would change when formatted. The names of the files go to standard error; nothing is written. |
| Breaking changes | apigenie diff base.yaml api/openapi.yaml |
A change is breaking by the rules of the standard or the configuration. |
The commands take several files, and glob patterns where the shell expands them. lint, format
and diff read the project’s apigenie.yaml when there is one, so the job checks what the
developers check. Status 2 means a command could not run: a file that cannot be read, an invalid
configuration, an option that does not exist. The exit statuses
are the same in the four binaries.
Reports the systems read
A report goes to standard output, so the job redirects it to a file and hands the file to the system. The job log then shows nothing of the issues; the exit status still fails the job, and the system shows the report next to the pull request.
| Formatter | Written by | Read by |
|---|---|---|
junit |
lint |
The test reports of GitLab CI, Azure Pipelines, CircleCI and Bitbucket Pipelines, and the JUnit plugin of Jenkins: a test suite per file and a failed test case per issue, named after the rule, warnings included. |
checkstyle |
lint |
The Warnings plugin of Jenkins and other readers of Checkstyle XML, which keep the severity and the rule of each issue. |
sarif |
lint |
GitHub code scanning, the Warnings plugin of Jenkins and the SARIF viewers. The results link the rule pages, and carry fingerprints that let a dashboard follow an issue across runs. |
markdown |
diff |
People: a comment on the pull request, a job summary, an artifact. |
json |
lint, diff |
Scripts, with the JSON Schemas of the lint and diff reports. |
unix, visualstudio |
lint, diff |
One line per issue or change, with the file, the line and the column: for the job log, and for the problem matchers of editors and of some systems. |
The junit report marks every issue as a failed test, warnings too, so a test tab can show failures
on a job that passed: the status of the job comes from the exit status, which warnings do not
change.
Installing the CLI in a job
With npm, on an image that has Node.js 18 or later; the package installs the binary of the job’s platform, Linux x64, macOS arm64 or Windows x64:
npm install -g @mykulyak/api-genie
From the release archive, on any Linux x64 image with curl and tar. The archive holds the four
binaries, and tar extracts the one named; the .sha256 file next to each asset is in sha256sum
format:
version=0.10.0
curl -fsSLO "https://github.com/mykulyak/api-genie/releases/download/cli-$version/apigenie-$version-linux-x64.tar.gz"
curl -fsSLO "https://github.com/mykulyak/api-genie/releases/download/cli-$version/apigenie-$version-linux-x64.tar.gz.sha256"
sha256sum -c "apigenie-$version-linux-x64.tar.gz.sha256"
tar xzf "apigenie-$version-linux-x64.tar.gz" apigenie
./apigenie --version
In a container, with the mykulyak/apigenie image, Alpine Linux with the four binaries and
apigenie as its entry point; the files are read from the mounted directory:
docker run --rm -v "$PWD:/mnt/data" mykulyak/apigenie:0.10.0 lint api/openapi.yaml
A system that runs the job inside the image has to replace the entry point with a shell; the
GitLab CI page shows how. Pin the version in every case: a check
that moves with latest changes under the pull requests. Downloads lists the
packages for the other platforms.
Comparing with the base branch
The diff check compares the file of the pull request with its version on the branch the pull
request merges into. The system names that branch in a variable, and the job fetches its tip, which
a shallow clone does not have, and takes the file from it:
git fetch --depth=1 origin "$BASE"
git show FETCH_HEAD:api/openapi.yaml > base.yaml
apigenie diff base.yaml api/openapi.yaml
FETCH_HEAD is what the fetch brought, whatever branches the clone tracks; a clone of the pull
request branch alone, which most systems make, has no origin/main even after the fetch.
| System | The base branch |
|---|---|
| GitHub Actions | github.base_ref; the action finds it itself. |
| GitLab CI | CI_MERGE_REQUEST_TARGET_BRANCH_NAME, in a merge request pipeline. |
| Azure Pipelines | System.PullRequest.TargetBranch, as refs/heads/main for Azure Repos and main for GitHub. |
| Jenkins | CHANGE_TARGET, in a multibranch pipeline. |
| CircleCI | None. The job names the branch, or reads the pull request through the provider’s API. |
| Bitbucket Pipelines | BITBUCKET_PR_DESTINATION_BRANCH, in a pull request pipeline. |
A file the pull request adds has no base version, and git show fails; the script below skips the
comparison then. To compare with the last release instead of the base branch, fetch its tag:
git fetch --depth=1 origin tag v1.4.0 and git show v1.4.0:api/openapi.yaml.
One script for the three checks
Every check runs even when an earlier one fails, so the pull request shows all three results at
once; the exit status is 1 when any of them failed. BASE is the base branch from the table above.
#!/bin/sh
# The three checks of api/openapi.yaml; exits with 1 when one of them fails.
status=0
apigenie lint --formatter junit api/openapi.yaml > lint.xml || status=1
apigenie format --check api/openapi.yaml || status=1
git fetch --depth=1 origin "$BASE"
if git show FETCH_HEAD:api/openapi.yaml > base.yaml 2>/dev/null; then
apigenie diff base.yaml api/openapi.yaml || status=1
fi
exit $status
The script was run with the CLI 0.10.0 in a shallow clone of a pull request branch that removes an
operation, with BASE=main: the lint report was written, the formatting check passed, the
comparison reported the breaking change, and the exit status was 1. For a file the branch added,
the comparison was skipped.