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

success()

true, if no job the current job depends on failed or was skipped.

failure()

true, if any job the current job depends on failed.

cancelled()

true, if the workflow was cancelled.

always()

true, always - even if the workflow was cancelled.

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.