From 0649f1995235719701def538796aa4678b8c2b8e Mon Sep 17 00:00:00 2001 From: Aditya Tirmanwar Date: Tue, 28 Jul 2026 23:38:06 +0000 Subject: [PATCH] feat(test/tw): add version-pin-drift pipeline Flags drift between a package's pinned runtime version and the version the upstream project declares (python, nodejs, go, dotnet, ruby). Fetches the upstream config at the package version (X.Y.Z / vX.Y.Z, overridable via ref) and checks the pin: == for exact files, pin>= for go/.NET minimums, in-range for requires-python / engines.node / Gemfile. Co-Authored-By: Claude Opus 4.8 --- pipelines/test/tw/version-pin-drift.yaml | 117 +++++++++++++++++++++++ tests/suites/version-pin-drift.yaml | 93 ++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 pipelines/test/tw/version-pin-drift.yaml create mode 100644 tests/suites/version-pin-drift.yaml diff --git a/pipelines/test/tw/version-pin-drift.yaml b/pipelines/test/tw/version-pin-drift.yaml new file mode 100644 index 00000000..56006595 --- /dev/null +++ b/pipelines/test/tw/version-pin-drift.yaml @@ -0,0 +1,117 @@ +name: version-pin-drift + +description: | + Flag drift between a version pinned in a melange package and the version the + upstream project declares it targets. Fetches the upstream version config from + its repo (at the package version, trying both X.Y.Z and vX.Y.Z tags) and + checks the pin against it. + + Per ecosystem, the config file and comparison: + python : .python-version (==); requires-python / python_requires (in range) + nodejs : .nvmrc, .node-version (==); package.json engines.node (in range) + go : go.mod `go` directive (pin >= it, it is a minimum) + dotnet : global.json sdk.version (pin >= it) + ruby : .ruby-version, .tool-versions (==); Gemfile ruby (in range) + + The pin's granularity drives the comparison: "3.11" compares major.minor, "18" + major. Fails on a mismatch, an out-of-range pin, or no upstream evidence. + + Usage (ref defaults to the package version; only pin and repo are required): + - uses: test/tw/version-pin-drift + with: + ecosystem: python + repository: https://github.com/mlrun/mlrun + pin: ${{vars.python-version}} + +needs: + packages: + - busybox + - coreutils + - curl + +inputs: + ecosystem: + description: python, nodejs, go, dotnet or ruby. + required: true + repository: + description: Upstream GitHub repo URL (https://github.com/owner/repo). + required: true + pin: + description: The pinned version to validate (e.g. "3.11" or "18"). + required: true + ref: + description: | + Git ref to read the upstream config from. Optional: defaults to the + package version, trying both X.Y.Z and vX.Y.Z tags. Set it explicitly + when the upstream tag doesn't follow that pattern (e.g. release-1.2.3). + required: false + default: "" + +pipeline: + - name: Check pinned version against upstream declaration + runs: | + set -eu + pin="${{inputs.pin}}" + repo_path=$(echo "${{inputs.repository}}" | sed -E 's#^https?://github.com/##; s#\.git$##') + refs="${{inputs.ref}}" + [ -n "$refs" ] || refs="${{package.version}} v${{package.version}}" + found="" + + # Fetch an upstream file, trying each candidate ref (empty if none has it). + fetch() { + for r in $refs; do + out=$(curl -fsSL "https://raw.githubusercontent.com/${repo_path}/${r}/$1" 2>/dev/null) && { printf '%s' "$out"; return 0; } + done + } + # First number in a string, trimmed to the pin's component count. + ncomp=$(echo "$pin" | awk -F. '{print NF}') + trunc() { echo "$1" | grep -oE '[0-9][0-9.]*' | head -1 | cut -d. -f1-"$ncomp"; } + le() { [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -1)" = "$1" ]; } # $1 <= $2 + + want_eq() { # pin must equal this exact version (raw string, label) + got=$(trunc "$1"); [ -n "$got" ] || return 0 + [ "$got" = "$pin" ] || { echo "FAIL: upstream $2=$got != pin $pin"; exit 1; } + echo "ok $2=$got (==)"; found=1 + } + want_min() { # pin must be >= this minimum (raw string, label) + got=$(trunc "$1"); [ -n "$got" ] || return 0 + le "$got" "$pin" || { echo "FAIL: pin $pin below upstream $2 minimum $got"; exit 1; } + echo "ok $2=$got (pin>=)"; found=1 + } + want_range() { # pin must satisfy the >= / > / <= / < bounds (spec, label) + [ -n "$1" ] || return 0 + s=$(echo "$1" | tr -d '[:space:]') # ">= 3.1" -> ">=3.1" + for b in $(echo "$s" | grep -oE '>=?[0-9][0-9.]*'); do le "$(trunc "$b")" "$pin" || { echo "FAIL: pin $pin below $b ($1)"; exit 1; }; done + for b in $(echo "$s" | grep -oE '<=[0-9][0-9.]*'); do le "$pin" "$(trunc "$b")" || { echo "FAIL: pin $pin above $b ($1)"; exit 1; }; done + for b in $(echo "$s" | grep -oE '<[0-9][0-9.]*'); do t=$(trunc "$b"); { [ "$pin" != "$t" ] && le "$pin" "$t"; } || { echo "FAIL: pin $pin not < $b ($1)"; exit 1; }; done + echo "ok $2: $1 (in range)"; found=1 + } + + case "${{inputs.ecosystem}}" in + python) + want_eq "$(fetch .python-version)" ".python-version" + want_range "$(fetch pyproject.toml | grep -iE 'requires-python' | grep -oE '"[^"]+"' | head -1 | tr -d '"')" "requires-python" + want_range "$(fetch setup.py | grep -iE 'python_requires' | grep -oE '"[^"]+"' | head -1 | tr -d '"')" "python_requires" + ;; + nodejs) + want_eq "$(fetch .nvmrc)" ".nvmrc" + want_eq "$(fetch .node-version)" ".node-version" + want_range "$(fetch package.json | tr -d '\n' | grep -oE '"node"[[:space:]]*:[[:space:]]*"[^"]+"' | grep -oE '"[^"]+"$' | tr -d '"')" "engines.node" + ;; + go) + want_min "$(fetch go.mod | grep -oE '^go[[:space:]]+[0-9][0-9.]*' | head -1)" "go.mod go directive" + ;; + dotnet) + want_min "$(fetch global.json | tr -d '\n' | grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1)" "global.json sdk.version" + ;; + ruby) + want_eq "$(fetch .ruby-version)" ".ruby-version" + want_eq "$(fetch .tool-versions | grep -iE '^ruby[[:space:]]' | head -1)" ".tool-versions" + want_range "$(fetch Gemfile | grep -iE '^[[:space:]]*ruby[[:space:]]' | head -1)" "Gemfile ruby" + ;; + *) + echo "FAIL: unsupported ecosystem '${{inputs.ecosystem}}'"; exit 1 ;; + esac + + [ -n "$found" ] || { echo "FAIL: no upstream version evidence for ${{inputs.ecosystem}} (refs: $refs)"; exit 1; } + echo "PASS: pinned version '$pin' is consistent with upstream" diff --git a/tests/suites/version-pin-drift.yaml b/tests/suites/version-pin-drift.yaml new file mode 100644 index 00000000..0efce372 --- /dev/null +++ b/tests/suites/version-pin-drift.yaml @@ -0,0 +1,93 @@ +name: version-pin-drift pipeline validation tests + +description: Test suite for the test/tw/version-pin-drift pipeline across python and nodejs, using pinned upstream refs. + +testcases: + - name: python exact match (.python-version) + description: mlrun v1.10.2 ships .python-version=3.11; pin 3.11 is consistent + package: bash + pipelines: + - uses: test/tw/version-pin-drift + with: + ecosystem: python + repository: https://github.com/mlrun/mlrun + ref: v1.10.2 + pin: "3.11" + expect_pass: true + - name: python drift (.python-version mismatch) + description: mlrun v1.10.2 targets 3.11; a 3.12 pin must be flagged + package: bash + pipelines: + - uses: test/tw/version-pin-drift + with: + ecosystem: python + repository: https://github.com/mlrun/mlrun + ref: v1.10.2 + pin: "3.12" + expect_pass: false + - name: python range (requires-python) + description: fastapi 0.115.0 declares requires-python >=3.8; pin 3.11 satisfies it + package: bash + pipelines: + - uses: test/tw/version-pin-drift + with: + ecosystem: python + repository: https://github.com/fastapi/fastapi + ref: 0.115.0 + pin: "3.11" + expect_pass: true + - name: nodejs exact match (.node-version) + description: next.js v14.2.0 ships .node-version=18; pin 18 is consistent + package: bash + pipelines: + - uses: test/tw/version-pin-drift + with: + ecosystem: nodejs + repository: https://github.com/vercel/next.js + ref: v14.2.0 + pin: "18" + expect_pass: true + - name: nodejs drift (.node-version mismatch) + description: next.js v14.2.0 targets node 18; a node 16 pin must be flagged + package: bash + pipelines: + - uses: test/tw/version-pin-drift + with: + ecosystem: nodejs + repository: https://github.com/vercel/next.js + ref: v14.2.0 + pin: "16" + expect_pass: false + - name: go minimum satisfied (go.mod directive) + description: cosign v2.4.1 go.mod needs go 1.22; a 1.23 pin is >= the minimum + package: bash + pipelines: + - uses: test/tw/version-pin-drift + with: + ecosystem: go + repository: https://github.com/sigstore/cosign + ref: v2.4.1 + pin: "1.23" + expect_pass: true + - name: go below minimum (go.mod directive) + description: cosign v2.4.1 go.mod needs go 1.22; a 1.19 pin is below the minimum + package: bash + pipelines: + - uses: test/tw/version-pin-drift + with: + ecosystem: go + repository: https://github.com/sigstore/cosign + ref: v2.4.1 + pin: "1.19" + expect_pass: false + - name: ruby exact match (.ruby-version) + description: mastodon v4.2.0 ships .ruby-version=3.2.2; pin 3.2 is consistent + package: bash + pipelines: + - uses: test/tw/version-pin-drift + with: + ecosystem: ruby + repository: https://github.com/mastodon/mastodon + ref: v4.2.0 + pin: "3.2" + expect_pass: true