Skip to content

Version 0.3.0

Choose a tag to compare

@mribeirodantas mribeirodantas released this 08 Sep 18:13
· 79 commits to main since this release

[0.3.0] - 2026-09-08

Added

  • Lineage-backed DAG reconstruction (--diff-dag) — when a run's project
    directory has a Nextflow data-lineage store (.lineage/, produced with
    lineage.enabled = true on Nextflow 25.04+), the process wiring is now read
    from the authoritative provenance Nextflow persisted instead of being
    inferred from work-dir symlinks. A new LineageStore reads each .data.json
    record directly off disk (no compile-time dependency on the nf-lineage
    module), indexes the run's TaskRun records by their session id, and
    reconstructs producer→consumer edges from each task's recorded input LID
    references (lid://<producerTaskHash>/…). Because it reads what Nextflow
    recorded, this needs no work directories and is unaffected by cleanup.
    DagComparator.graphOf now takes the run's project directory and prefers the
    lineage store, falling back to the existing best-effort symlink
    reconstruction (symlinkGraphOf) when no lineage store recorded the run. Each
    run's RunGraph carries a source (LINEAGE / SYMLINK / NONE), and the
    wiring layer's note now states whether the graph is authoritative (lineage) or
    best-effort (symlinks), including the mixed case. The layer remains
    informational only — it never affects the "identical" verdict or
    --fail-on-change. Only the default <projectDir>/.lineage store location is
    auto-detected; a custom lineage.store.location still falls back to symlinks.

Changed

  • Unknown plugin command now exits with the usage code (2), not 1. An
    unrecognized verb (anything other than diff) is a usage error, in the same
    class as bad arguments, so it now returns 2 — matching the documented
    exit-code table — instead of 1 (which is reserved for runtime errors). The
    message also notes that only diff is supported.

  • Help summary lists all current diff layers. The one-line description shown
    by -h/--help still read "metadata, processes, and per-task
    resources/scripts" from the 0.1.0 days; it now enumerates the always-on layers
    (parameters, configuration, software & versions, failure rollup, performance
    regressions, resource-efficiency) and the three opt-in flags.

  • --diff-dag no longer requires work directories when lineage is enabled.
    Previously the wiring layer always needed the tasks' work directories to still
    exist locally; with a lineage store present it is reconstructed from persisted
    provenance instead.

  • Continuous integration & tag-based releases — a GitHub Actions CI
    workflow (.github/workflows/ci.yml) now runs the full verification suite
    (make check) on every push and pull request to main, across JDK 17 and 21,
    uploading test reports as build artifacts. A companion release workflow
    (.github/workflows/release.yml) publishes to the
    Nextflow plugin registry when a v* version
    tag is pushed: it verifies the tag matches build.gradle's version (so a tag
    can never publish a mismatched artifact), runs make check, then make release, authenticating with an NPR_API_KEY repository secret. See the
    README's "Continuous integration" and "Releasing" sections for setup and the
    tagging flow.

  • DAG (process wiring) diff (--diff-dag) — a new opt-in layer that
    reconstructs each run's process;process wiring and diffs the two edge
    sets, so nf-diff surfaces topology changes the task-count-per-process view
    cannot see — e.g. a pipeline rewired from A → C to A → B → C. Nextflow
    does not persist DAG edges in its history or cache, so there is no
    authoritative edge list to read; what it does leave on disk is every task's
    staged inputs, materialised as symbolic links inside the task's work
    directory. DagComparator walks each task's work dir, resolves every input
    symlink, and attributes any target that resolves into another task's work dir
    (walking the parent chain so a link into a nested output subdir still
    attributes to the producer) as a producer;consumer edge; links that
    resolve outside every work dir are external inputs and yield no edge. Because
    it walks work directories, this layer needs them to still exist locally (like
    --diff-outputs / --diff-logs) and is a best-effort reconstruction: if some
    work dirs were cleaned up, the recovered wiring is incomplete, and a note
    reports how many task work dirs were missing so a partial diff is not read as
    authoritative. Surfaced in all three report formats (HTML "Process wiring
    (DAG)" section + nav link, Markdown section, and a dag block with
    dagEdgesAdded/dagEdgesRemoved in JSON). Because the reconstruction is
    best-effort, this layer is informational only and never affects the
    "identical" verdict or --fail-on-change.

  • Failure rollup (top-level "what failed and why") — a new always-on layer
    that answers, at a glance, which tasks failed and why, instead of leaving that
    scattered across per-task detail. Failed tasks are detected from the cached
    status/exit fields (an explicit FAILED/ABORTED status, or a non-zero
    exit code — the NO_EXIT sentinel and blanks are ignored), so the layer reads
    straight from the run cache and needs no work directories. Failures are rolled
    up by their (process, status, exit) signature and counted per run, sorted by
    biggest blast radius first; a signature seen only in Run B is flagged new
    (a regression), one present in Run A but gone in Run B is resolved, and one
    in both is persistent. The layer also surfaces a run-level error state
    (history status starting ERR or equal to FAILED/ABORTED/KILLED) even
    when no individual task failure was recorded. Surfaced in all three report
    formats (HTML "Failure rollup" section + nav link + summary cards, Markdown
    section, and a failures block with failedA/failedB/newFailures/
    resolvedFailures summary counts in JSON). Because the meaningful identity
    signal — a task whose status or exit changed — is already carried by the task
    field diffs, this rollup is informational only and never separately affects
    isIdentical() / --fail-on-change.

  • Resource-efficiency layer (requested vs. measured-peak provisioning) — a
    new always-on layer that, per process, compares what each run requested
    (cpus, memory) against what it actually peaked at (%cpu, peak_rss).
    Both requested and peak values are read straight from the run cache trace and
    taken as the max across a process's tasks (a retried task that used more, or
    was bumped a higher request, is the honest worst case), so the layer needs no
    work directories and is always computed. The efficiency ratio is
    measured-peak / requested; each process is classified per run as over
    (below 50% — wasted allocation, e.g. "requested 32 GB, peaked at 4 GB"),
    tight (90%+ — risk of OOM kills or CPU throttling), or ok in between.
    Surfaced in all three report formats (HTML section + an "Over-provisioned (B)"
    summary card, Markdown table, and an efficiency array plus
    overProvisionedA/overProvisionedB and tightA/tightB summary counts in
    JSON). Because provisioning is a tuning signal rather than a correctness
    change, this layer is informational only — it never affects isIdentical() /
    --fail-on-change.

  • Cross-project comparison (--dir-a / --dir-b) — the two runs no longer
    have to live in the same project. Previously a single --dir resolved both
    runs' .nextflow/ history, cache, config and params, so you could not compare
    "the same pipeline in two checkouts" (or on two machines). --dir-a=<dir> and
    --dir-b=<dir> now set each run's project directory independently; each falls
    back to --dir when omitted, so existing invocations are unchanged. Run A is
    loaded from and resolved against dir-a, run B against dir-b: the parameters
    layer reads each run's own -params-file, and the configuration layer rebuilds
    each run's effective nextflow.config from its own working tree, so a
    -profile docker in project A is diffed against project B's config. The
    git-provenance caveat became per-tree: ConfigProvenance now carries a
    crossProject flag plus each side's directory, current HEAD and dirty state,
    and its warning describes the two working trees separately (currentRevisionB,
    dirtyB, dirA, dirB are surfaced in the JSON report). --last still needs
    a single history, so it is rejected when combined with differing
    --dir-a/--dir-b.

Fixed

  • Diff errors with no message printed a blank line. DiffPlugin.exec()
    reported a caught throwable via e.message only, so a message-less exception
    (notably NullPointerException) produced a bare nf-diff: line with nothing
    after it, while the stack trace went only to the debug-gated log. It now falls
    back to the exception's simple class name, so both the stderr line and the log
    always name the failure.
  • Cache-lock retry backoff was not interruptible. The backoff between
    attempts to open a contended run cache used Groovy's sleep(), which swallows
    InterruptedException and clears the interrupt flag, so a Ctrl-C during a
    contended open was ignored and the loop kept retrying. It now uses
    Thread.sleep(), restoring the interrupt flag and aborting the retry on
    interruption.
  • GitProvenance subprocess timeout was ineffective — the git subprocess's
    stdout/stderr were read inline with getText() before the timed waitFor,
    which blocks until the process exits, so a hung git could never be timed out.
    Both streams are now drained on background threads started before waitFor, so
    the 5s timeout actually fires and a chatty command cannot deadlock on a full
    pipe buffer.