Tasks and plugins
pinup's core decides. A task applies. A task is a command on the branch's checkout, after the edits and before the commit: a lock refresh the manager knows, or a command a rule requested (postUpgradeTasks). What it changes is checked in with the edits in one commit. A manifest never lands without its lock.
- No platform token, no signing key in the task, only
PATH,LANG,TZand whateverPINUP_PLUGIN_ENVnames - An allowlist on the compiled command, a scope on the result
Lock refreshes
| Manager | Command | Scope |
|---|---|---|
| composer | composer update <names> --with-all-dependencies --no-plugins --no-install --no-scripts --no-audit --ignore-platform-reqs (without names on maintenance) | composer.lock |
| npm | npm install --package-lock-only --no-audit --ignore-scripts | package-lock.json, npm-shrinkwrap.json |
| npm (yarn classic) | yarn install --ignore-scripts --ignore-engines --ignore-platform --non-interactive | yarn.lock |
| gomod | go mod tidy | go.mod, go.sum |
A machine without the toolchain holds the branch with pluginRequired and names the tool. A maintenance for a lock no plugin refreshes (a Terraform lock) is held the same way. A refresh that changes nothing is nothingToRefresh and opens nothing.
Post-upgrade commands
"packageRules": [{
"matchDatasources": ["git-refs"],
"postUpgradeTasks": {
"commands": ["node tools/update-expected-commit.mjs {{{packageFile}}}"],
"fileFilters": ["*.yaml"],
"executionMode": "update"
}
}]
Templates in the command render with the update's variables (depName, currentValue, newValue, packageFile …). executionModeupdate runs once per update, branch once per branch with every update of the rule.
The command must match a pattern of the runner's allowedCommands (PINUP_ALLOWED_COMMANDS, anchored patterns, never a repository's setting). What is checked is the compiled command, after templating. One the allowlist does not admit holds the branch with taskRefused and names the command. Renovate skipped the command and pushed the branch anyway. That is how an estate lost every first-party lock refresh for weeks without a red job.
The scope
A task runs in its own HOME, with PATH, LANG, TZ and whatever PINUP_PLUGIN_ENV names. Nothing else: no platform token, no signing key. A toolchain that has to fetch a private module reads a .netrc in that HOME, from PINUP_TASK_NETRC, a read-only credential that is never passed on as a variable.
What the task changed is checked against its fileFilters (the lock on a refresh, the rule's list on a command). A change outside the scope fails the branch by name and discards everything the task did. A task is not a way to write arbitrary files into a repository. A repository that commits node_modules/ fails its npm refresh in exactly this way, and rightly so: the refresh writes node_modules/.package-lock.json.
PINUP_EXECUTION_TIMEOUT (minutes) bounds every task.
Where the tools come from
Tasks use the toolchain on the job image's PATH: the runner's toolchain image carries composer, npm and go, the commands run in-process, without a shell (a || b hands composer a package named ||). A container variant of the same contract, one digest-pinned image per task, is what the specification describes; the exec variant is the one proven in production.