Repository navigation
Lint
The lint workflows of ncmake-workflows as make targets. Each target collects the same files, runs the same tool in the same version and fails where its workflow fails, so a finding shows on your machine before it shows in a public CI log. make lint-actions asks whether the workflows of the repository are valid, make lint-actions-security whether one of them can be attacked, and one target per template asks the same of shell scripts, Containerfiles, Markdown, YAML, PHP and plain web files; make lint runs every lint target. The lint-actions-security workflow runs that very target on the runner.
Tip
TL;DR — make dev-init once per machine, then make lint (the first run fetches each tool once). What fails the workflow is [!] and exits 1, what passes it is [i]. ARGS=--details adds the files a target checked, ARGS=--json the machine-readable form, SARIF=<file> on make lint-actions-security the form GitHub code scanning reads, make help-<target> the full option list of a target.
- The targets
- Which files they read
- What fails and what passes
- What lint-actions-security checks
- Where the tools come from
- make lint
- Options
- FAQ
| Target | Tool | What it checks |
|---|---|---|
make lint-actions |
actionlint, with shellcheck over the run: scripts |
whether every tracked workflow is valid |
make lint-actions-security |
zizmor | what makes a workflow or action definition attackable |
make lint-containerfiles |
hadolint, with shellcheck over every RUN
|
every tracked Containerfile and Dockerfile |
make lint-markdown |
markdownlint-cli2 | every tracked Markdown file |
make lint-php-syntax |
php -l, once per PHP version |
every tracked PHP file |
make lint-shell |
shellcheck | every tracked shell script |
make lint-vue-imports |
the analyser itself | imports of the @nextcloud/vue root in the tracked source files |
make lint-web |
ESLint, Stylelint and html-validate | the plain JavaScript, CSS and HTML of a repository without a package.json
|
make lint-yaml |
yamllint | every tracked YAML file |
make lint |
all of the above | every target of the module, one after the other |
Every target reads the checkout, writes nothing into it and needs no token. Each is named after the workflow of ncmake-workflows it mirrors, see Workflows.
The tracked files only, collected with git ls-files as the template collects them, so a file that is not committed is never checked; node_modules/ and vendor/ are left out where the template leaves them out. A repository without files of the kind passes. A directory that is not a git checkout is a failed run, exit 2.
A configuration of the tool in the repository wins, as it does on the runner. Without one, lint-markdown, lint-web and lint-yaml apply the default of their template, made for GitHub repositories (see Workflows).
-
lint-actionslints every YAML file under.github/workflowsand.ncmake/workflowsand every YAML file with a.properties.jsonbeside it, the form of a workflow template. It reads.github/actionlint.yamlor.yml; without one it declaresubuntu-latest-low, the self-hosted runner of Nextcloud, as a known runner label. -
lint-actions-securityreads whatlint-actionsreads and everyaction.ymloraction.yamloutsidenode_modules/andvendor/, with.github/zizmor.ymlorzizmor.yml. -
lint-shelltakes every*.shand*.bash, and every other file whose shebang names sh, bash, dash or ksh. It reads.shellcheckrc. -
lint-containerfilestakesContainerfile,Dockerfile,*.containerfileand*.dockerfile, such asDockerfile.devorapp.Containerfile. It reads.hadolint.yamlor.yml. -
lint-markdownreads a.markdownlint-cli2.*or.markdownlint.*file,lint-yamla.yamllint,.yamllint.yamlor.yamllint.yml. -
lint-vue-importsfinds a static or dynamic import, a re-export, arequireand avi.mockorjest.mockof the root in.vue,.js,.mjs,.cjs,.jsx,.ts,.mts,.ctsand.tsxfiles. An entry point such as@nextcloud/vue/components/NcButtonpasses. -
lint-webpasses a repository with apackage.json, which lints through its own scripts, and leaves out minified*.min.jsand*.min.css. -
lint-php-syntaxchecks the PHP versions ofPHP_VERSIONS, of a.php-versionfile or the default, separated by spaces, commas or semicolons; a word that is not digits and dots is refused:make lint-php-syntax PHP_VERSIONS="8.1 8.3". In an app the default is the PHP range its servers run on, the floor and the ceiling thatphp-compatderives fromappinfo/info.xmland thelib/versioncheck.phpof the servers; outside an app it is8.2 8.4.
[!] marks what fails the template and exits 1, [i] what passes it:
- every error of actionlint and every finding of shellcheck and of
lint-vue-importsis[!] - a warning of ESLint, Stylelint or markdownlint is
[i], an error[!] - a warning of yamllint is
[i], unless the yamllint configuration setsstrict - a hadolint finding below its
failure-thresholdis[i]; the threshold isinfounless the configuration sets one - a parse or fatal error of
php -lis[!]under the version that reports it, a warning or deprecation[i]; a linephp -lprints about a file without one of those kinds, as when the parse runs out of memory, is[!]when php exits with an error.php -lruns without a memory limit, in the template as in the target - zizmor: see below
Each finding names the file, the line, the rule and the page of the rule, which says how to fix it or how to ignore it.
zizmor reads GitHub Actions workflows for what makes them attackable: pull_request_target or workflow_run running the code of a pull request, ${{ github.event.* }} expanded into run:, an action not pinned to a commit, permissions broader than the job needs, credentials that actions/checkout leaves behind, cache poisoning in a release workflow. It runs offline, so it needs no token and reads nothing but the checkout, and it reads the repository's own .github/zizmor.yml or zizmor.yml when there is one, as it does on the runner.
Findings of medium and high severity are [!] and exit 1, as they fail the workflow; low and informational ones are [i]. Each names the file and line, the severity and confidence zizmor gives it, and the page of the zizmor audit, which says how to fix the finding or how to ignore it. A finding the repository's configuration or an inline # zizmor: ignore[...] ignores is left out.
make dev-init fetches mk/lint.mk along with the other developer modules. Every tool version is pinned in that module and nowhere else.
| Tools | Source | Check |
|---|---|---|
| actionlint, shellcheck, hadolint, zizmor | the Linux binary of the GitHub release, for x86_64 or aarch64 | the sha256 digest the module pins |
| yamllint, with pathspec and PyYAML | the source distributions from PyPI, run on the Python of the analysers' Node image without pip | the sha256 digest the module pins |
| markdownlint-cli2, ESLint, Stylelint, html-validate | npm, installed in the analysers' container | the integrity the registry records for the pinned version |
php -l |
the image docker.io/library/php:<version>-cli-alpine, one per version |
— |
All but PHP are kept in tools/ of the per-machine ncmake cache, fetched once per version and mounted read-only into the throwaway Node container of the analysers, next to the analyser itself, so the host needs none of them and nothing is written into the checkout. A newer version replaces the older one once no ncmake container runs. make cache-clean lists the fetched tools as entries of their own. The image ghcr.io/zizmorcore/zizmor is not used: on an older x86-64 CPU its binary stops with an illegal instruction, while the release binary runs.
With RUNTIME=bare the tools run on the host directly. An npm package whose engines.node the host's Node does not meet stops the run with that range, and lint-php-syntax runs the host's php<version>, or its php when that is the version, and reports a version it has neither for as not run.
make lint runs every lint target of the module, one after the other, each in a make of its own and the way it runs alone. A target that fails does not keep the next one from running, and make lint fails when one failed. ARGS, JSON= and SARIF= belong to a single target, and make lint refuses them.
| Option | Targets | What it does |
|---|---|---|
ARGS=--details |
every lint-*
|
lists the files that were checked |
ARGS=--json |
every lint-*
|
the full result as JSON, nothing else |
JSON=<file> |
every lint-*
|
the report as usual, and the JSON in <file> from the same run |
PHP_VERSIONS="<versions>" |
lint-php-syntax |
the PHP versions to check, instead of .php-version or the default |
ARGS=--persona=pedantic |
lint-actions-security |
zizmor's code smells as well; auditor shows everything, regular is the default |
SARIF=<file> |
lint-actions-security |
the report as usual, and the findings as SARIF in <file>, for GitHub code scanning |
Every finding of lint-actions-security in the JSON carries "security": true, because it says how a workflow can be attacked. The JSON and the SARIF file can lie outside the checkout; each is replaced only once it is complete, and a run that stops on an error writes neither.
Does my app need the workflows for this? No. The targets check whatever the repository has, installed by ncmake or not.
Why the release binaries and not a package manager? They are the builds the projects publish, their digests are pinned in the module, and they need nothing on the host. The same files run on the runner.
How do I ignore a finding I accept? As the tool documents it on the page each finding links: for zizmor an inline # zizmor: ignore[<audit>] comment or an entry in .github/zizmor.yml, for shellcheck a # shellcheck disable=<code> directive or .shellcheckrc, for the others their configuration file. The targets read all of them.
Why does lint-web pass my app? An app with a package.json lints its JavaScript and CSS through its own scripts, lint-eslint among them; lint-web is for a repository that ships plain web files without a build.
© 2026 [ernolf] Raphael Gradenwitz · MIT-licensed · Report an issue
For app developers
- Getting started
- How ncmake understands your app
- Building and packaging
- Bundle analysis
- Build tools
- Checkout audit
- Lint
- Releasing
- Integrity signature
- Translations
- Per-app tuning
- Target reference
- AI assistant
CI workflows
App Store
For app users
Git
Background
Reference