Development
Todo
Development - Explain how to write new job templates.
Conditional Jobs
Almost every job template offers switches to disable parts of a pipeline: apptest, documentation_steps,
cleanup, … A disabled job is not removed from the pipeline, it is skipped, and a skipped job influences all
jobs depending on it. Handling that influence correctly is the single most error-prone part of writing a job template,
therefore the rules are collected here.
Status Check Functions
GitHub Actions offers four status check functions, which can be used in a job’s if expression:
Function |
Result |
|---|---|
|
|
|
|
|
|
|
|
If a job has an if expression, but that expression contains no status check function at all, GitHub adds an
implicit success(). This is the behavior that surprises most template authors, because such a condition reads like
a pure feature switch, while it also demands that every dependency succeeded.
# This condition is evaluated as: success() && contains(inputs.documentation_steps, 'pages')
if: ${{ contains(inputs.documentation_steps, 'pages') }}
Propagation of Skipped Jobs
The status check functions are not evaluated on the jobs listed in needs, but on the transitive dependency
closure of the job: all jobs reachable from the current job by following needs edges. Therefore a skipped job
propagates its state to jobs it isn’t directly connected to.
An intermediate job does not stop that propagation. A job surviving on !cancelled() runs itself, but the
skipped ancestor remains part of the closure of all its successors:
AppTestingParams -> AppTesting -> PublishTestResults -> Documentation -> PDFDocumentation
(skipped by (skipped) (if: !cancelled() (if: !cancelled() (if: contains(...))
apptest: false) - runs) - runs) => skipped!
With apptest: false, PDFDocumentation was skipped although its own condition was true and neither of its
two needs jobs was skipped. Symmetrically, a job outside the closure cannot suppress a job guarded by
!failure().
Attention
A skipped job is not an error, so nothing in the pipeline turns red. The affected jobs are simply missing from the run, which makes this class of defect easy to overlook - the pipeline is green, but it did less than it claims.
Guidelines
Every job whose if expression contains a feature switch needs an explicit status check function, otherwise the
implicit success() links the switch to unrelated jobs:
if: >-
${{ !failure() && !cancelled()
&& contains(inputs.documentation_steps, 'pages')
}}
Which function to choose:
!failure() && !cancelled()- the job’s own condition decides, a skipped dependency is tolerated, but a failed dependency suppresses the job. This is the default for optional jobs (PublishToGitHubPages, LaTeXDocumentation) as well as for release jobs, which must not run if any check failed.!cancelled()- the job runs regardless of the outcome of its dependencies. Use it for jobs collecting or cleaning up results (PublishTestResults, CleanupArtifacts), because artifacts of a failed run still need to be published or deleted.always()- avoid it. It also runs the job when the workflow was cancelled, which delays the cancellation.
Hint
Do not use success() || failure() as a substitute for !cancelled(). It is false if a dependency was
skipped, because a skipped dependency is neither a success nor a failure.
A status check function tolerates a skipped dependency, which is exactly what it is for. A job downloading an artifact must additionally demand that the job producing that artifact succeeded, because a tolerated skip leaves the artifact missing:
if: >-
${{ !failure() && !cancelled()
&& needs.Documentation.result == 'success'
&& contains(inputs.documentation_steps, 'pages')
}}
Without the needs.<job>.result term the job starts and fails while downloading:
Unable to download artifact(s): Artifact not found for name: documentation-HTML
Important
The distinction is between an unrelated dependency and a producing one. AppTesting, skipped by
apptest: 'false', has nothing to do with the documentation and must not suppress it - that is the reason the
status check function is there. Documentation uploads what PublishToGitHubPages downloads, so
its skip has to suppress it.
Observed on a real pipeline: three packaging jobs were cancelled by GitHub, the cascade skipped the documentation
job, and the publishing job ran anyway on the strength of !failure() && !cancelled() alone.
A condition hard-coded in a job template that gates a job on the ref is a blocklist of the refs that cannot work, never an allowlist of the refs that have been seen to work. A deployment environment rejects some refs and admits the rest; observing one rejection says nothing about which of the others are admitted, so an allowlist built from that observation silently drops every ref that was simply never tried - and it drops it by skipping the job, which no pipeline reports. Where the admitted refs are per-repository configuration, the condition belongs into an input set by the caller, like publish_pages_on.
Attention
PublishToGitHubPages was guarded this way after a feature branch was rejected by the
github-pages environment. The condition allowed the default branch and dev, the two refs that had been
observed to deploy - which silently stopped publishing on tags, i.e. exactly the runs whose documentation
matters most.
Conditions combining a status check function with further terms are written as a folded block scalar, one term per line, so a condition can be read - and reviewed - without horizontal scrolling:
if: >-
${{ !failure() && !cancelled()
&& needs.Prepare.outputs.is_release_commit == 'true'
&& github.event_name != 'schedule'
}}
Verification
A skipped job cannot be detected by looking at a green pipeline, so each combination needs a verification pipeline that
actually exercises it. The templates in .github/workflows/_Checking_*.yml cover the relevant combinations:
_Checking_SimplePackage_Pipeline.yml runs with application testing enabled, while
_Checking_NamespacePackage_Pipeline.yml disables it and requests html latex pdf, so it combines a skipped
job with jobs conditioned on documentation_steps.
When a new switch is added to a job template, add a combination disabling it to one of these pipelines and check the list of executed jobs of the resulting run, not only its conclusion.