Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ jobs:
# GitHub-hosted, change this line to: runs-on: ubuntu-latest
runs-on: [self-hosted, orion]
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# JDK 21: the io.nextflow.nextflow-plugin toolchain builds with a JDK 21
# toolchain (sources still target Java 17). Provide 21 so Gradle uses it
# rather than downloading one.
- name: Setup JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: temurin
java-version: '21'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@48b5f213c81028ace310571dc5ec0fbbca0b2947 # v4
uses: gradle/actions/setup-gradle@48b5f213c81028ace310571dc5ec0fbbca0b2947 # v4.4.3

- name: Build
run: ./gradlew assemble --no-daemon
Expand All @@ -43,3 +43,14 @@ jobs:
# main safety net and were previously never executed in CI.
- name: Test
run: ./gradlew test --no-daemon

# The half of pin hygiene that needs the network, so it can't live in the
# (hermetic) Spock suite: does each pin's `# vX.Y.Z` comment actually name
# the tag its SHA carries? A wrong label is worse than none — it makes a
# major-version jump read as a routine same-line bump, which is exactly how
# a checkout v6 -> v7.0.1 bump nearly landed labelled `# v6`.
#
# Read-only (git ls-remote), no auth. Runs on PRs, so a Dependabot bump that
# mislabels a pin is caught before merge rather than after.
- name: Verify pin comments match their SHAs
run: ./scripts/verify-pins.sh
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ jobs:
name: Build and publish plugin
runs-on: [self-hosted, orion]
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# JDK 21 — see ci.yml (the nextflow-plugin toolchain builds on JDK 21).
- name: Setup JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: temurin
java-version: '21'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@48b5f213c81028ace310571dc5ec0fbbca0b2947 # v4
uses: gradle/actions/setup-gradle@48b5f213c81028ace310571dc5ec0fbbca0b2947 # v4.4.3

# Sanity-check the tag matches build.gradle's version, so the artifact and
# the release tag never drift.
Expand All @@ -46,7 +46,7 @@ jobs:
# Use the action rather than the gh CLI — the self-hosted runner container
# has no gh installed.
- name: Publish release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
files: build/distributions/nf-spawn-*.zip
generate_release_notes: true
6 changes: 3 additions & 3 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
name: Secret Scan (gitleaks)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # full history so leaked-then-removed secrets are caught
# Run the gitleaks BINARY directly (MIT-licensed, free for orgs). The
Expand All @@ -37,7 +37,7 @@ jobs:
name: Trivy Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Trivy filesystem scan (vulns + secrets)
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
Expand All @@ -60,7 +60,7 @@ jobs:
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Enforcing: fails the build on findings. Triaged false-positives are
# annotated inline with `# nosemgrep: <rule-id> -- <reason>` (#368).
- name: Semgrep scan
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`nextflow-plugin` toolchain, already a release behind. A new
`SpawnCiHygieneTest` fails the build if a pin is reverted or an entry dropped.

### Changed
- **Actions bumped, and every pin's version comment now states the truth (#83).**
`actions/checkout` → v7.0.1, `actions/setup-java` → v5.6.0,
`softprops/action-gh-release` → v3.0.2. The bumps arrived from Dependabot
labelled `# v6` on all five `checkout` refs — but the SHA it moved to
(`3d3c42e`) is **v7.0.1**, so a three-major jump read as a routine same-line
bump; `gradle/actions/setup-gradle` had likewise carried a bare `# v4` while
pinning v4.4.3. Comments now name exact versions. Nothing in the majors affects
this repo: `action-gh-release@v3` requires the Node 24 runtime, which the orion
runner (2.336.0) provides and `checkout@v6.0.3` was already using, and
`checkout@v7`'s one behavior change only blocks fork checkouts under
`pull_request_target`/`workflow_run`, neither of which appears here.

### Fixed
- **A pin's version comment can no longer silently misstate what CI runs (#83).**
`SpawnCiHygieneTest` required only that *some* `# vN` comment be present, so the
mislabelled `checkout` bump above passed it. It now requires an exact `vX.Y.Z`,
and a new `scripts/verify-pins.sh` — wired into CI, so it gates PRs — resolves
each SHA against the tag its comment claims and fails if they disagree. The
network half lives in the script to keep the Spock suite hermetic. Both accept
annotated-tag pins (the tag object *or* the commit it points at); an earlier
draft compared against only one form and called a correct pin mislabelled.
- **`./gradlew test` no longer reports success without running the tests when only
CI config changed (#80).** Gradle's up-to-date check considered just the Groovy
sources, so a commit touching only `.github/` or `build.gradle` would skip
Expand Down
87 changes: 87 additions & 0 deletions scripts/verify-pins.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# Verify every action pin's `# vX.Y.Z` comment actually names the tag its SHA
# carries.
#
# This is the half of pin hygiene that needs the network, so it is NOT in the
# Spock suite (which must stay offline and hermetic). SpawnCiHygieneTest enforces
# the offline half: every pin is a 40-hex SHA with an exact vX.Y.Z comment.
#
# Why it exists: a pin's comment can be silently false. Dependabot bumped
# actions/checkout to 3d3c42e — which is v7.0.1 — while leaving `# v6` on all
# five refs, and a stale `# v4` on gradle/actions/setup-gradle (really v4.4.3)
# had sat on main for months. Both read as fine. A wrong label is worse than no
# label: it makes a major-version jump look like a routine same-line bump.
#
# Read-only: git ls-remote only. No auth, no writes, no side effects.
#
# Usage: scripts/verify-pins.sh
set -euo pipefail

cd "$(dirname "$0")/.."

fail=0
found=0

while IFS= read -r line; do
# file:lineno: ... uses: owner/action@<sha> # vX.Y.Z
loc=${line%%:*}
rest=${line#*:}
lineno=${rest%%:*}

ref=$(printf '%s\n' "$line" | sed -nE 's/.*uses:[[:space:]]*([^[:space:]]+@[0-9a-f]{40}).*/\1/p')
ver=$(printf '%s\n' "$line" | sed -nE 's/.*@[0-9a-f]{40}[[:space:]]*#[[:space:]]*(v[0-9][^[:space:]]*).*/\1/p')
[ -n "$ref" ] && [ -n "$ver" ] || continue

action=${ref%@*}
sha=${ref#*@}
# gradle/actions/setup-gradle is a subdirectory action: the repo is the first
# two path segments, so trim anything deeper before querying.
repo=$(printf '%s\n' "$action" | cut -d/ -f1,2)

found=$((found + 1))

# Resolve the claimed tag to BOTH acceptable SHAs and accept either.
#
# For an annotated tag, `refs/tags/vX` is the tag OBJECT and `refs/tags/vX^{}`
# the commit it points at, and the two differ. Both are legitimate pins —
# gradle/actions/setup-gradle@48b5f21 is v4.4.3's tag object, actions/checkout
# pins the commit — and GitHub resolves either. Comparing against just one form
# reports a correct pin as mislabeled (which this script did on its first run,
# absurdly printing "comment says v4.4.3 ... SHA is actually v4.4.3").
claimed=$(git ls-remote "https://github.com/$repo" \
"refs/tags/$ver" "refs/tags/$ver^{}" 2>/dev/null | awk '{print $1}')

if [ -z "$claimed" ]; then
printf ' MISSING TAG %s:%s %s @ %.12s — tag %s does not exist in %s\n' \
"$loc" "$lineno" "$action" "$sha" "$ver" "$repo"
fail=1
elif ! printf '%s\n' "$claimed" | grep -qFx "$sha"; then
# Name what the SHA really is, so the fix is obvious rather than a puzzle.
actual=$(git ls-remote --tags "https://github.com/$repo" 2>/dev/null \
| awk -v s="$sha" '$1==s {gsub("refs/tags/",""); gsub(/\^\{\}$/,"",$2); print $2}' \
| sort -u | paste -sd, -)
printf ' MISLABELED %s:%s %s @ %.12s — comment says %s (that tag is %.12s); SHA is actually %s\n' \
"$loc" "$lineno" "$action" "$sha" "$ver" "$(printf '%s' "$claimed" | head -1)" \
"${actual:-untagged}"
fail=1
else
printf ' ok %s:%s %s %s\n' "$loc" "$lineno" "$action" "$ver"
fi
done < <(grep -rn 'uses:.*@[0-9a-f]\{40\}' .github/workflows/)

# Anti-vacuous: a grep that stops matching would otherwise report success forever.
if [ "$found" -eq 0 ]; then
echo "verify-pins: found no SHA-pinned actions under .github/workflows — the check" >&2
echo "matched nothing, which is a parser problem, not a clean bill of health." >&2
exit 1
fi

if [ "$fail" -ne 0 ]; then
echo
echo "verify-pins: FAIL — a pin's version comment does not match its SHA." >&2
echo "Fix the comment to name the tag the SHA really carries (or move the SHA)." >&2
exit 1
fi

echo
echo "verify-pins: OK — all $found pins' comments match their SHAs."
29 changes: 29 additions & 0 deletions src/test/groovy/io/nextflow/spawn/SpawnCiHygieneTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,35 @@ class SpawnCiHygieneTest extends Specification {
'\nUse: uses: owner/action@<40-hex-sha> # vX.Y.Z'
}

def 'every version comment names an exact patch version, not a bare major'() {
// The comment is the only human-readable part of a pin, and a bare `# v6`
// is not just vague — it can be actively false, which is worse than absent.
// Dependabot bumped checkout to 3d3c42e (which is v7.0.1) while leaving the
// comment reading `# v6` on all five refs. The pin was correct; the label
// said it was a major version behind what it actually ran, and the previous
// spec passed because it only required SOME `# vN` to be present. A stale
// `# v4` on setup-gradle (really v4.4.3) had sat on main the same way.
//
// Requiring X.Y.Z is the strongest version of this claim checkable offline:
// a bare major can silently keep meaning something new as the pin moves,
// whereas an exact version either matches the SHA or is a visible lie, and
// reviewers can resolve it. Verifying comment-vs-tag needs the network, so
// it stays out of the unit suite — see scripts/verify-pins.sh.
given:
def exact = Pattern.compile(/^[^@\s]+@[0-9a-f]{40}\s+#\s*v\d+\.\d+\.\d+\s*$/)
def refs = usesRefs()

expect: 'anti-vacuous — if the parser stops matching, this would pass forever'
!refs.isEmpty()

and:
def vague = refs.findAll { !exact.matcher(it[2] as String).matches() }
assert vague.isEmpty(), 'these pins do not name an exact vX.Y.Z, so the comment ' +
'cannot be checked against the SHA and may silently misstate what CI runs:\n' +
vague.collect { " ${it[0]}:${it[1]} ${it[2]}" }.join('\n') +
'\nResolve the SHA to its tag and write that: # vX.Y.Z'
}

def 'Dependabot has a github-actions entry whose group patterns cover every action'() {
// The other half of pinning. A SHA never moves, including past a security
// fix, so pinning without Dependabot trades a mutable-tag hole for a
Expand Down
Loading