← all scripts

.github/scripts/dependency-scan.sh

1532 of 1532 statements covered (100.00%).

coveredmissednever traced by Bash (not counted)A line ending in … continues the statement above it and shares its fate.

1#!/usr/bin/env bash
2# =============================================================================
3# dependency-scan.sh — check Python, Node.js, Docker, Helm, and EKS versions
4# =============================================================================
5#
6# Invoked by .github/workflows/deps-scan.yml (monthly schedule).
7#
8# Checks for drift across:
9#
10# - Python packages pinned in pyproject.toml
11# - Docker images referenced from workflows, K8s manifests, examples,
12# local live-validation manifests, and Helm chart values
13# - Helm chart versions from charts.yaml
14# - EKS add-on versions from gco/stacks/constants.py (AWS creds)
15# - EKS Kubernetes minor from cdk.json (AWS creds)
16# - Aurora PostgreSQL engine versions (AWS creds)
17# - EMR Serverless release labels (AWS creds)
18# - Bedrock default model ids from cdk.json
19# context.bedrock.mission_default_model_id (Mission sampling),
20# context.bedrock.capacity_advisor_default_model_id (capacity advisor),
21# context.bedrock.claude_code_default_model_id (Claude Autopilot),
22# context.bedrock.codex_default_model_id (Codex Autopilot),
23# context.bedrock.opencode_default_model_id (OpenCode Autopilot),
24# context.bedrock.embedding_model_id (Mission memory), and
25# context.vector_store.embedding_model_id (workload RAG corpus), each
26# compared against the newest same-family release — inference profiles
27# for the generation keys, EMBEDDING foundation models for the embedding
28# keys (AWS creds)
29# - Accelerator catalog and Karpenter NodePool policy (offline), plus live
30# NVIDIA GPU / AWS Neuron EC2 catalog drift across enabled Regions (AWS creds)
31# - Dockerfile.dev ARG pins (Node LTS major, npm, CDK CLI, kubectl,
32# AWS CLI v2, Docker CLI, Docker Buildx, uv) and the immutable AWS CLI
33# runtime image in gco/services/inference_monitor.py — public registries,
34# no AWS creds needed
35# - GCO Autopilot pins from cli/autopilot.py: CLAUDE_CODE_VERSION,
36# CODEX_VERSION and OPENCODE_VERSION install pins vs their npm latest
37# dist-tags, plus companion MCP server liveness (missing/deprecated/yanked
38# on npm or PyPI) — public endpoints, no AWS creds needed
39# - Pre-commit hook revisions in .pre-commit-config.yaml compared
40# against the latest tag published upstream (GitHub API)
41# - CDK enum constants from gco/stacks/constants.py compared against the
42# installed aws-cdk-lib (LAMBDA_PYTHON_RUNTIME, LAMBDA_NODEJS_RUNTIME;
43# the Aurora engine is a plain version string checked against live RDS)
44# - Latest stable Python release from endoflife.date — public endpoint
45# - CI tooling the workflows install by hand: Trivy (the install-trivy
46# composite action's version default),
47# actionlint (ACTIONLINT_VERSION), Helm and kubectl (HELM_VERSION /
48# KUBECTL_VERSION), kubeconform (KUBECONFORM_VERSION), Calico
49# (CALICO_VERSION), Metrics Server (METRICS_SERVER_VERSION), kind + its
50# node image, and the Crossplane function-go-templating package
51# post-helm-crossplane.yaml installs — public endpoints, no AWS creds
52# - Version consistency: ruff (pyproject / pre-commit / lint workflow),
53# Python and Node runtime pins, npm packageManager + CDK CLI pins, every
54# owned npm graph's lockfile/Dependabot coverage, duplicated *_VERSION
55# workflow environment pins, every per-Lambda requirements.txt pin
56# against the version resolved centrally, and digest-pinned images
57# carrying two digests under one tag
58# - Base-image security epochs (APT_SECURITY_EPOCH / DNF_SECURITY_EPOCH)
59# older than SECURITY_EPOCH_STALE_DAYS
60# - Suppression expiries: .trivyignore / .pip-audit-ignore /
61# .npm-audit-ignore entries expiring within SUPPRESSION_EXPIRY_WARN_DAYS
62# (before the CI validator hard-fails)
63# - Lockfile freshness: direct deps in pyproject.toml missing from or pinned
64# differently in requirements-lock.txt
65#
66# Ports the `.dependency-scan-script` YAML anchor from the retired
67# GitLab pipeline into a standalone shell script. Two behavior changes:
68#
69# 1. Workflow file input. The GitLab version grepped `.gitlab-ci.yml` for
70# CI image tags. This version scans every file under
71# `$WORKFLOWS_DIR` (default: `.github/workflows`).
72# 2. Reporting. The GitLab version POSTed directly to the GitLab issues
73# API. This version writes a Markdown report to a file and emits
74# `has_drift=true|false`, `scan_complete=true|false`, and `report_path=…`
75# on $GITHUB_OUTPUT so the calling workflow can manage a rolling issue.
76#
77# Environment inputs:
78# WORKFLOWS_DIR default: .github/workflows
79#
80# Outputs (via $GITHUB_OUTPUT):
81# has_drift "true" when any version is outdated, else "false"
82# scan_complete "false" when any check fails or is explicitly skipped,
83# else "true"
84# report_path path to the Markdown report (only set when has_drift=true)
85# =============================================================================
8635set -uo pipefail
87
8835WORKFLOWS_DIR="${WORKFLOWS_DIR:-.github/workflows}"
8970REPORT_FILE="$(mktemp -t dep-scan-XXXXXX.md 2>/dev/null || mktemp --suffix=.md)"
90# The same findings as JSON, for anything that acts on them without parsing
91# prose (see the "Machine-readable findings" helpers in lib_dependency_scan.sh).
92# Written on every run, drift or not, next to the Markdown report.
9335FINDINGS_FILE="${REPORT_FILE%.md}.json"
9470FINDINGS_SURFACES="$(mktemp)"
9570INCOMPLETE_REASONS_FILE="$(mktemp -t dep-scan-incomplete-XXXXXX 2>/dev/null || mktemp)"
96
97# Persist incomplete reasons to a file because many checks run in pipeline
98# subshells. A shell variable assignment made there would be lost, while an
99# append to this channel survives and is consumed by the final completeness
100# predicate. Messages go to stderr so they cannot corrupt command substitutions.
101mark_scan_incomplete() {
102154 local reason="$1"
103154 printf '%s\n' "$reason" >> "$INCOMPLETE_REASONS_FILE"
104154 echo " INCOMPLETE: $reason" >&2
105}
106
107join_scan_incomplete_reasons() {
10815 sort -u "$INCOMPLETE_REASONS_FILE" \
10915 | awk 'BEGIN { first = 1 } { if (!first) printf "; "; printf "%s", $0; first = 0 } END { print "" }'
110}
111
112# Source shared functions (also used by BATS tests)
113140SCAN_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
114# shellcheck source=.github/scripts/lib_dependency_scan.sh
11535source "${SCAN_SCRIPT_DIR}/lib_dependency_scan.sh"
116
117# ---------------------------------------------------------------------------
118# Report helpers
119#
120# Small Markdown emitters shared by every section of the drift report so the
121# table formatting lives in one place — previously each section hand-rolled
122# its own ``| … |`` header + separator, which drifted as sections were added.
123# ``emit_md_table`` turns a pipe-delimited results file into a GitHub table;
124# ``md_anchor`` builds the in-page heading slug the top-of-report summary
125# links to.
126#
127# Thresholds for the recurring-hygiene checks. Tunable in one place:
128# SUPPRESSION_EXPIRY_WARN_DAYS surface .trivyignore / .pip-audit-ignore /
129# .npm-audit-ignore entries expiring within
130# this many days
131# (the CI validator still hard-fails on the
132# day itself — this is the early warning).
133# SECURITY_EPOCH_STALE_DAYS flag a Dockerfile APT/DNF security epoch
134# older than this many days.
135# ---------------------------------------------------------------------------
13635SUPPRESSION_EXPIRY_WARN_DAYS="${SUPPRESSION_EXPIRY_WARN_DAYS:-30}"
13735SECURITY_EPOCH_STALE_DAYS="${SECURITY_EPOCH_STALE_DAYS:-45}"
138
139# md_anchor <title> — GitHub heading slug (lowercase, punctuation dropped,
140# spaces → hyphens). Close enough to GitHub's own algorithm for the
141# summary-table links to resolve.
142md_anchor() {
143529 printf '%s' "$1" \
144529 | tr '[:upper:]' '[:lower:]' \
145529 | sed -E 's/[^a-z0-9 -]//g; s/ /-/g'
146}
147
148# emit_md_table <header> <results-file> [wrap]
149#
150# <header> pipe-delimited column labels, e.g. "Package|Current|Latest"
151# <results-file> file of pipe-delimited rows with the same column count
152# [wrap] when "code", every non-empty cell is wrapped in backticks
153#
154# Prints a GitHub-flavoured Markdown table. With no wrap, cells are emitted
155# verbatim, so a caller that wants a link in a cell just writes the
156# ``[text](url)`` markdown straight into the results file.
157emit_md_table() {
158146 local header="$1" file="$2" wrap="${3:-}"
159146 local -a cells cols
160292 IFS='|' read -r -a cells <<< "$header"
161146 local head="|" sep="|" c
162514 for c in "${cells[@]}"; do
163514 head+=" ${c} |"
164514 sep+="---|"
165 done
166146 printf '%s\n%s\n' "$head" "$sep"
1671426 while IFS='|' read -r -a cols; do
168567 [ "${#cols[@]}" -eq 0 ] && continue
169567 local row="|" cell
1701958 for cell in "${cols[@]}"; do
1712020 if [ "$wrap" = "code" ] && [ -n "$cell" ]; then
17262 row+=" \`${cell}\` |"
173 else
1741896 row+=" ${cell} |"
175 fi
176 done
177567 printf '%s\n' "$row"
178 done < "$file"
179}
180
181# days_until <YYYY-MM-DD> — integer days from today to the given date
182# (negative when the date is in the past). Empty output on a malformed date.
183days_until() {
184106 python3 -c "
185import datetime, sys
186try:
187 d = datetime.date.fromisoformat(sys.argv[1])
188except Exception:
189 sys.exit(0)
190print((d - datetime.date.today()).days)
191" "$1" 2>/dev/null
192}
193
194# days_since <YYYY-MM-DD> — integer days from the given date to today
195# (negative when the date is in the future). Empty output on a malformed date.
196days_since() {
197111 python3 -c "
198import datetime, sys
199try:
200 d = datetime.date.fromisoformat(sys.argv[1])
201except Exception:
202 sys.exit(0)
203print((datetime.date.today() - d).days)
204" "$1" 2>/dev/null
205}
206
207# ---------------------------------------------------------------------------
208# Python packages
209#
210# We run ``pip list --outdated`` on the installed interpreter, but
211# filter the JSON result down to packages we pin *directly* in
212# ``pyproject.toml::[project.dependencies]`` or the
213# ``[project.optional-dependencies]`` groups. Every other outdated
214# entry is a transitive dependency — its version is controlled by
215# something we pin (``jsii``, ``aws-cdk-lib``, ``fastmcp``,
216# ``botocore``, …) and bumping it ourselves either does nothing or
217# breaks the resolver. Leaving those entries in the monthly scan
218# report was creating noise: the operator had no action to take on
219# them beyond "wait for upstream". Filter them out so the report
220# only lists packages we can act on.
221#
222# The ``[build-system]`` requires pins (the exact-pinned build backend)
223# are appended to the same surface below via a direct PyPI lookup — pip
224# resolves them inside build isolation, so ``pip list`` never sees them.
225# ---------------------------------------------------------------------------
22635echo "=== Checking for outdated Python dependencies ==="
22735
22835# Install the project with EVERY optional-dependency group, not just the
22935# base dependencies: ``pip list --outdated`` can only report packages that
23035# are installed, so a base-only install silently dropped pins that live
23135# exclusively in an extras group (``aws-cdk-lib`` in ``cdk``, ``playwright``
23235# in ``diagrams``, ``mypy`` in ``typecheck``, ...) even though
23335# ``extract_direct_python_deps`` already includes them in the direct-pin
23435# filter below. Groups are enumerated from pyproject.toml so a new extras
23535# group joins the surface automatically; if enumeration fails, fall back to
23635# the old base-only install rather than dropping the report section.
237105PYTHON_EXTRAS="$(extract_python_extras pyproject.toml | paste -sd, -)"
23835if [ -z "$PYTHON_EXTRAS" ]; then
2391 mark_scan_incomplete "Could not enumerate optional dependency groups from pyproject.toml."
240fi
24135if [ -n "$PYTHON_EXTRAS" ]; then
24234 echo "Installing with extras: [${PYTHON_EXTRAS}]"
24334 if ! pip install -e ".[${PYTHON_EXTRAS}]" --quiet --root-user-action=ignore; then
2441 mark_scan_incomplete "Python dependency installation failed."
245 fi
246else
2471 if ! pip install -e . --quiet --root-user-action=ignore; then
2481 mark_scan_incomplete "Python dependency installation failed and optional groups could not be enumerated."
249 fi
250fi
25170if ! OUTDATED_RAW="$(pip list --outdated --format=json)"; then
2521 mark_scan_incomplete "pip list --outdated failed."
2531 OUTDATED_RAW="[]"
25468elif ! printf '%s' "$OUTDATED_RAW" | jq -e 'type == "array"' >/dev/null 2>&1; then
2551 mark_scan_incomplete "pip list --outdated returned malformed JSON."
2561 OUTDATED_RAW="[]"
257fi
258
259# Build a newline-separated list of PEP-503-normalised direct-dep names.
260# An empty list disables the filter so we never silently hide drift when
261# the TOML parse breaks. In practice the file always parses — we just
262# can't risk a dropped report section.
26370DIRECT_DEPS="$(extract_direct_python_deps pyproject.toml)"
26435if [ -z "$DIRECT_DEPS" ]; then
2651 mark_scan_incomplete "Could not parse direct Python dependencies from pyproject.toml."
266fi
267
268105if ! OUTDATED="$(printf '%s' "$OUTDATED_RAW" | python3 -c "
269105import json, re, sys
270105raw = sys.stdin.read()
271105direct = set(
272105 line.strip() for line in '''$DIRECT_DEPS'''.splitlines() if line.strip()
273105)
274105try:
275105 data = json.loads(raw) if raw else []
276105except json.JSONDecodeError:
277105 raise SystemExit(1)
278105if direct:
279105 data = [
280105 e for e in data
281105 if re.sub(r'[-_.]+', '-', e.get('name', '')).lower() in direct
282105 ]
283105print(json.dumps(data))
284105")"; then
2851 mark_scan_incomplete "Could not parse or filter pip's outdated-package response."
2861 OUTDATED="[]"
287fi
288
289# Build-backend pins ([build-system] requires) are Python dependencies
290# too, but they are invisible to ``pip list --outdated``: pip resolves
291# them inside build isolation, not in this venv (a Python 3.14 venv does
292# not even ship setuptools). Compare each exact pin against its PyPI
293# latest and report it through the same Python-packages surface as every
294# other pyproject pin. Non-exact entries are skipped here — the
295# version-consistency section flags those as a policy finding.
29670BUILD_SYSTEM_PINS="$(extract_build_system_pins pyproject.toml)"
29735if [ -z "$BUILD_SYSTEM_PINS" ]; then
2981 mark_scan_incomplete "Could not parse [build-system] requirements from pyproject.toml."
299else
300136 while IFS='|' read -r bs_name bs_version bs_raw; do
301 # Only exact pins are compared; non-exact entries surface through
302 # the version-consistency policy check instead.
30368 if [ -z "$bs_name" ] || [ -z "$bs_version" ]; then
3041 continue
305 fi
306131 if ! bs_latest="$(curl -fsSL --max-time 15 "${SCAN_CURL_RETRY[@]}" \
307 "https://pypi.org/pypi/${bs_name}/json" 2>/dev/null \
308 | jq -r '.info.version // empty' 2>/dev/null)" || [ -z "$bs_latest" ]; then
3091 mark_scan_incomplete "PyPI lookup failed or returned an invalid version for build dependency ${bs_name}."
3101 continue
311 fi
31264 if [ "$(compare_semver "$bs_version" "$bs_latest")" = "newer" ]; then
31324 OUTDATED="$(echo "$OUTDATED" | jq \
314 --arg name "$bs_name" --arg cur "$bs_version" --arg latest "$bs_latest" \
315 '. + [{"name": $name, "version": $cur, "latest_version": $latest}]')"
316 fi
317 done <<< "$BUILD_SYSTEM_PINS"
318fi
319
320105PYTHON_COUNT="$(echo "$OUTDATED" | jq 'length')"
32135if [ "$PYTHON_COUNT" -eq 0 ]; then
32227 echo "All Python dependencies are up to date."
32327 PYTHON_OUTDATED=""
324else
3258 echo "Found $PYTHON_COUNT outdated Python package(s) (direct dependencies only — transitive bumps are upstream's job)"
32616 echo "$OUTDATED" | jq -r '.[] | " - \(.name): \(.version) -> \(.latest_version)"'
3278 PYTHON_OUTDATED="$OUTDATED"
328fi
329
330# ---------------------------------------------------------------------------
331# npm packages (every repository-owned graph)
332# ---------------------------------------------------------------------------
333# Direct npm dependencies never had a drift surface of their own: aws-cdk and
334# markdownlint-cli2 only leaked into the report indirectly (via the
335# Dockerfile.dev ARG and the pre-commit hook rev), and the
336# inference-streaming-proxy's @aws-sdk clients were reported nowhere at all.
337# This walks the same repository-owned graphs the npm package-management
338# check validates and compares each exact direct pin against the registry's
339# ``latest`` dist-tag — the npm analogue of the Python Packages surface.
34035echo ""
34135echo "=== Checking for outdated npm packages ==="
34235
34370NPM_RESULTS="$(mktemp)"
34435NPM_COUNT=0
345
34670NPM_PACKAGE_DIRS="$(list_npm_package_dirs .)"
34735if [ -z "$NPM_PACKAGE_DIRS" ]; then
3481 mark_scan_incomplete "Could not enumerate repository-owned npm package manifests."
349fi
350144while IFS= read -r package_dir; do
35138 [ -n "$package_dir" ] || continue
35236 manifest="${package_dir}/package.json"
35372 package_pins="$(extract_npm_direct_pins "$manifest")"
35436 if [ -z "$package_pins" ]; then
3551 mark_scan_incomplete "Could not parse exact npm dependency pins from ${manifest}."
3561 continue
357 fi
358160 while IFS='|' read -r pkg_name pkg_version; do
35945 [ -n "$pkg_name" ] || continue
360 # Scoped names carry a '/', which must be encoded in the registry URL.
361135 encoded_name="$(printf '%s' "$pkg_name" | sed 's|/|%2F|g')"
362179 if ! pkg_latest="$(curl -fsSL --max-time 15 "${SCAN_CURL_RETRY[@]}" \
363 "https://registry.npmjs.org/${encoded_name}/latest" 2>/dev/null \
364 | jq -r '.version // empty' 2>/dev/null)" || [ -z "$pkg_latest" ]; then
3651 mark_scan_incomplete "npm registry lookup failed or returned an invalid version for ${pkg_name}."
3661 continue
367 fi
36888 if [ "$(compare_semver "$pkg_version" "$pkg_latest")" = "newer" ]; then
36914 echo " - ${package_dir}: ${pkg_name} ${pkg_version} -> ${pkg_latest}"
37014 echo "${package_dir}|${pkg_name}|${pkg_version}|${pkg_latest}" >> "$NPM_RESULTS"
371 fi
372 done <<< "$package_pins"
373done <<< "$NPM_PACKAGE_DIRS"
374
375105NPM_COUNT="$(wc -l < "$NPM_RESULTS" | tr -d ' ')"
37635if [ "$NPM_COUNT" -eq 0 ]; then
37727 echo "All npm direct dependencies are up to date."
378else
3798 echo "Found $NPM_COUNT outdated npm package pin(s) across the owned graphs"
380fi
381
382# ---------------------------------------------------------------------------
383# Docker image tags
384# ---------------------------------------------------------------------------
38535echo ""
38635echo "=== Checking for outdated Docker images ==="
38735
38870DOCKER_RESULTS="$(mktemp)"
38970ALL_IMAGES="$(mktemp)"
390
391check_image() {
392375 local image="$1"
393375 local current_tag="$2"
394
395 # Only handle semver tags
396375 if ! is_semver_tag "$current_tag"; then
39732 return
398 fi
399 # Skip images we build in this project
400343 if is_project_image "$image"; then
40132 return
402 fi
403
404311 local parsed registry repo
405622 parsed="$(parse_image_registry "$image")"
406933 registry="$(echo "$parsed" | cut -d'|' -f1)"
407933 repo="$(echo "$parsed" | cut -d'|' -f2)"
408
409 # Fetch and filter separately. A registry/network failure marks the scan
410 # incomplete, while "the registry answered and nothing newer exists in
411 # this variant family" is an up-to-date pin. The previous single pipeline
412 # conflated the two under pipefail: the strict bare-semver grep matched
413 # nothing for suffix-tagged repositories (…-py3, …-cuda…, …-ubuntu…), so
414 # they reported "tag lookup failed" every month even though the registry
415 # was fine.
416311 local raw_tags=""
4171228 if ! raw_tags="$(skopeo list-tags "${SCAN_SKOPEO_RETRY[@]}" "docker://${registry}/${repo}" 2>/dev/null \
418 | jq -r '.Tags[]' 2>/dev/null)" || [ -z "$raw_tags" ]; then
41924 mark_scan_incomplete "Container registry tag lookup failed for ${registry}/${repo}."
42024 return
421 fi
422
423287 local latest_tag
424861 latest_tag="$(printf '%s\n' "$raw_tags" | newer_same_variant_tag "$current_tag")" || latest_tag=""
425
426287 if [ -n "$latest_tag" ]; then
42771 echo " - ${image}:${current_tag} -> ${latest_tag}"
42871 echo "${image}|${current_tag}|${latest_tag}" >> "$DOCKER_RESULTS"
42971 return
430 fi
431
432 # No newer family member. If the pinned tag itself is no longer listed,
433 # the pin points at something the registry stopped advertising (renamed
434 # variant scheme, withdrawn tag) — that deserves eyes, not silence.
435 # tag_listed reads the list from an argument, not a printf pipe: under
436 # pipefail, grep -q's early exit gave printf SIGPIPE on large tag lists
437 # and inverted "tag present" into a false INCOMPLETE (2026-09 scan).
438216 if ! tag_listed "$current_tag" "$raw_tags"; then
43913 mark_scan_incomplete "Pinned tag ${current_tag} is no longer listed by ${registry}/${repo}."
440 fi
441}
442
443# Collect image:tag pairs from workflow files (bare `image:` references in
444# container specs and `uses: …@sha` are handled by Dependabot; here we look
445# for free-form image references in run steps).
44635echo "Checking workflow files in $WORKFLOWS_DIR..."
44735if [ -d "$WORKFLOWS_DIR" ]; then
44834 grep -rhoE "image: [a-zA-Z0-9_./-]+:[a-zA-Z0-9._-]+" "$WORKFLOWS_DIR" 2>/dev/null \
44966 | sed 's/image: //' >> "$ALL_IMAGES" || true
45034 grep -rhoE "[a-zA-Z0-9_./-]+:[a-zA-Z0-9._-]+" "$WORKFLOWS_DIR" 2>/dev/null \
45134 | grep -E '^(alpine|hadolint|koalaman|semgrep|bridgecrew|checkmarx|trufflesecurity|zricethezav|aquasec|bats|python):' \
45234 | sed 's/[[:space:]]*$//' >> "$ALL_IMAGES" || true
453fi
454
45535echo "Checking K8s manifest images..."
45635grep -rhoE "image: [a-zA-Z0-9_./-]+:[a-zA-Z0-9._-]+" lambda/kubectl-applier-simple/manifests/ 2>/dev/null \
45771 | grep -v '{{' | sed 's/image: //' >> "$ALL_IMAGES" || true
458
45935echo "Checking example manifest images..."
46035grep -rhoE "image: [a-zA-Z0-9_./-]+:[a-zA-Z0-9._-]+" examples/ 2>/dev/null \
46136 | sed 's/image: //' >> "$ALL_IMAGES" || true
462
46335echo "Checking local live-validation manifest images..."
46435grep -rhoE "image: [a-zA-Z0-9_./-]+:[a-zA-Z0-9._-]+" scripts/live_release_validation/manifests/ 2>/dev/null \
46536 | sed 's/image: //' >> "$ALL_IMAGES" || true
466
46735echo "Checking Helm chart value images..."
468# Registry-aware walk over every charts.yaml values block (see
469# extract_chart_value_images in lib_dependency_scan.sh — moved there so BATS
470# exercises the real logic). charts.yaml always pins values images, so an
471# empty result means the parse broke — surface that as an incomplete scan
472# rather than silently dropping the sweep.
47370CHART_VALUE_IMAGES="$(extract_chart_value_images lambda/helm-installer/charts.yaml)"
47435if [ -n "$CHART_VALUE_IMAGES" ]; then
47532 printf '%s\n' "$CHART_VALUE_IMAGES" >> "$ALL_IMAGES"
476else
4773 mark_scan_incomplete "Could not parse Helm chart value images."
478fi
479
480# Mooncake default image — pinned as a Python constant in cli/images.py
481# (_DISAGGREGATED_DEFAULT_IMAGE), so it is invisible to Dependabot (docker
482# ecosystem) and to the manifest/workflow sweeps above. Add it here so a newer
483# upstream vLLM release shows up in the monthly drift report — the cue to
484# validate and bump the pin.
48535echo "Checking Mooncake default image (cli/images.py)..."
48670MOONCAKE_IMAGE="$(extract_mooncake_default_image cli/images.py)"
48735if [ -n "$MOONCAKE_IMAGE" ]; then
48834 echo "$MOONCAKE_IMAGE" >> "$ALL_IMAGES"
489else
4901 mark_scan_incomplete "Could not parse the Mooncake default image from cli/images.py."
491fi
492
493# The model-sync init container uses an official AWS CLI image pinned by both
494# version and manifest-list digest. Strip only the digest for the registry tag
495# lookup; the offline provenance contract separately requires the digest.
49635echo "Checking AWS CLI runtime image (gco/services/inference_monitor.py)..."
49770AWS_CLI_RUNTIME_IMAGE="$(extract_python_string_constant \
498 AWS_CLI_IMAGE gco/services/inference_monitor.py)"
499# check_pinned_digest <repo:tag@sha256:digest> <origin label> [sweep]
500#
501# Shared digest-freshness check for every digest-pinned image the repository
502# commits: verify the tag's currently published manifest-list digest still
503# equals the committed one. A moved digest is a drift row (the tag was
504# re-pushed upstream — the pin is stale); an unreachable registry or an
505# implausible response marks the scan incomplete. The tag also joins the
506# image sweep below unless <sweep> is ``no``, for a pin whose release drift
507# another section already reports (the Crossplane Function package's GitHub
508# release check), so the same release is not reported twice.
509check_pinned_digest() {
510138 local pinned_ref="$1" origin="$2" sweep="${3:-yes}" parts repository tag committed published
511276 if ! parts="$(split_pinned_image_ref "$pinned_ref")"; then
5121 mark_scan_incomplete "Could not parse an immutable image reference from ${origin}."
5131 return
514 fi
515411 repository="$(echo "$parts" | cut -d'|' -f1)"
516411 tag="$(echo "$parts" | cut -d'|' -f2)"
517411 committed="$(echo "$parts" | cut -d'|' -f3)"
518137 if [ "$sweep" != "no" ]; then
51970 printf '%s:%s\n' "$repository" "$tag" >> "$ALL_IMAGES"
520 fi
521
522274 if ! published="$(published_manifest_digest "${repository}:${tag}")"; then
5234 mark_scan_incomplete "Container manifest lookup failed for ${repository}:${tag}."
5244 return
525 fi
526133 if [ "$committed" != "$published" ]; then
52737 echo " - ${repository}:${tag}: committed digest does not match the tag (${origin})"
52837 echo "${repository}|${tag}@${committed}|${tag}@${published}" >> "$DOCKER_RESULTS"
529 fi
530}
531
53235if [[ "$AWS_CLI_RUNTIME_IMAGE" =~ ^[^@]+:[^@]+@sha256:[0-9a-f]{64}$ ]]; then
53334 check_pinned_digest "$AWS_CLI_RUNTIME_IMAGE" "gco/services/inference_monitor.py"
534else
5351 mark_scan_incomplete "Could not parse an immutable AWS_CLI_IMAGE from gco/services/inference_monitor.py."
536fi
537
538# The live-validation smoke manifests pin every image by tag AND manifest-list
539# digest (tests/test_live_release_validation.py enforces the shape). The tag
540# half already rides the normal drift check above; this pass keeps the digest
541# half honest too, so an upstream same-tag re-push shows up as drift instead
542# of silently diverging from what a validation run would actually pull.
54335echo "Checking live-validation smoke image digests (scripts/live_release_validation/manifests)..."
544140SMOKE_PINNED_REFS="$(grep -rhoE \
545 "image: [a-zA-Z0-9_./-]+:[a-zA-Z0-9._-]+@sha256:[0-9a-f]{64}" \
546 scripts/live_release_validation/manifests/ 2>/dev/null | sed 's/^image: //' | sort -u)"
54735if [ -z "$SMOKE_PINNED_REFS" ]; then
5481 mark_scan_incomplete "No digest-pinned smoke images found under scripts/live_release_validation/manifests/."
549else
55071 while read -r pinned_ref; do
55137 [ -z "$pinned_ref" ] && continue
55237 check_pinned_digest "$pinned_ref" "live-validation smoke manifest"
553 done <<< "$SMOKE_PINNED_REFS"
554fi
555
556# The Crossplane Function package post-helm-crossplane.yaml installs is pinned
557# by release tag AND image-index digest, because Crossplane re-resolves a bare
558# tag against the registry on every reconcile. Its release drift is the GitHub
559# release check's (CI tooling, below), so it stays out of the tag sweep; this
560# pass keeps the digest bound to the tag it names.
56135echo "Checking the Crossplane function package digest (post-helm-crossplane.yaml)..."
56270FUNCTION_PACKAGE_REFS="$(extract_crossplane_function_packages \
563 lambda/kubectl-applier-simple/manifests/post-helm-crossplane.yaml function-go-templating)"
56435if [ -z "$FUNCTION_PACKAGE_REFS" ]; then
5651 mark_scan_incomplete "Could not parse the Crossplane function-go-templating package from post-helm-crossplane.yaml."
566else
56768 while read -r package_ref; do
56834 check_pinned_digest "$package_ref" "lambda/kubectl-applier-simple/manifests/post-helm-crossplane.yaml" no
569 done <<< "$FUNCTION_PACKAGE_REFS"
570fi
571
572# The kind node image CI boots its clusters from is pinned by tag AND digest:
573# kind rebuilds and re-pushes the same version tag for every kind release, so
574# the tag alone names a different image after each one. A newer patch within
575# the pinned minor is the CI tooling check's (below), which also reports a
576# pin that carries no digest; this pass keeps the digest bound to the tag it
577# names, out of the tag sweep for the same reason as the Function package.
57835echo "Checking the kind node image digest (integration-tests.yml)..."
579140KIND_NODE_IMAGE_REF="$(extract_kind_pins .github/workflows/integration-tests.yml | awk -F'|' '$1=="kind-node"{print $2}' | head -1)"
58035if split_pinned_image_ref "$KIND_NODE_IMAGE_REF" >/dev/null; then
58133 check_pinned_digest "$KIND_NODE_IMAGE_REF" ".github/workflows/integration-tests.yml KIND_NODE_IMAGE" no
582fi
583
584# One lookup per distinct image: ``python:3.14.8-slim`` and
585# ``docker.io/library/python:3.14.8-slim`` are the same Docker Hub tag, and
586# listing them separately doubled both the report row and the registry calls.
587445dedupe_image_refs < "$ALL_IMAGES" | while read -r img; do
588375 [ -z "$img" ] && continue
5891125 image="$(echo "$img" | cut -d':' -f1)"
5901125 tag="$(echo "$img" | cut -d':' -f2)"
591375 check_image "$image" "$tag"
592done
59335rm -f "$ALL_IMAGES"
594
595105DOCKER_COUNT="$(wc -l < "$DOCKER_RESULTS" | tr -d ' ')"
59635[ -z "$DOCKER_COUNT" ] && DOCKER_COUNT=0
597
598# ---------------------------------------------------------------------------
599# Helm chart versions
600# ---------------------------------------------------------------------------
60135echo ""
60235echo "=== Checking Helm chart versions ==="
60335
60470HELM_RESULTS="$(mktemp)"
60535CHARTS_FILE="lambda/helm-installer/charts.yaml"
606
60735if [ -f "$CHARTS_FILE" ]; then
60868 CHART_ENTRIES="$(extract_helm_charts "$CHARTS_FILE")"
60934 if [ -z "$CHART_ENTRIES" ]; then
6101 mark_scan_incomplete "Could not parse Helm chart pins from ${CHARTS_FILE}."
611 fi
612322 while IFS= read -r entry; do
613128 [ -n "$entry" ] || continue
614378 chart_name="$(echo "$entry" | jq -r '.name // empty')"
615378 repo_url="$(echo "$entry" | jq -r '.repo_url // empty')"
616378 chart="$(echo "$entry" | jq -r '.chart // empty')"
617378 current="$(echo "$entry" | jq -r '.version // empty')"
618378 use_oci="$(echo "$entry" | jq -r '.use_oci // false')"
619503 if [ -z "$chart_name" ] || [ -z "$repo_url" ] || [ -z "$chart" ] || [ -z "$current" ]; then
6201 mark_scan_incomplete "Helm chart parser emitted an incomplete record from ${CHARTS_FILE}."
6211 continue
622 fi
623
624125 latest=""
625125 if [ "$use_oci" = "true" ]; then
626272 if ! latest="$(with_retries 3 helm show chart "${repo_url}/${chart}" 2>/dev/null | grep '^version:' | awk '{print $2}')" \
62764 || [ -z "$latest" ]; then
6284 mark_scan_incomplete "Helm OCI lookup failed for ${repo_url}/${chart}."
6294 continue
630 fi
631 else
63257 if ! with_retries 3 helm repo add "$chart_name" "$repo_url" --force-update > /dev/null 2>&1; then
6331 mark_scan_incomplete "Helm repository refresh failed for ${chart_name} (${repo_url})."
6341 continue
635 fi
636224 if ! latest="$(with_retries 3 helm search repo "${chart_name}/${chart}" --output json 2>/dev/null \
637 | jq -r '.[0].version // empty')" || [ -z "$latest" ]; then
6382 mark_scan_incomplete "Helm chart lookup failed for ${chart_name}/${chart}."
6392 continue
640 fi
641 fi
642
643118 if [ "$current" != "$latest" ]; then
64438 current_stripped="${current#v}"
64538 latest_stripped="${latest#v}"
64638 if [ "$current_stripped" != "$latest_stripped" ]; then
64738 echo " - ${chart_name} (${chart}): ${current} -> ${latest}"
64838 echo "${chart_name}|${chart}|${current}|${latest}" >> "$HELM_RESULTS"
649 fi
650 fi
651 done <<< "$CHART_ENTRIES"
652else
6531 mark_scan_incomplete "${CHARTS_FILE} is missing."
654fi
655
656105HELM_COUNT="$(wc -l < "$HELM_RESULTS" 2>/dev/null | tr -d ' ')"
65735[ -z "$HELM_COUNT" ] && HELM_COUNT=0
658
659# ---------------------------------------------------------------------------
660# EKS add-on versions (best-effort — requires AWS credentials)
661#
662# Pre-flight: probe for usable AWS credentials. If `sts get-caller-identity`
663# fails the scan is skipped entirely and a one-line note goes into both the
664# console log and the Markdown report — this is more honest than silently
665# dropping the section. Wire AWS creds through OIDC (see the deps-scan
666# section in .github/CI.md) to enable the check.
667# ---------------------------------------------------------------------------
66835echo ""
66935echo "=== Checking EKS add-on versions ==="
67035
67170ADDON_RESULTS="$(mktemp)"
67235ADDON_SKIP_REASON=""
67370K8S_VERSION="$(extract_k8s_version "")"
674
67535if ! aws sts get-caller-identity >/dev/null 2>&1; then
6763 ADDON_SKIP_REASON="No AWS credentials available (scan needs eks:DescribeAddonVersions). Configure OIDC to enable."
6773 echo " $ADDON_SKIP_REASON"
678else
67964 EKS_ADDONS="$(extract_eks_addons "gco/stacks/regional_stack.py")"
68032 if [ -z "$EKS_ADDONS" ]; then
6811 ADDON_SKIP_REASON="Could not read EKS add-on pins from gco/stacks/regional_stack.py."
6821 echo " $ADDON_SKIP_REASON"
683 else
684352 while IFS='|' read -r addon_name current_version; do
685147 [ -z "$addon_name" ] && continue
686294 latest="$(aws eks describe-addon-versions \
687 --addon-name "$addon_name" \
688 --kubernetes-version "$K8S_VERSION" \
689 --query 'addons[0].addonVersions[0].addonVersion' \
690 --output text 2>/dev/null)" || latest=""
691
692147 if ! [[ "$latest" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-eksbuild\.[0-9]+$ ]]; then
6932 ADDON_SKIP_REASON="EKS add-on lookup failed or returned an invalid version for ${addon_name}."
6942 echo " $ADDON_SKIP_REASON"
6952 break
696 fi
697145 if [ "$current_version" != "$latest" ]; then
69840 echo " - ${addon_name}: ${current_version} -> ${latest}"
69940 echo "${addon_name}|${current_version}|${latest}" >> "$ADDON_RESULTS"
700 fi
701 done <<< "$EKS_ADDONS"
702 fi
703fi
704
705105ADDON_COUNT="$(wc -l < "$ADDON_RESULTS" 2>/dev/null | tr -d ' ')"
70635[ -z "$ADDON_COUNT" ] && ADDON_COUNT=0
707
708# ---------------------------------------------------------------------------
709# EKS Kubernetes version (best-effort — requires AWS credentials)
710#
711# Compares ``kubernetes_version`` in cdk.json against the latest minor
712# available from ``aws eks describe-cluster-versions`` (filtered to
713# ``STANDARD_SUPPORT``). If a newer minor is available, we also report
714# when standard support for the currently-pinned minor ends so the
715# upgrade urgency is visible in the PR.
716#
717# IAM action: ``eks:DescribeClusterVersions``. Same credential preflight
718# as the EKS add-on / Aurora / EMR checks.
719# ---------------------------------------------------------------------------
72035echo ""
72135echo "=== Checking EKS Kubernetes version ==="
72235
72370EKS_K8S_RESULTS="$(mktemp)"
72435EKS_K8S_SKIP_REASON=""
725
72635if ! aws sts get-caller-identity >/dev/null 2>&1; then
7273 EKS_K8S_SKIP_REASON="No AWS credentials available (scan needs eks:DescribeClusterVersions). Configure OIDC to enable."
7283 echo " $EKS_K8S_SKIP_REASON"
729else
730 # ``--version-status STANDARD_SUPPORT`` returns every minor still in
731 # standard support — we don't want to flag the extended-support
732 # lifecycle as "newer." It must be the only selector: the API rejects
733 # combining it with ``--include-all`` ("Only one of the defaultOnly,
734 # clusterVersions, includeAll or status request parameters is accepted
735 # at a time"), which is exactly how this check silently broke once.
736 # stderr is captured into the skip reason so the next API-shape change
737 # is diagnosable from the report instead of reading as a generic skip.
73864 EKS_K8S_ERR_FILE="$(mktemp)"
73965 CLUSTER_VERSIONS_JSON="$(aws eks describe-cluster-versions \
740 --version-status STANDARD_SUPPORT \
741 --output json 2>"$EKS_K8S_ERR_FILE")" || CLUSTER_VERSIONS_JSON=""
742
74332 if [ -z "$CLUSTER_VERSIONS_JSON" ]; then
7443 EKS_K8S_ERR="$(head -n 1 "$EKS_K8S_ERR_FILE" 2>/dev/null | tr -d '\r')"
7451 EKS_K8S_SKIP_REASON="EKS Kubernetes version lookup failed${EKS_K8S_ERR:+: ${EKS_K8S_ERR}}"
7461 [ -z "$EKS_K8S_ERR" ] && EKS_K8S_SKIP_REASON="EKS Kubernetes version lookup returned an empty response."
7471 echo " $EKS_K8S_SKIP_REASON"
7481 rm -f "$EKS_K8S_ERR_FILE"
749 else
75031 rm -f "$EKS_K8S_ERR_FILE"
751 # Max of ``clusterVersion`` across all rows is the newest standard-
752 # support minor. We use Python for a proper numeric sort so 1.10
753 # beats 1.9 (sort -V already does this, but Python keeps the data
754 # wrangling in one place).
75594 LATEST_K8S="$(echo "$CLUSTER_VERSIONS_JSON" | python3 -c '
756import json, sys
757try:
758 data = json.load(sys.stdin)
759except Exception:
760 sys.exit(0)
761versions = sorted(
762 {row["clusterVersion"] for row in data.get("clusterVersions", [])},
763 key=lambda v: tuple(int(p) for p in v.split(".")),
764)
765print(versions[-1] if versions else "")
766' 2>/dev/null)" || LATEST_K8S=""
767
76831 if [ -z "$LATEST_K8S" ]; then
7692 EKS_K8S_SKIP_REASON="EKS Kubernetes version response contained no parseable standard-support versions."
7702 echo " $EKS_K8S_SKIP_REASON"
771 else
77258 CURRENT_K8S="$(extract_k8s_version "cdk.json")"
773
77429 if [ "$CURRENT_K8S" != "$LATEST_K8S" ] \
77514 && [ "$(compare_semver "$CURRENT_K8S" "$LATEST_K8S")" = "newer" ]; then
776 # Grab the standard-support end date for the currently-pinned
777 # minor. Blank when EKS hasn't published one yet (brand-new release).
77821 EOS_DATE="$(echo "$CLUSTER_VERSIONS_JSON" | python3 -c "
77921import json, sys
78021cv = sys.argv[1]
78121try:
78221 data = json.load(sys.stdin)
78321except Exception:
78421 sys.exit(0)
78521for row in data.get('clusterVersions', []):
78621 if row.get('clusterVersion') == cv:
78721 ts = row.get('endOfStandardSupportDate', '')
78821 # Strip time-of-day; the date is what the report cares about.
78921 print(str(ts).split('T', 1)[0].split(' ', 1)[0])
79021 break
79121" "$CURRENT_K8S" 2>/dev/null)" || EOS_DATE=""
792
7937 echo " - kubernetes_version: ${CURRENT_K8S} -> ${LATEST_K8S} (std support ends ${EOS_DATE:-unknown})"
7947 echo "kubernetes_version|${CURRENT_K8S}|${LATEST_K8S}|${EOS_DATE:-unknown}" >> "$EKS_K8S_RESULTS"
795 fi
796 fi
797 fi
798fi
799
800105EKS_K8S_COUNT="$(wc -l < "$EKS_K8S_RESULTS" 2>/dev/null | tr -d ' ')"
80135[ -z "$EKS_K8S_COUNT" ] && EKS_K8S_COUNT=0
802
803# ---------------------------------------------------------------------------
804# Aurora PostgreSQL engine versions (best-effort — requires AWS credentials)
805#
806# Checks whether the Aurora PostgreSQL engine version pinned in
807# regional_stack.py has a newer minor or major release available.
808# Uses the same credential gate as the EKS add-on check above.
809# ---------------------------------------------------------------------------
81035echo ""
81135echo "=== Checking Aurora PostgreSQL engine versions ==="
81235
81370AURORA_RESULTS="$(mktemp)"
81435AURORA_SKIP_REASON=""
815
81635if ! aws sts get-caller-identity >/dev/null 2>&1; then
8173 AURORA_SKIP_REASON="No AWS credentials available (scan needs rds:DescribeDBEngineVersions). Configure OIDC to enable."
8183 echo " $AURORA_SKIP_REASON"
819else
820 # The pinned Aurora PostgreSQL version (AURORA_POSTGRES_VERSION in
821 # gco/stacks/constants.py — a plain version string applied through
822 # AuroraPostgresEngineVersion.of(), so no CDK enum is involved).
82364 AURORA_VERSIONS="$(extract_aurora_versions "gco/stacks/regional_stack.py")"
82432 if [ -z "$AURORA_VERSIONS" ]; then
8251 AURORA_SKIP_REASON="Could not read AURORA_POSTGRES_VERSION from gco/stacks/constants.py."
8261 echo " $AURORA_SKIP_REASON"
827 else
82861 while read -r current_ver; do
82931 [ -z "$current_ver" ] && continue
83093 major="$(echo "$current_ver" | cut -d. -f1)"
831
832 # Query the latest available engine version for this major line.
833155 latest="$(aws rds describe-db-engine-versions \
834 --engine aurora-postgresql \
835 --query "DBEngineVersions[?starts_with(EngineVersion, '${major}.')].EngineVersion" \
836 --output text 2>/dev/null \
837 | tr '\t' '\n' | sort -V | tail -1)" || latest=""
838
83931 if ! [[ "$latest" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
8401 AURORA_SKIP_REASON="Aurora PostgreSQL engine lookup failed or returned an invalid version for major ${major}."
8411 echo " $AURORA_SKIP_REASON"
8421 break
843 fi
84430 if [ "$current_ver" != "$latest" ]; then
8458 echo " - aurora-postgresql: ${current_ver} -> ${latest}"
8468 echo "aurora-postgresql|${current_ver}|${latest}" >> "$AURORA_RESULTS"
847 fi
848 done <<< "$AURORA_VERSIONS"
849 fi
850fi
851
852105AURORA_COUNT="$(wc -l < "$AURORA_RESULTS" 2>/dev/null | tr -d ' ')"
85335[ -z "$AURORA_COUNT" ] && AURORA_COUNT=0
854
855# ---------------------------------------------------------------------------
856# EMR Serverless release labels (best-effort — requires AWS credentials)
857#
858# Checks whether the EMR Serverless release label pinned in
859# gco/stacks/constants.py has a newer release available. Uses the same
860# credential gate as the EKS add-on / Aurora checks above.
861#
862# AWS CLI note: the `list-release-labels` subcommand lives on the classic
863# `aws emr` service, not on `aws emr-serverless`. Classic EMR and EMR
864# Serverless share the same release-label namespace (e.g. emr-7.13.0),
865# so calling the classic service returns the labels usable by Serverless
866# applications. The IAM action is ``elasticmapreduce:ListReleaseLabels``
867# (which is what the OIDC policy grants) and is shared between the two
868# services — the CLI routing is just a surface-level difference.
869#
870# Two label families are in play. The classic line is ``emr-N.x.y``; the
871# current major line is the "AWS runtime for Apache Spark", ``emr-spark-N.x.y``
872# (``emr-spark-8.0.0`` GA, then ``emr-spark-8.1.0``), which the service lists
873# on the *last* page of a paged response. ``list_emr_release_labels`` follows
874# every page and ``newest_emr_label`` ranks by version across both families,
875# so a newer patch in the pinned line and a newer major in either family are
876# both reported (the latter as "new major available"). Both families are
877# valid release labels for the ``type: SPARK`` application GCO creates.
878# ---------------------------------------------------------------------------
87935echo ""
88035echo "=== Checking EMR Serverless release labels ==="
88135
88270EMR_RESULTS="$(mktemp)"
88335EMR_SKIP_REASON=""
884
88535if ! aws sts get-caller-identity >/dev/null 2>&1; then
8863 EMR_SKIP_REASON="No AWS credentials available (scan needs elasticmapreduce:ListReleaseLabels). Configure OIDC to enable."
8873 echo " $EMR_SKIP_REASON"
888else
88964 EMR_VERSIONS="$(extract_emr_versions "gco/stacks/constants.py")"
89032 if [ -z "$EMR_VERSIONS" ]; then
8911 EMR_SKIP_REASON="Could not read the EMR Serverless release-label pin from gco/stacks/constants.py."
8921 echo " $EMR_SKIP_REASON"
893 else
89460 while read -r current_label; do
89531 [ -z "$current_label" ] && continue
896 # current_label is "emr-7.14.0" or "emr-spark-8.1.0". Within the pinned
897 # major line, the newest GA label is a routine bump; the newest GA label
898 # across both families is the new-major signal. Preview, beta and
899 # release-candidate labels carry a suffix and never qualify.
90062 current_version="$(emr_label_version "$current_label")"
90131 major="${current_version%%.*}"
90262 release_labels="$(list_emr_release_labels us-east-1)" || release_labels=""
903
90431 if [ -z "$release_labels" ]; then
9051 EMR_SKIP_REASON="EMR release-label lookup failed or returned an empty response."
9061 echo " $EMR_SKIP_REASON"
9071 break
908 fi
909
91060 latest="$(newest_emr_label "$major" <<< "$release_labels")"
91160 latest_any="$(newest_emr_label <<< "$release_labels")"
912
91330 if [ -z "$latest_any" ]; then
9141 EMR_SKIP_REASON="EMR release-label response contained no parseable stable releases."
9151 echo " $EMR_SKIP_REASON"
9161 break
917 fi
918
91957 if [ -n "$latest" ] && [ "$current_label" != "$latest" ] \
92018 && [ "$(compare_semver "$current_version" "$(emr_label_version "$latest")")" = "newer" ]; then
9216 echo " - emr-serverless: ${current_label} -> ${latest}"
9226 echo "emr-serverless|${current_label}|${latest}" >> "$EMR_RESULTS"
92323 elif [ "$current_label" != "$latest_any" ] \
9246 && [ "$(compare_semver "$current_version" "$(emr_label_version "$latest_any")")" = "newer" ]; then
925 # No newer release in the pinned major line, but a newer major exists
926 # (today that is the emr-spark-8.x.y family).
9271 echo " - emr-serverless: ${current_label} -> ${latest_any} (new major available)"
9281 echo "emr-serverless|${current_label}|${latest_any}" >> "$EMR_RESULTS"
929 fi
930 done <<< "$EMR_VERSIONS"
931 fi
932fi
933
934105EMR_COUNT="$(wc -l < "$EMR_RESULTS" 2>/dev/null | tr -d ' ')"
93535[ -z "$EMR_COUNT" ] && EMR_COUNT=0
936
937# ---------------------------------------------------------------------------
938# Bedrock default model (best-effort — requires AWS credentials)
939#
940# Compares each configured Bedrock model default in cdk.json —
941# context.bedrock.mission_default_model_id (Mission sampling),
942# context.bedrock.capacity_advisor_default_model_id (the capacity
943# advisor), context.bedrock.claude_code_default_model_id (the session
944# model GCO Autopilot hands to Claude Code),
945# context.bedrock.codex_default_model_id (the session model it hands to Codex),
946# context.bedrock.opencode_default_model_id (the session model it hands to
947# OpenCode), and context.bedrock.embedding_model_id (Mission memory's
948# text-embedding model) — against the newest release in the SAME model
949# family. The generation keys compare against system-defined inference profiles
950# (aws bedrock list-inference-profiles); the embedding key is a plain
951# foundation model, so it compares against
952# aws bedrock list-foundation-models --by-output-modality EMBEDDING.
953# Every consumer resolves its key through gco.bedrock, so the scan and
954# the runtime paths cannot silently diverge; the keys are independent
955# knobs and each gets its own drift row.
956#
957# Same-family scoping (see bedrock_model_family) means we only flag a newer
958# release of the same model line (e.g. a newer global Amazon Nova Lite) — never a
959# different tier or provider, since switching those is a human decision,
960# not drift. When a newer release is reported, update the flagged key in
961# cdk.json; for the Mission key also re-capture the scaffold
962# fixture with scripts/capture_scaffold_fixtures.py. For the embedding
963# key, remember stored vectors are only comparable to vectors from the
964# same model: adopting a newer embedding model means re-embedding or
965# segregating existing Mission-memory data, not just bumping the pin.
966#
967# IAM actions: bedrock:ListInferenceProfiles and
968# bedrock:ListFoundationModels. Pinned to us-east-1 (the advisor +
969# Mission sampling + Mission memory default region) regardless of the
970# workflow's configured region. Same credential preflight as the EKS
971# add-on / Aurora / EMR checks.
972# ---------------------------------------------------------------------------
97335echo ""
97435echo "=== Checking Bedrock default model ==="
97535
97670BEDROCK_MODEL_RESULTS="$(mktemp)"
97735BEDROCK_MODEL_SKIP_REASON=""
978
97935if ! aws sts get-caller-identity >/dev/null 2>&1; then
9803 BEDROCK_MODEL_SKIP_REASON="No AWS credentials available (scan needs bedrock:ListInferenceProfiles). Configure OIDC to enable."
9813 echo " $BEDROCK_MODEL_SKIP_REASON"
982else
983192 for BEDROCK_MODEL_LEAF in mission_default_model_id capacity_advisor_default_model_id claude_code_default_model_id codex_default_model_id opencode_default_model_id embedding_model_id; do
984384 CURRENT_BEDROCK_MODEL="$(extract_default_bedrock_model cdk.json "$BEDROCK_MODEL_LEAF")"
985192 if [ -z "$CURRENT_BEDROCK_MODEL" ]; then
9866 BEDROCK_MODEL_SKIP_REASON="Could not read context.bedrock.${BEDROCK_MODEL_LEAF} from cdk.json."
9876 echo " $BEDROCK_MODEL_SKIP_REASON"
9886 continue
989 fi
990186 if [ "$BEDROCK_MODEL_LEAF" = "embedding_model_id" ]; then
991 # Embedding defaults are foundation models, not inference profiles.
99262 LATEST_BEDROCK_MODEL="$(get_latest_bedrock_embedding_model "$CURRENT_BEDROCK_MODEL" us-east-1)" || LATEST_BEDROCK_MODEL=""
993 else
994310 LATEST_BEDROCK_MODEL="$(get_latest_bedrock_model "$CURRENT_BEDROCK_MODEL" us-east-1)" || LATEST_BEDROCK_MODEL=""
995 fi
996186 if [ -z "$LATEST_BEDROCK_MODEL" ]; then
99712 BEDROCK_MODEL_SKIP_REASON="Bedrock model lookup failed or returned no active release in the model family of context.bedrock.${BEDROCK_MODEL_LEAF}."
99812 echo " $BEDROCK_MODEL_SKIP_REASON"
999174 elif [ "$CURRENT_BEDROCK_MODEL" != "$LATEST_BEDROCK_MODEL" ] \
100084 && [ "$(compare_bedrock_model "$CURRENT_BEDROCK_MODEL" "$LATEST_BEDROCK_MODEL")" = "newer" ]; then
100142 echo " - bedrock ${BEDROCK_MODEL_LEAF}: ${CURRENT_BEDROCK_MODEL} -> ${LATEST_BEDROCK_MODEL}"
100242 echo "context.bedrock.${BEDROCK_MODEL_LEAF}|${CURRENT_BEDROCK_MODEL}|${LATEST_BEDROCK_MODEL}" >> "$BEDROCK_MODEL_RESULTS"
1003 fi
1004 done
1005 # The vector store keeps its own embedding model at
1006 # context.vector_store.embedding_model_id (independent of mission memory's
1007 # bedrock.embedding_model_id by design). Same foundation-model drift check,
1008 # same re-embed caveat: adopting a newer model means re-ingesting the corpus.
1009 # The key is optional (the block ships with defaults), so absence is not a
1010 # skip condition for the whole check.
101164 VECTOR_STORE_MODEL="$(extract_default_bedrock_model cdk.json embedding_model_id vector_store)"
101232 if [ -n "$VECTOR_STORE_MODEL" ]; then
101362 LATEST_VECTOR_STORE_MODEL="$(get_latest_bedrock_embedding_model "$VECTOR_STORE_MODEL" us-east-1)" || LATEST_VECTOR_STORE_MODEL=""
101431 if [ -z "$LATEST_VECTOR_STORE_MODEL" ]; then
10152 BEDROCK_MODEL_SKIP_REASON="Bedrock model lookup failed or returned no active release in the model family of context.vector_store.embedding_model_id."
10162 echo " $BEDROCK_MODEL_SKIP_REASON"
101729 elif [ "$VECTOR_STORE_MODEL" != "$LATEST_VECTOR_STORE_MODEL" ] \
101814 && [ "$(compare_bedrock_model "$VECTOR_STORE_MODEL" "$LATEST_VECTOR_STORE_MODEL")" = "newer" ]; then
10197 echo " - vector_store embedding_model_id: ${VECTOR_STORE_MODEL} -> ${LATEST_VECTOR_STORE_MODEL}"
10207 echo "context.vector_store.embedding_model_id|${VECTOR_STORE_MODEL}|${LATEST_VECTOR_STORE_MODEL}" >> "$BEDROCK_MODEL_RESULTS"
1021 fi
1022 fi
1023fi
1024
1025105BEDROCK_MODEL_COUNT="$(wc -l < "$BEDROCK_MODEL_RESULTS" 2>/dev/null | tr -d ' ')"
102635[ -z "$BEDROCK_MODEL_COUNT" ] && BEDROCK_MODEL_COUNT=0
1027
1028
1029# ---------------------------------------------------------------------------
1030# Dockerfile.dev ARG pins
1031#
1032# Checks the tooling versions pinned in ``Dockerfile.dev`` (Node.js
1033# release, AWS CDK CLI, kubectl, AWS CLI v2, Docker CLI, Docker Buildx). These ARGs sit
1034# outside the main dependency surfaces above — Dependabot watches the
1035# ``FROM python:…`` base image but not the ARG pins — so drift here has
1036# historically gone undetected until someone rebuilt the image.
1037#
1038# Each pin has its own upstream:
1039#
1040# NODE_VERSION github://nodejs/Release → schedule.json (active LTS
1041# major) + nodejs.org/dist/index.json (newest release
1042# on that major)
1043# NPM_VERSION registry.npmjs.org/npm/latest
1044# CDK_VERSION registry.npmjs.org/aws-cdk/latest
1045# KUBECTL_VERSION https://dl.k8s.io/release/stable-<minor>.txt
1046# (minor from cdk.json::kubernetes_version)
1047# AWSCLI_VERSION github://aws/aws-cli/tags (v2.x.y semver, no GitHub Releases)
1048# DOCKER_VERSION github://moby/moby/releases/latest (``docker-v<ver>``)
1049# BUILDX_VERSION github://docker/buildx/releases/latest (v<ver>)
1050# UV_VERSION github://astral-sh/uv/releases/latest (bare semver)
1051# SESSION_MANAGER_PLUGIN_VERSION
1052# github://aws/session-manager-plugin/releases/latest
1053# (bare four-part version, e.g. 1.2.896.0)
1054#
1055# All endpoints are public — no AWS credentials needed.
1056# ---------------------------------------------------------------------------
105735echo ""
105835echo "=== Checking Dockerfile.dev ARG pins ==="
105935
106070DOCKERFILE_RESULTS="$(mktemp)"
106135DOCKERFILE_PIN_FILE="Dockerfile.dev"
1062
1063check_dockerfile_pin() {
1064297 local name="$1" current="$2" latest=""
10651023 case "$name" in
1066 NODE_VERSION)
1067 # Two drift signals folded into one compare. First pick the
1068 # highest major with an active LTS window from the release
1069 # schedule (lts <= today AND (end missing or end > today)),
1070 # then resolve that major's newest release from the official
1071 # dist index — the same origin the Dockerfile downloads from.
1072 # A new LTS line and a new patch on the current line both
1073 # surface as ``newer``.
107433 local lts_major
1075100 lts_major="$(curl -fsSL --max-time 15 "${SCAN_CURL_RETRY[@]}" \
1076 "https://raw.githubusercontent.com/nodejs/Release/main/schedule.json" 2>/dev/null \
1077 | python3 -c '
1078import sys, json, datetime
1079try:
1080 data = json.load(sys.stdin)
1081except Exception:
1082 sys.exit(0)
1083today = datetime.date.today().isoformat()
1084candidates = []
1085for k, v in data.items():
1086 if not k.startswith("v") or "lts" not in v:
1087 continue
1088 if v["lts"] > today:
1089 continue
1090 if v.get("end", "9999-12-31") <= today:
1091 continue
1092 try:
1093 candidates.append(int(k[1:]))
1094 except ValueError:
1095 continue
1096if candidates:
1097 print(max(candidates))
1098' 2>/dev/null)" || true
109933 if [ -z "$lts_major" ]; then
11001 mark_scan_incomplete "Node.js release-schedule lookup failed or returned no active LTS major."
11011 return
1102 fi
1103 # index.json is newest-first per line, so the first entry whose
1104 # version sits on the LTS major is that major's latest release.
110596 latest="$(curl -fsSL --max-time 15 "${SCAN_CURL_RETRY[@]}" \
1106 "https://nodejs.org/dist/index.json" 2>/dev/null \
1107 | jq -r --arg prefix "v${lts_major}." \
1108 '[.[].version | select(startswith($prefix))][0] // empty' 2>/dev/null)" || true
1109 ;;
1110 CDK_VERSION)
1111100 latest="$(curl -fsSL --max-time 15 "${SCAN_CURL_RETRY[@]}" \
1112 "https://registry.npmjs.org/aws-cdk/latest" 2>/dev/null \
1113 | jq -r '.version // empty' 2>/dev/null)" || true
1114 ;;
1115 NPM_VERSION)
1116 # ``npm`` is part of the dev container's pinned tooling — the
1117 # npm bundled inside the Node dist tarball is fixed per Node
1118 # release but lags npm's own line, so the Dockerfile installs a
1119 # specific ``npm@X.Y.Z`` to keep rebuilds reproducible (same
1120 # rationale as CDK_VERSION above). The canonical "latest" is the
1121 # ``latest`` dist-tag on npmjs.org, same source CDK uses.
1122100 latest="$(curl -fsSL --max-time 15 "${SCAN_CURL_RETRY[@]}" \
1123 "https://registry.npmjs.org/npm/latest" 2>/dev/null \
1124 | jq -r '.version // empty' 2>/dev/null)" || true
1125 ;;
1126 KUBECTL_VERSION)
1127 # Match the minor line already committed to cdk.json so the pin
1128 # and the EKS cluster stay within the ±1 minor skew policy.
112933 local k8s_minor
113066 k8s_minor="$(extract_k8s_version "cdk.json")"
1131100 latest="$(curl -fsSL --max-time 15 "${SCAN_CURL_RETRY[@]}" \
1132 "https://dl.k8s.io/release/stable-${k8s_minor}.txt" 2>/dev/null | tr -d '[:space:]')" || true
1133 ;;
1134 AWSCLI_VERSION)
1135 # aws/aws-cli doesn't publish GitHub Releases for v2; tags are the
1136 # canonical source. First page (per_page=20) is newest-first;
1137 # filter to 2.x.y semver and take the top match.
1138102 latest="$(github_api_get \
1139 "https://api.github.com/repos/aws/aws-cli/tags?per_page=20" \
1140 | jq -r '[.[].name | select(test("^2\\.[0-9]+\\.[0-9]+$"))][0] // empty' 2>/dev/null)" || true
1141 ;;
1142 DOCKER_VERSION)
1143 # moby/moby tags releases as ``docker-v<semver>``; strip the
1144 # prefix so compare_semver can handle the value.
1145135 latest="$(github_api_get \
1146 "https://api.github.com/repos/moby/moby/releases/latest" \
1147 | jq -r '.tag_name // empty' 2>/dev/null \
1148 | sed -E 's/^(docker-)?v//')" || true
1149 ;;
1150 BUILDX_VERSION)
1151 # docker/buildx publishes GitHub Releases tagged v<semver>
1152 # (e.g. v0.35.0). The release tag is the canonical source;
1153 # compare_semver strips the leading v on both sides. The
1154 # Dockerfile installs the plugin binary buildx-<tag>.linux-<arch>.
1155102 latest="$(github_api_get \
1156 "https://api.github.com/repos/docker/buildx/releases/latest" \
1157 | jq -r '.tag_name // empty' 2>/dev/null)" || true
1158 ;;
1159 UV_VERSION)
1160 # astral-sh/uv publishes GitHub Releases tagged with a bare semver
1161 # (e.g. 0.12.1). The dev container ships uv/uvx for the `gco
1162 # autopilot` companion MCP servers; bump the two SHA256 ARGs in
1163 # lockstep from the per-artifact *.sha256 release files.
1164102 latest="$(github_api_get \
1165 "https://api.github.com/repos/astral-sh/uv/releases/latest" \
1166 | jq -r '.tag_name // empty' 2>/dev/null)" || true
1167 ;;
1168 SESSION_MANAGER_PLUGIN_VERSION)
1169 # aws/session-manager-plugin tags releases with the bare four-part
1170 # version the S3 download path embeds (1.2.896.0). Bump the two
1171 # SESSION_MANAGER_PLUGIN_SHA256_* ARGs from the new .deb packages.
1172102 latest="$(github_api_get \
1173 "https://api.github.com/repos/aws/session-manager-plugin/releases/latest" \
1174 | jq -r '.tag_name // empty' 2>/dev/null)" || true
1175 ;;
1176 esac
1177 # extract_dockerfile_pins only emits the names above. Should its allowlist
1178 # ever grow without a matching arm here, ``latest`` stays empty and the
1179 # pin is reported as an incomplete lookup below, rather than skipped in
1180 # silence.
1181
1182296 if [ -z "$latest" ]; then
118333 case "$name" in
1184 AWSCLI_VERSION|DOCKER_VERSION|BUILDX_VERSION|UV_VERSION|SESSION_MANAGER_PLUGIN_VERSION)
118530 mark_scan_incomplete "Upstream version lookup failed for Dockerfile.dev pin ${name}$(github_api_failure_hint)."
1186 ;;
1187 *)
11883 mark_scan_incomplete "Upstream version lookup failed for Dockerfile.dev pin ${name}."
1189 ;;
1190 esac
119118 return
1192 fi
1193
1194 # Every pin is a semver, NODE_VERSION included. compare_semver strips
1195 # a leading ``v`` on both sides, matching the kubectl and buildx pins
1196 # that keep the prefix.
1197278 local relation
1198556 relation="$(compare_semver "$current" "$latest")"
1199
1200278 if [ "$relation" = "newer" ]; then
120172 echo " - ${name}: ${current} -> ${latest}"
120272 echo "${name}|${current}|${latest}" >> "$DOCKERFILE_RESULTS"
1203 fi
1204}
1205
120635if [ -f "$DOCKERFILE_PIN_FILE" ]; then
120768 DOCKERFILE_PINS="$(extract_dockerfile_pins "$DOCKERFILE_PIN_FILE")"
120834 if [ -z "$DOCKERFILE_PINS" ]; then
12091 mark_scan_incomplete "Could not parse tooling pins from ${DOCKERFILE_PIN_FILE}."
1210 fi
1211664 while IFS='|' read -r pin_name pin_value; do
1212299 [ -z "$pin_name" ] && continue
1213297 check_dockerfile_pin "$pin_name" "$pin_value"
1214 done <<< "$DOCKERFILE_PINS"
1215else
12161 mark_scan_incomplete "$DOCKERFILE_PIN_FILE is missing."
1217fi
1218
1219105DOCKERFILE_COUNT="$(wc -l < "$DOCKERFILE_RESULTS" 2>/dev/null | tr -d ' ')"
122035[ -z "$DOCKERFILE_COUNT" ] && DOCKERFILE_COUNT=0
1221
1222# ---------------------------------------------------------------------------
1223# GCO Autopilot pins (agent CLI releases + companion MCP servers)
1224#
1225# ``gco autopilot`` (cli/autopilot.py) carries four dependency surfaces that
1226# live in Python constants, invisible to Dependabot and to every sweep above:
1227#
1228# CLAUDE_CODE_VERSION exact @anthropic-ai/claude-code release installed
1229# by the default engine on first use.
1230# CODEX_VERSION exact @openai/codex release installed by the Codex
1231# engine on first use.
1232# OPENCODE_VERSION exact opencode-ai release installed by the
1233# OpenCode engine on first use.
1234# COMPANION_MCP_SERVERS the npx/uvx-launched companion MCP servers wired
1235# into every autopilot session. Nothing pins them
1236# (they resolve at launch), so the risk isn't
1237# staleness — it's disappearance: a package that is
1238# unpublished, deprecated, or yanked breaks every
1239# new session. Each is resolved on its registry and
1240# reported when unhealthy. This is exactly how
1241# mcp-server-fetch and mcp-server-calculator broke
1242# before being pruned in 2026-08.
1243#
1244# Remediation for companion findings: replace or drop the server in
1245# cli/autopilot.py *and* the "Recommended Companion MCP Servers" tables in
1246# gco_mcp/README.md — tests/test_cli_autopilot.py fails the PR until the two
1247# agree. All endpoints are public — no AWS credentials needed.
1248# ---------------------------------------------------------------------------
124935echo ""
125035echo "=== Checking GCO Autopilot pins ==="
125135
125270AUTOPILOT_RESULTS="$(mktemp)"
125335AUTOPILOT_SKIP_REASON=""
125435AUTOPILOT_SOURCE="cli/autopilot.py"
1255
125635if [ ! -f "$AUTOPILOT_SOURCE" ]; then
12572 AUTOPILOT_SKIP_REASON="${AUTOPILOT_SOURCE} not found."
12582 echo " $AUTOPILOT_SKIP_REASON"
1259else
1260 check_autopilot_npm_pin() {
126199 local constant_name="$1"
126299 local package_name="$2"
126399 local extractor="$3"
126499 local pin="" package_status="" latest="" verdict=""
126599 local package_url="https://www.npmjs.com/package/${package_name}"
1266
1267198 pin="$("$extractor" "$AUTOPILOT_SOURCE")"
126899 if [ -z "$pin" ]; then
12693 AUTOPILOT_SKIP_REASON="${constant_name} not found in ${AUTOPILOT_SOURCE}."
12703 echo " $AUTOPILOT_SKIP_REASON"
12713 return
1272 fi
1273
1274192 package_status="$(get_registry_package_status npm "$package_name")"
127596 if [ -z "$package_status" ]; then
12763 AUTOPILOT_SKIP_REASON="npm lookup for ${package_name} failed (network)."
12773 echo " $AUTOPILOT_SKIP_REASON"
12783 return
1279 fi
1280
128193 latest="${package_status#*|}"
128293 case "$package_status" in
1283 ok\|*)
128490 if [ -n "$latest" ] \
1285180 && [ "$(compare_semver "$pin" "$latest")" = "newer" ]; then
128624 echo " - ${constant_name}: ${pin} -> ${latest}"
128724 echo "${package_name} (${constant_name})|${pin}|${latest}|${package_url}" >> "$AUTOPILOT_RESULTS"
1288 fi
1289 ;;
1290 *)
12913 verdict="${package_status%%|*}"
12923 echo " - ${package_name}: ${verdict}"
12933 echo "${package_name} (${constant_name})|${pin}|${verdict}|${package_url}" >> "$AUTOPILOT_RESULTS"
1294 ;;
1295 esac
1296 }
1297
129833 check_autopilot_npm_pin \
1299 "CLAUDE_CODE_VERSION" "@anthropic-ai/claude-code" extract_claude_code_pin
130033 check_autopilot_npm_pin \
1301 "CODEX_VERSION" "@openai/codex" extract_codex_pin
130233 check_autopilot_npm_pin \
1303 "OPENCODE_VERSION" "opencode-ai" extract_opencode_pin
1304
1305 # Companion MCP server liveness. Missing/deprecated/yanked is drift; a
1306 # network failure marks the scan incomplete rather than inventing findings.
1307234 while IFS='|' read -r companion_name companion_registry companion_package; do
130884 [ -z "$companion_name" ] && continue
1309168 companion_status="$(get_registry_package_status "$companion_registry" "$companion_package")"
131084 if [ -z "$companion_status" ]; then
13114 if [ -z "$AUTOPILOT_SKIP_REASON" ]; then
13121 AUTOPILOT_SKIP_REASON="Registry lookup failed for ${companion_package} (${companion_registry}); companion liveness incomplete."
13131 echo " $AUTOPILOT_SKIP_REASON"
1314 fi
13154 continue
1316 fi
131780 case "$companion_status" in
1318 ok\|*)
1319 ;;
1320 *)
13214 companion_verdict="${companion_status%%|*}"
13224 companion_detail="${companion_status#*|}"
13236 [ -n "$companion_detail" ] && companion_verdict="${companion_verdict}: ${companion_detail}"
13244 if [ "$companion_registry" = "npm" ]; then
13252 companion_url="https://www.npmjs.com/package/${companion_package}"
1326 else
13272 companion_url="https://pypi.org/project/${companion_package}/"
1328 fi
13294 echo " - companion ${companion_name}: ${companion_verdict}"
13304 echo "companion ${companion_name} (${companion_registry}: ${companion_package})|launch-time (unpinned)|${companion_verdict}|${companion_url}" >> "$AUTOPILOT_RESULTS"
1331 ;;
1332 esac
1333 done < <(extract_companion_mcp_packages "$AUTOPILOT_SOURCE")
133433fi
1335
1336105AUTOPILOT_COUNT="$(wc -l < "$AUTOPILOT_RESULTS" 2>/dev/null | tr -d ' ')"
133735[ -z "$AUTOPILOT_COUNT" ] && AUTOPILOT_COUNT=0
1338
1339# ---------------------------------------------------------------------------
1340# Pre-commit hook revisions
1341#
1342# Compares the ``rev:`` pinned for each ``repo:`` block in
1343# ``.pre-commit-config.yaml`` against the latest semver-shaped tag
1344# published by the upstream Git host. Dependabot has a pre-commit
1345# ecosystem, but .github/dependabot.yml leaves it off on purpose: the
1346# ruff, mypy, and yamllint hooks stay in lockstep with their pins in
1347# requirements-lock.txt, which Dependabot does not manage, so this
1348# report is what surfaces hook drift. It matters in practice because
1349# stale hook pins quietly miss new lint rules and bug fixes.
1350#
1351# Each hook's repo URL is resolved to a tag list via the GitHub API
1352# (the only host we use today). Full SHA-1/SHA-256 object ids are accepted as
1353# immutable exemptions. Other unsupported refs (branches, floating labels,
1354# prereleases) mark the scan incomplete rather than silently disappearing.
1355# Calls are unauthenticated; we make one request per hook, which is well below
1356# the 60 req/h public limit.
1357# ---------------------------------------------------------------------------
135835echo ""
135935echo "=== Checking pre-commit hook revisions ==="
136035
136170PRECOMMIT_RESULTS="$(mktemp)"
136235PRECOMMIT_CONFIG=".pre-commit-config.yaml"
1363
136435if [ -f "$PRECOMMIT_CONFIG" ]; then
136568 PRECOMMIT_HOOKS="$(extract_precommit_hooks "$PRECOMMIT_CONFIG")"
136634 if [ -z "$PRECOMMIT_HOOKS" ]; then
13671 mark_scan_incomplete "Could not parse hook pins from ${PRECOMMIT_CONFIG}."
1368 fi
1369216 while IFS='|' read -r repo current_rev; do
137075 [ -z "$repo" ] && continue
137173 [ -z "$current_rev" ] && continue
1372 # Complete Git object ids are immutable and intentionally have no release
1373 # drift lookup. Every other non-semver ref may move or cannot be compared
1374 # safely, so it makes this scan incomplete instead of receiving a blanket
1375 # "SHA" exemption.
137673 if is_full_git_commit_sha "$current_rev"; then
13771 continue
1378 fi
137972 if ! [[ "$current_rev" =~ ^v?[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
13801 mark_scan_incomplete "Unsupported mutable or non-semver pre-commit rev '${current_rev}' for ${repo}."
13811 continue
1382 fi
1383
1384142 latest_rev="$(get_latest_precommit_hook_release "$repo")"
138571 if [ -z "$latest_rev" ]; then
138614 mark_scan_incomplete "Pre-commit tag lookup failed for ${repo}$(github_api_failure_hint)."
13877 continue
1388 fi
1389
1390 # Strip ``v`` so compare_semver ranks ``v0.22.1`` vs ``v0.22.2``
1391 # (and the rare unprefixed ``1.38.0`` from yamllint historically)
1392 # consistently. We keep the original ``current_rev`` / ``latest_rev``
1393 # strings in the report so the operator copy-pastes the exact
1394 # value pre-commit expects.
139564 if [ "$current_rev" != "$latest_rev" ] \
139640 && [ "$(compare_semver "$current_rev" "$latest_rev")" = "newer" ]; then
139720 echo " - ${repo}: ${current_rev} -> ${latest_rev}"
139820 echo "${repo}|${current_rev}|${latest_rev}" >> "$PRECOMMIT_RESULTS"
1399 fi
1400 done <<< "$PRECOMMIT_HOOKS"
1401else
14021 mark_scan_incomplete "$PRECOMMIT_CONFIG is missing."
1403fi
1404
1405105PRECOMMIT_COUNT="$(wc -l < "$PRECOMMIT_RESULTS" 2>/dev/null | tr -d ' ')"
140635[ -z "$PRECOMMIT_COUNT" ] && PRECOMMIT_COUNT=0
1407
1408# ---------------------------------------------------------------------------
1409# CDK enum constants
1410#
1411# Compares the CDK-enum-name constants pinned in
1412# ``gco/stacks/constants.py`` against the highest enum members exposed
1413# by the installed ``aws-cdk-lib``. This catches the case where
1414# aws-cdk-lib already supports a newer enum (because we bumped the
1415# library, or simply because the latest published release added one)
1416# but ``constants.py`` still pins an older one.
1417#
1418# Two enums are tracked today:
1419#
1420# - ``LAMBDA_PYTHON_RUNTIME`` → ``aws_cdk.aws_lambda.Runtime.PYTHON_X_Y``
1421# - ``LAMBDA_NODEJS_RUNTIME`` → ``aws_cdk.aws_lambda.Runtime.NODEJS_<major>_X``
1422#
1423# The Aurora engine deliberately is NOT an enum: constants.py pins a plain
1424# version string applied through ``AuroraPostgresEngineVersion.of()``, and
1425# the "Aurora PostgreSQL engine" section validates it against the live RDS
1426# API — the authoritative source — instead of the CDK library's catalog.
1427#
1428# The deps-scan workflow installs the latest ``aws-cdk-lib`` for this
1429# section; locally the helper just reflects whatever's already on the
1430# active interpreter. If aws-cdk-lib isn't importable we skip with a
1431# one-line note (mirrors the AWS-creds skip pattern used elsewhere).
1432# ---------------------------------------------------------------------------
143335echo ""
143435echo "=== Checking CDK enum constants ==="
143535
143670CDK_ENUM_RESULTS="$(mktemp)"
143735CDK_ENUM_SKIP_REASON=""
1438
143935if ! python3 -c "import aws_cdk" 2>/dev/null; then
14402 CDK_ENUM_SKIP_REASON="aws-cdk-lib not importable. Install with 'pip install aws-cdk-lib' to enable."
14412 echo " $CDK_ENUM_SKIP_REASON"
1442else
1443 # Lambda Python runtime enum
144466 LAMBDA_RT_CURRENT="$(extract_constant_value LAMBDA_PYTHON_RUNTIME)"
144566 LAMBDA_RT_LATEST="$(get_latest_lambda_python_runtime)"
144665 if [ -z "$LAMBDA_RT_CURRENT" ] || [ -z "$LAMBDA_RT_LATEST" ]; then
14472 mark_scan_incomplete "Could not parse the current or latest Lambda Python runtime enum."
144831 elif [ "$LAMBDA_RT_CURRENT" != "$LAMBDA_RT_LATEST" ]; then
1449 # Convert PYTHON_3_14 → 3.14 so compare_semver can rank them.
145021 cur_v="$(echo "$LAMBDA_RT_CURRENT" | sed -E 's/^PYTHON_([0-9]+)_([0-9]+)$/\1.\2/')"
145121 lat_v="$(echo "$LAMBDA_RT_LATEST" | sed -E 's/^PYTHON_([0-9]+)_([0-9]+)$/\1.\2/')"
145214 if [ "$(compare_semver "$cur_v" "$lat_v")" = "newer" ]; then
14537 echo " - LAMBDA_PYTHON_RUNTIME: ${LAMBDA_RT_CURRENT} -> ${LAMBDA_RT_LATEST}"
14547 echo "LAMBDA_PYTHON_RUNTIME|aws_lambda.Runtime|${LAMBDA_RT_CURRENT}|${LAMBDA_RT_LATEST}" >> "$CDK_ENUM_RESULTS"
1455 fi
1456 fi
1457
1458 # Lambda Node.js runtime enum
145966 LAMBDA_NODE_RT_CURRENT="$(extract_constant_value LAMBDA_NODEJS_RUNTIME)"
146066 LAMBDA_NODE_RT_LATEST="$(get_latest_lambda_nodejs_runtime)"
146165 if [ -z "$LAMBDA_NODE_RT_CURRENT" ] || [ -z "$LAMBDA_NODE_RT_LATEST" ]; then
14622 mark_scan_incomplete "Could not parse the current or latest Lambda Node.js runtime enum."
146331 elif [ "$LAMBDA_NODE_RT_CURRENT" != "$LAMBDA_NODE_RT_LATEST" ]; then
14647 cur_major="${LAMBDA_NODE_RT_CURRENT#NODEJS_}"
14657 cur_major="${cur_major%_X}"
14667 lat_major="${LAMBDA_NODE_RT_LATEST#NODEJS_}"
14677 lat_major="${lat_major%_X}"
146814 if [[ "$cur_major" =~ ^[0-9]+$ ]] && [[ "$lat_major" =~ ^[0-9]+$ ]] \
14697 && [ "$lat_major" -gt "$cur_major" ]; then
14707 echo " - LAMBDA_NODEJS_RUNTIME: ${LAMBDA_NODE_RT_CURRENT} -> ${LAMBDA_NODE_RT_LATEST}"
14717 echo "LAMBDA_NODEJS_RUNTIME|aws_lambda.Runtime|${LAMBDA_NODE_RT_CURRENT}|${LAMBDA_NODE_RT_LATEST}" >> "$CDK_ENUM_RESULTS"
1472 fi
1473 fi
1474
1475fi
1476
1477105CDK_ENUM_COUNT="$(wc -l < "$CDK_ENUM_RESULTS" 2>/dev/null | tr -d ' ')"
147835[ -z "$CDK_ENUM_COUNT" ] && CDK_ENUM_COUNT=0
1479
1480# ---------------------------------------------------------------------------
1481# Python release
1482#
1483# Compares the Lambda Python runtime constant (which encodes the major
1484# Python version we standardise on across every Lambda in the project)
1485# against the latest stable Python release on endoflife.date.
1486#
1487# This is informational drift — Lambda may not ship support for a brand-
1488# new Python release for several months — but the signal is useful so
1489# the operator knows when to start planning a runtime bump. It also
1490# complements the CDK-enum check above: that check answers "what does
1491# aws-cdk-lib expose?", this one answers "what has python.org actually
1492# shipped?".
1493# ---------------------------------------------------------------------------
149435echo ""
149535echo "=== Checking Python release ==="
149635
149770PYTHON_RELEASE_RESULTS="$(mktemp)"
149835PYTHON_RELEASE_SKIP_REASON=""
1499
1500# Re-read in case the CDK section was skipped and never set the var.
150138LAMBDA_RT_CURRENT="${LAMBDA_RT_CURRENT:-$(extract_constant_value LAMBDA_PYTHON_RUNTIME)}"
150270LATEST_PYTHON="$(get_latest_python_release)"
1503
150435if [ -z "$LATEST_PYTHON" ]; then
15052 PYTHON_RELEASE_SKIP_REASON="endoflife.date query failed (network or schema change)."
15062 echo " $PYTHON_RELEASE_SKIP_REASON"
150733elif [ -z "$LAMBDA_RT_CURRENT" ]; then
15081 PYTHON_RELEASE_SKIP_REASON="Could not parse LAMBDA_PYTHON_RUNTIME for the Python release comparison."
15091 echo " $PYTHON_RELEASE_SKIP_REASON"
1510else
151196 cur_v="$(echo "$LAMBDA_RT_CURRENT" | sed -E 's/^PYTHON_([0-9]+)_([0-9]+)$/\1.\2/')"
151232 if [ "$cur_v" != "$LATEST_PYTHON" ] \
151318 && [ "$(compare_semver "$cur_v" "$LATEST_PYTHON")" = "newer" ]; then
15148 echo " - python (LAMBDA_PYTHON_RUNTIME): ${cur_v} -> ${LATEST_PYTHON}"
15158 echo "python|${cur_v}|${LATEST_PYTHON}" >> "$PYTHON_RELEASE_RESULTS"
1516 fi
1517fi
1518
1519105PYTHON_RELEASE_COUNT="$(wc -l < "$PYTHON_RELEASE_RESULTS" 2>/dev/null | tr -d ' ')"
152035[ -z "$PYTHON_RELEASE_COUNT" ] && PYTHON_RELEASE_COUNT=0
1521
1522# ---------------------------------------------------------------------------
1523# Ruby release
1524#
1525# Ruby is a CI-only toolchain: bashcov (see the Gemfile) measures which lines
1526# of each shell script the BATS suite executes, and the unit:bats:shell job
1527# gets its interpreter from .ruby-version via ruby/setup-ruby. Dependabot
1528# watches the *gems* in Gemfile.lock but has nothing to say about the
1529# interpreter series, so — exactly like .python-version — the pin is compared
1530# against the newest supported series here. Informational, not a failure: a new
1531# Ruby series is a deliberate move, not a security patch.
1532# ---------------------------------------------------------------------------
153335echo ""
153435echo "=== Checking Ruby release ==="
153535
153670RUBY_RELEASE_RESULTS="$(mktemp)"
153735RUBY_RELEASE_SKIP_REASON=""
1538
153970RUBY_PIN_CURRENT="$(read_ruby_version_pin .ruby-version)"
154070LATEST_RUBY="$(get_latest_ruby_release)"
1541
154235if [ -z "$LATEST_RUBY" ]; then
15432 RUBY_RELEASE_SKIP_REASON="endoflife.date query failed (network or schema change)."
15442 echo " $RUBY_RELEASE_SKIP_REASON"
154533elif [ -z "$RUBY_PIN_CURRENT" ]; then
15461 RUBY_RELEASE_SKIP_REASON="Could not parse .ruby-version for the Ruby release comparison."
15471 echo " $RUBY_RELEASE_SKIP_REASON"
1548else
154932 if [ "$RUBY_PIN_CURRENT" != "$LATEST_RUBY" ] \
155062 && [ "$(compare_semver "$RUBY_PIN_CURRENT" "$LATEST_RUBY")" = "newer" ]; then
15518 echo " - ruby (.ruby-version): ${RUBY_PIN_CURRENT} -> ${LATEST_RUBY}"
15528 echo "ruby|${RUBY_PIN_CURRENT}|${LATEST_RUBY}" >> "$RUBY_RELEASE_RESULTS"
1553 else
155424 echo " .ruby-version pins ${RUBY_PIN_CURRENT}; newest supported series is ${LATEST_RUBY}."
1555 fi
1556fi
1557
1558105RUBY_RELEASE_COUNT="$(wc -l < "$RUBY_RELEASE_RESULTS" 2>/dev/null | tr -d ' ')"
155935[ -z "$RUBY_RELEASE_COUNT" ] && RUBY_RELEASE_COUNT=0
1560
1561# ---------------------------------------------------------------------------
1562# Ubuntu LTS base (Slinky Slurm images)
1563#
1564# Slinky publishes every Slurm image per Ubuntu LTS base
1565# (``<slurm>-ubuntu24.04``, ``<slurm>-ubuntu26.04``) and GCO pins one base
1566# through SLURM_IMAGE_TAG in gco/slurm_config.py. The image sweep above
1567# deliberately compares within a tag's variant family — a newer Slurm patch
1568# on the same base is drift there, a newer base never is — so without this
1569# section the pin could ride an Ubuntu release towards end of life with
1570# nothing in the report saying so. The cue fires once the newest LTS has
1571# shipped its first point release (get_latest_ubuntu_lts) and the registry
1572# carries the pinned Slurm version on that base; until upstream publishes
1573# that variant there is nothing to move to, and the section says so without
1574# a finding. Informational: changing base is a deliberate move (re-run the
1575# live Slurm validation), not a security patch.
1576# ---------------------------------------------------------------------------
157735echo ""
157835echo "=== Checking Ubuntu LTS base (Slurm images) ==="
157935
158070UBUNTU_LTS_RESULTS="$(mktemp)"
158135UBUNTU_LTS_SKIP_REASON=""
1582
158370SLURM_IMAGE_TAG_CURRENT="$(extract_python_string_constant SLURM_IMAGE_TAG gco/slurm_config.py)"
158470SLURM_IMAGE_UBUNTU="$(ubuntu_variant_release "$SLURM_IMAGE_TAG_CURRENT")"
1585# The registry probe uses slurmd: every Slinky daemon image shares the one
1586# tag, and the chart-values walk above already yielded its repository.
1587140SLURMD_REPOSITORY="$(printf '%s\n' "${CHART_VALUE_IMAGES:-}" | sed -nE 's#^([^|]*/slurmd):.*$#\1#p' | head -1)"
158835if [ -z "$SLURM_IMAGE_TAG_CURRENT" ]; then
15891 UBUNTU_LTS_SKIP_REASON="Could not parse SLURM_IMAGE_TAG from gco/slurm_config.py for the Ubuntu LTS comparison."
15901 echo " $UBUNTU_LTS_SKIP_REASON"
159134elif [ -z "$SLURM_IMAGE_UBUNTU" ]; then
15921 echo " SLURM_IMAGE_TAG ${SLURM_IMAGE_TAG_CURRENT} is not an Ubuntu variant; nothing to compare."
1593else
159466 LATEST_UBUNTU_LTS="$(get_latest_ubuntu_lts)"
159533 if [ -z "$LATEST_UBUNTU_LTS" ]; then
15962 UBUNTU_LTS_SKIP_REASON="endoflife.date query failed (network or schema change)."
15972 echo " $UBUNTU_LTS_SKIP_REASON"
159862 elif [ "$(compare_semver "$SLURM_IMAGE_UBUNTU" "$LATEST_UBUNTU_LTS")" != "newer" ]; then
159923 echo " Slurm images are on ubuntu${SLURM_IMAGE_UBUNTU}; newest adoptable Ubuntu LTS is ${LATEST_UBUNTU_LTS}."
16008 elif [ -z "$SLURMD_REPOSITORY" ]; then
16011 UBUNTU_LTS_SKIP_REASON="Could not find the slurmd image repository in lambda/helm-installer/charts.yaml for the Ubuntu LTS comparison."
16021 echo " $UBUNTU_LTS_SKIP_REASON"
1603 else
16047 UBUNTU_LTS_CANDIDATE="${SLURM_IMAGE_TAG_CURRENT%-ubuntu*}-ubuntu${LATEST_UBUNTU_LTS}"
16057 slurmd_tags=""
160627 if ! slurmd_tags="$(skopeo list-tags "${SCAN_SKOPEO_RETRY[@]}" "docker://${SLURMD_REPOSITORY}" 2>/dev/null \
1607 | jq -r '.Tags[]' 2>/dev/null)" || [ -z "$slurmd_tags" ]; then
16081 mark_scan_incomplete "Container registry tag lookup failed for ${SLURMD_REPOSITORY} (Ubuntu LTS comparison)."
16096 elif tag_listed "$UBUNTU_LTS_CANDIDATE" "$slurmd_tags"; then
16105 echo " - SLURM_IMAGE_TAG (gco/slurm_config.py): ${SLURM_IMAGE_TAG_CURRENT} -> ${UBUNTU_LTS_CANDIDATE} (Ubuntu ${LATEST_UBUNTU_LTS} LTS)"
16115 echo "SLURM_IMAGE_TAG (gco/slurm_config.py)|${SLURM_IMAGE_TAG_CURRENT}|${UBUNTU_LTS_CANDIDATE}" >> "$UBUNTU_LTS_RESULTS"
1612 else
16131 echo " Ubuntu ${LATEST_UBUNTU_LTS} LTS is adoptable, but ${SLURMD_REPOSITORY} does not publish ${UBUNTU_LTS_CANDIDATE} yet; nothing to move to."
1614 fi
1615 fi
1616fi
1617
1618105UBUNTU_LTS_COUNT="$(wc -l < "$UBUNTU_LTS_RESULTS" 2>/dev/null | tr -d ' ')"
161935[ -z "$UBUNTU_LTS_COUNT" ] && UBUNTU_LTS_COUNT=0
1620
1621# ---------------------------------------------------------------------------
1622# Runner images
1623#
1624# `runs-on: ubuntu-latest` is not a version pin, so Dependabot has nothing to
1625# bump and the platform underneath every job changes only when GitHub moves the
1626# label. Two opposite mistakes are possible: staying on an explicitly pinned
1627# image long after a newer one is generally available (and eventually on one
1628# upstream has marked deprecated, which is a removal notice), or chasing an
1629# image that is still in preview.
1630#
1631# check_runner_images.py reports those separately: `--format rows` is drift to
1632# act on (newer GA, or deprecated), `--format notes` is context (a newer image
1633# exists but is preview, so the current pin is deliberate). It exits 2 without
1634# printing anything when the upstream catalog cannot be read, which is treated
1635# as a skip here rather than as an all-clear.
1636# ---------------------------------------------------------------------------
163735echo ""
163835echo "=== Checking runner images ==="
163935
164070RUNNER_IMAGE_RESULTS="$(mktemp)"
164170RUNNER_IMAGE_NOTES="$(mktemp)"
164235RUNNER_IMAGE_SKIP_REASON=""
1643
164435if python3 .github/scripts/check_runner_images.py --format rows > "$RUNNER_IMAGE_RESULTS" 2>/dev/null; then
164533 python3 .github/scripts/check_runner_images.py --format notes > "$RUNNER_IMAGE_NOTES" 2>/dev/null || true
164668 while IFS='|' read -r label current recommended; do
16471 [ -z "$label" ] && continue
16481 echo " - runner ${label}: ${current} -> ${recommended}"
1649 done < "$RUNNER_IMAGE_RESULTS"
1650132 while IFS='|' read -r label current recommended; do
165133 [ -z "$label" ] && continue
165233 echo " ${label} pins ${current}; ${recommended} exists but is still in preview."
1653 done < "$RUNNER_IMAGE_NOTES"
165433 if [ ! -s "$RUNNER_IMAGE_RESULTS" ]; then
165532 echo " every runner label in use is the newest generally-available image."
1656 fi
1657else
16582 RUNNER_IMAGE_SKIP_REASON="Could not read the actions/runner-images catalog (network or upstream README change)."
16592 echo " $RUNNER_IMAGE_SKIP_REASON"
16602 : > "$RUNNER_IMAGE_RESULTS"
1661fi
1662
1663105RUNNER_IMAGE_COUNT="$(wc -l < "$RUNNER_IMAGE_RESULTS" 2>/dev/null | tr -d ' ')"
166435[ -z "$RUNNER_IMAGE_COUNT" ] && RUNNER_IMAGE_COUNT=0
1665
1666# ---------------------------------------------------------------------------
1667# CI tooling pins (public endpoints — no AWS creds)
1668#
1669# The workflows install their own pinned tooling — Trivy (cve-scan.yml /
1670# security.yml), actionlint (lint.yml), Helm + kubectl (deps-scan.yml), and
1671# kubeconform, Calico, and Metrics Server (integration-tests.yml) — from plain
1672# ``*_VERSION`` env strings. The integration workflow also pins kind + its node
1673# image on the ``helm/kind-action`` step. None are ``uses:`` refs or Dockerfile
1674# ``FROM`` lines, so Dependabot never sees them. Compare each against upstream.
1675# ---------------------------------------------------------------------------
167635echo ""
167735echo "=== Checking CI tooling pins ==="
167835
167970CI_TOOLING_RESULTS="$(mktemp)"
1680# The kind-action lockstep check below files its finding under Version
1681# Consistency, so that section's results file has to exist already; it is
1682# created here, once, and the consistency section appends to it.
168370CONSISTENCY_RESULTS="$(mktemp)"
1684
1685# check_github_tool <display-name> <current-pin> <owner/repo> <ref-url>
1686# Records drift when the pinned semver is behind the latest GitHub Release.
1687check_github_tool() {
1688280 local name="$1" current="$2" repo="$3" url="$4" latest=""
1689280 if [ -z "$current" ]; then
16908 mark_scan_incomplete "Could not parse the committed version pin for ${name}."
16918 return 0
1692 fi
1693544 latest="$(get_latest_github_release_tag "$repo")"
1694272 if [ -z "$latest" ]; then
169548 mark_scan_incomplete "GitHub release lookup failed for ${name} (${repo})$(github_api_failure_hint)."
169624 return 0
1697 fi
1698248 if [ "$current" != "$latest" ] \
1699144 && [ "$(compare_semver "$current" "$latest")" = "newer" ]; then
170069 echo " - ${name}: ${current} -> ${latest}"
170169 echo "${name}|${current}|${latest}|${url}" >> "$CI_TOOLING_RESULTS"
1702 fi
1703}
1704
1705# Trivy (aquasecurity/trivy) — the version-input default of the
1706# install-trivy composite action, the single pin every caller inherits.
1707105TRIVY_PIN="$(extract_install_trivy_pin .github/actions/install-trivy/action.yml | head -1)"
170835check_github_tool "Trivy (install-trivy action default)" "$TRIVY_PIN" "aquasecurity/trivy" \
1709 "https://github.com/aquasecurity/trivy/releases"
1710
1711# actionlint (rhysd/actionlint) — lint.yml downloads this release archive.
1712105ACTIONLINT_PIN="$(extract_workflow_env_pin ACTIONLINT_VERSION | head -1)"
171335check_github_tool "actionlint (ACTIONLINT_VERSION)" "$ACTIONLINT_PIN" "rhysd/actionlint" \
1714 "https://github.com/rhysd/actionlint/releases"
1715
1716# Helm (helm/helm) — the authenticated RUN-line pin in
1717# lambda/helm-installer/Dockerfile, the single source every CI job derives
1718# its HELM_VERSION/HELM_SHA256 from at runtime.
1719140HELM_PIN="$(extract_helm_installer_pins lambda/helm-installer/Dockerfile | awk -F'|' '$1=="HELM_VERSION"{print $2}' | head -1)"
172035check_github_tool "Helm (helm-installer Dockerfile)" "$HELM_PIN" "helm/helm" \
1721 "https://github.com/helm/helm/releases"
1722
1723# kubeconform (yannh/kubeconform) — KUBECONFORM_VERSION the
1724# integration:k8s:manifest-schema job installs to schema-validate the K8s
1725# manifests. Plain env pin Dependabot doesn't watch; a stale kubeconform
1726# silently validates against outdated Kubernetes schemas.
1727105KUBECONFORM_PIN="$(extract_workflow_env_pin KUBECONFORM_VERSION | head -1)"
172835check_github_tool "kubeconform (KUBECONFORM_VERSION)" "$KUBECONFORM_PIN" "yannh/kubeconform" \
1729 "https://github.com/yannh/kubeconform/releases"
1730
1731# Metrics Server (kubernetes-sigs/metrics-server) — kind installs this pin so
1732# the inference proxy HPA must reach ScalingActive, mirroring the EKS managed
1733# add-on contract used in production.
1734105METRICS_SERVER_PIN="$(extract_workflow_env_pin METRICS_SERVER_VERSION | head -1)"
173535check_github_tool \
1736 "Metrics Server (METRICS_SERVER_VERSION)" \
1737 "$METRICS_SERVER_PIN" \
1738 "kubernetes-sigs/metrics-server" \
1739 "https://github.com/kubernetes-sigs/metrics-server/releases"
174035
1741# Calico (projectcalico/calico) — kind installs the authenticated release
1742# manifest so NetworkPolicy behavior is exercised by the E2E job.
1743105CALICO_PIN="$(extract_workflow_env_pin CALICO_VERSION | head -1)"
174435check_github_tool "Calico (CALICO_VERSION)" "$CALICO_PIN" "projectcalico/calico" \
1745 "https://github.com/projectcalico/calico/releases"
1746
1747# Crossplane function-go-templating (crossplane-contrib/function-go-templating)
1748# — the composition Function package post-helm-crossplane.yaml installs when
1749# helm.crossplane is on. The Argo CD, Crossplane and Crossview charts (and
1750# Crossview's image) are charts.yaml pins the Helm sections above cover; this
1751# xpkg reference is the one add-on pin outside charts.yaml, and it is not an
1752# ``image:`` line, so no image sweep would notice it ageing.
1753105FUNCTION_GO_TEMPLATING_PIN="$(extract_crossplane_function_pin \
1754 lambda/kubectl-applier-simple/manifests/post-helm-crossplane.yaml function-go-templating \
1755 | head -1)"
175635check_github_tool "Crossplane function-go-templating (post-helm-crossplane.yaml)" \
1757 "$FUNCTION_GO_TEMPLATING_PIN" "crossplane-contrib/function-go-templating" \
1758 "https://github.com/crossplane-contrib/function-go-templating/releases"
1759
1760# kind (kubernetes-sigs/kind) — the kind binary on the kind-action step.
1761# Both kind-action steps (cluster-e2e and examples-smoke) must pin the same
1762# kind binary + node image; extract_kind_pins de-duplicates identical pins,
1763# so >1 value per key means the two jobs drifted apart.
176470for kind_key in kind kind-node; do
1765210 kind_vals="$(extract_kind_pins .github/workflows/integration-tests.yml \
1766 | awk -F'|' -v k="$kind_key" '$1==k{print $2}')"
1767280 kind_distinct="$(printf '%s\n' "$kind_vals" | sed '/^$/d' | grep -c .)"
176870 if [ "$kind_distinct" -gt 1 ]; then
17698 kind_list="$(printf '%s\n' "$kind_vals" | sed '/^$/d' | paste -sd',' -)"
17702 echo " - ${kind_key} pins disagree across kind-action steps: ${kind_list}"
17712 echo "${kind_key} (across kind-action steps)|${kind_list}" >> "$CONSISTENCY_RESULTS"
1772 fi
1773done
1774140KIND_PIN="$(extract_kind_pins .github/workflows/integration-tests.yml | awk -F'|' '$1=="kind"{print $2}' | head -1)"
177535check_github_tool "kind" "$KIND_PIN" "kubernetes-sigs/kind" \
1776 "https://github.com/kubernetes-sigs/kind/releases"
1777
1778# kubectl — the authenticated RUN-line pin in lambda/helm-installer/
1779# Dockerfile (the single source the CI jobs derive from); compared against
1780# the stable release for its own minor line (dl.k8s.io), the same source
1781# the Dockerfile.dev kubectl pin uses.
1782140KUBECTL_WF_PIN="$(extract_helm_installer_pins lambda/helm-installer/Dockerfile | awk -F'|' '$1=="KUBECTL_VERSION"{print $2}' | head -1)"
178335if [ -n "$KUBECTL_WF_PIN" ]; then
1784102 kubectl_minor="$(echo "${KUBECTL_WF_PIN#v}" | cut -d. -f1-2)"
1785102 if ! kubectl_latest="$(curl -fsSL --max-time 15 "${SCAN_CURL_RETRY[@]}" \
1786 "https://dl.k8s.io/release/stable-${kubectl_minor}.txt" 2>/dev/null | tr -d '[:space:]')" \
178733 || [ -z "$kubectl_latest" ]; then
17881 mark_scan_incomplete "kubectl stable-version lookup failed for minor ${kubectl_minor}."
178933 elif [ "$KUBECTL_WF_PIN" != "$kubectl_latest" ] \
179018 && [ "$(compare_semver "$KUBECTL_WF_PIN" "$kubectl_latest")" = "newer" ]; then
17919 echo " - kubectl (helm-installer Dockerfile): ${KUBECTL_WF_PIN} -> ${kubectl_latest}"
17929 echo "kubectl (helm-installer Dockerfile)|${KUBECTL_WF_PIN}|${kubectl_latest}|https://kubernetes.io/releases/" >> "$CI_TOOLING_RESULTS"
1793 fi
1794else
17951 mark_scan_incomplete "Could not parse the kubectl pin from lambda/helm-installer/Dockerfile."
1796fi
1797
1798# kind node image (kindest/node) — report a newer PATCH within the pinned K8s
1799# minor only. Jumping minors is governed by the kind release, not free drift,
1800# so scoping to the same minor avoids false "upgrade" noise. The pin is tag
1801# AND digest (the digest half is checked in the Docker section above), so the
1802# tag is taken from the split reference, never from the text after the last
1803# colon, which for a digest-pinned image is the digest.
1804140KIND_NODE_PIN="$(extract_kind_pins .github/workflows/integration-tests.yml | awk -F'|' '$1=="kind-node"{print $2}' | head -1)"
180535if [ -n "$KIND_NODE_PIN" ]; then
180668 if kind_node_parts="$(split_pinned_image_ref "$KIND_NODE_PIN")"; then
180799 node_tag="$(echo "$kind_node_parts" | cut -d'|' -f2)"
1808 else
1809 # Tag only: still check the tag for a newer patch, and say that the pin
1810 # names a different image after every kind release until a digest binds it.
18111 node_tag="${KIND_NODE_PIN##*:}"
18121 echo " - kind node image (integration-tests.yml): ${KIND_NODE_PIN} is not pinned by digest"
18131 echo "kind node image (integration-tests.yml)|${KIND_NODE_PIN} is not pinned by digest; use the kindest/node:<tag>@sha256:<digest> reference from the kind release notes" >> "$CONSISTENCY_RESULTS"
1814 fi
1815102 node_minor="$(echo "${node_tag#v}" | cut -d. -f1-2)"
1816233 if ! node_latest="$(skopeo list-tags "${SCAN_SKOPEO_RETRY[@]}" "docker://docker.io/kindest/node" 2>/dev/null \
1817 | jq -r '.Tags[]' 2>/dev/null \
1818 | grep -E "^v?${node_minor}\.[0-9]+$" \
1819 | sort -V | tail -1)" || [ -z "$node_latest" ]; then
18205 mark_scan_incomplete "Container registry lookup failed for kindest/node minor ${node_minor}."
182129 elif [ "$node_tag" != "$node_latest" ] \
182212 && [ "$(compare_semver "$node_tag" "$node_latest")" = "newer" ]; then
18236 echo " - kind node image (kindest/node): ${node_tag} -> ${node_latest}"
18246 echo "kind node image (kindest/node)|${node_tag}|${node_latest}|https://hub.docker.com/r/kindest/node/tags" >> "$CI_TOOLING_RESULTS"
1825 fi
1826else
18271 mark_scan_incomplete "Could not parse the kind node-image pin from integration-tests.yml."
1828fi
1829
1830105CI_TOOLING_COUNT="$(wc -l < "$CI_TOOLING_RESULTS" 2>/dev/null | tr -d ' ')"
183135[ -z "$CI_TOOLING_COUNT" ] && CI_TOOLING_COUNT=0
1832
1833# ---------------------------------------------------------------------------
1834# Version consistency (no network)
1835#
1836# Some versions are pinned in more than one place and must move together.
1837# The other sections answer "is this pin behind upstream?"; this one answers
1838# "do the copies of this pin agree with each other?" — a class of drift that
1839# otherwise only surfaces when a formatter/linter behaves differently in CI
1840# than it does locally.
1841#
1842# - ruff: pyproject (dev install) vs the pre-commit hook vs the prebuilt-
1843# binary ruff-action step in lint.yml.
1844# - python-version across the workflows vs the project's canonical Python
1845# (the LAMBDA_PYTHON_RUNTIME the Lambdas ship on).
1846# - Node major across LAMBDA_NODEJS_RUNTIME, .nvmrc, every package engine,
1847# and Dockerfile.dev; npm across every packageManager and Dockerfile.dev.
1848# - AWS CDK CLI across the locked root npm graph and Dockerfile.dev.
1849# - every repository-owned package.json has a lockfile, exact direct pins,
1850# and a matching npm entry in Dependabot.
1851# - the same tool env pin (HELM_VERSION/KUBECTL_VERSION/CALICO_*)
1852# resolving to different values in different workflow files.
1853# - every [build-system] requires entry in pyproject.toml is an exact
1854# ``==`` pin (the drift itself reports through the Python surface).
1855# - every per-Lambda requirements.txt pin agrees with the version the
1856# repository resolves centrally (pyproject, then the lock for
1857# transitives) — the copies that ship to production.
1858# - no digest-pinned image carries two different digests under one tag,
1859# which means an upstream re-push was only half applied.
1860# ---------------------------------------------------------------------------
186135echo ""
186235echo "=== Checking version consistency ==="
186335
186470RUFF_PINS="$(extract_ruff_pins pyproject.toml .pre-commit-config.yaml .github/workflows/lint.yml)"
186535if [ -n "$RUFF_PINS" ]; then
1866170 ruff_distinct="$(echo "$RUFF_PINS" | cut -d'|' -f2 | sort -u | grep -c .)"
186734 if [ "$ruff_distinct" -gt 1 ]; then
18684 detail="$(echo "$RUFF_PINS" | awk -F'|' '{printf "%s=%s ", $1, $2}' | sed 's/ *$//')"
18691 echo " - ruff pins disagree: $detail"
18701 echo "ruff (pyproject / pre-commit / lint action)|${detail}" >> "$CONSISTENCY_RESULTS"
1871 fi
1872fi
1873
1874105CANON_PY="$(echo "${LAMBDA_RT_CURRENT:-}" | sed -E 's/^PYTHON_([0-9]+)_([0-9]+)$/\1.\2/')"
187538[ -z "$CANON_PY" ] && CANON_PY="$(extract_constant_value LAMBDA_PYTHON_RUNTIME | sed -E 's/^PYTHON_([0-9]+)_([0-9]+)$/\1.\2/')"
1876105PY_PINS_UNIQUE="$(extract_python_version_pins "$WORKFLOWS_DIR" .python-version | sort -u)"
187735if [ -n "$PY_PINS_UNIQUE" ]; then
1878102 py_distinct="$(echo "$PY_PINS_UNIQUE" | grep -c .)"
1879102 py_list="$(echo "$PY_PINS_UNIQUE" | paste -sd',' -)"
1880100 if [ "$py_distinct" -gt 1 ] || { [ -n "$CANON_PY" ] && [ "$py_list" != "$CANON_PY" ]; }; then
18811 echo " - python-version pins: ${py_list} (project runtime: ${CANON_PY:-unknown})"
18821 echo "python-version (CI vs runtime)|CI: ${py_list}; runtime: ${CANON_PY:-unknown}" >> "$CONSISTENCY_RESULTS"
1883 fi
1884fi
1885
1886212NPM_PACKAGE_SOURCES="$(
1887 list_npm_package_dirs . | while IFS= read -r package_dir; do
1888 [ -n "$package_dir" ] || continue
1889 if [ "$package_dir" = "." ]; then
1890 echo "package.json"
1891 else
1892 echo "${package_dir}/package.json"
1893 fi
1894 done
1895)"
189636
1897141NODE_PINS="$(extract_node_major_pins . gco/stacks/constants.py .nvmrc Dockerfile.dev | sort -u)"
189870EXPECTED_NODE_SOURCES="$(
1899 {
1900 printf '%s\n' gco/stacks/constants.py .nvmrc Dockerfile.dev
1901 printf '%s\n' "$NPM_PACKAGE_SOURCES"
1902 } | sed '/^$/d' | sort -u
1903)"
1904280NODE_PIN_SOURCES="$(printf '%s\n' "$NODE_PINS" | cut -d'|' -f1 | sed '/^$/d' | sort -u)"
1905140NODE_MISSING="$(comm -23 <(printf '%s\n' "$EXPECTED_NODE_SOURCES") <(printf '%s\n' "$NODE_PIN_SOURCES"))"
1906210node_distinct="$(printf '%s\n' "$NODE_PINS" | cut -d'|' -f2 | sed '/^$/d' | sort -u | grep -c .)"
190767if [ -n "$NODE_MISSING" ] || [ "$node_distinct" -gt 1 ]; then
190812 node_detail="$(printf '%s\n' "$NODE_PINS" | awk -F'|' '{printf "%s=%s ", $1, $2}' | sed 's/ *$//')"
19093 if [ -n "$NODE_MISSING" ]; then
19109 node_missing_list="$(printf '%s\n' "$NODE_MISSING" | paste -sd',' -)"
19113 node_detail="${node_detail}; missing=${node_missing_list}"
1912 fi
19133 echo " - Node.js major pins disagree or are missing: ${node_detail}"
19143 echo "Node.js major (runtime / packages / dev container)|${node_detail}" >> "$CONSISTENCY_RESULTS"
1915fi
1916
1917105NPM_PINS="$(extract_npm_version_pins . Dockerfile.dev | sort -u)"
191870EXPECTED_NPM_SOURCES="$(
1919 {
1920 printf '%s\n' Dockerfile.dev
1921 printf '%s\n' "$NPM_PACKAGE_SOURCES"
1922 } | sed '/^$/d' | sort -u
1923)"
1924280NPM_PIN_SOURCES="$(printf '%s\n' "$NPM_PINS" | cut -d'|' -f1 | sed '/^$/d' | sort -u)"
1925140NPM_MISSING="$(comm -23 <(printf '%s\n' "$EXPECTED_NPM_SOURCES") <(printf '%s\n' "$NPM_PIN_SOURCES"))"
1926210npm_distinct="$(printf '%s\n' "$NPM_PINS" | cut -d'|' -f2 | sed '/^$/d' | sort -u | grep -c .)"
192768if [ -n "$NPM_MISSING" ] || [ "$npm_distinct" -gt 1 ]; then
192812 npm_detail="$(printf '%s\n' "$NPM_PINS" | awk -F'|' '{printf "%s=%s ", $1, $2}' | sed 's/ *$//')"
19293 if [ -n "$NPM_MISSING" ]; then
19306 npm_missing_list="$(printf '%s\n' "$NPM_MISSING" | paste -sd',' -)"
19312 npm_detail="${npm_detail}; missing=${npm_missing_list}"
1932 fi
19333 echo " - npm pins disagree or are missing: ${npm_detail}"
19343 echo "npm (packageManager / dev container)|${npm_detail}" >> "$CONSISTENCY_RESULTS"
1935fi
1936
1937105CDK_CLI_PINS="$(extract_cdk_cli_pins . Dockerfile.dev | sort -u)"
1938280CDK_CLI_MISSING="$(comm -23 \
1939 <(printf '%s\n' Dockerfile.dev package.json | sort -u) \
1940 <(printf '%s\n' "$CDK_CLI_PINS" | cut -d'|' -f1 | sed '/^$/d' | sort -u))"
1941210cdk_cli_distinct="$(printf '%s\n' "$CDK_CLI_PINS" | cut -d'|' -f2 | sed '/^$/d' | sort -u | grep -c .)"
194267if [ -n "$CDK_CLI_MISSING" ] || [ "$cdk_cli_distinct" -gt 1 ]; then
194312 cdk_detail="$(printf '%s\n' "$CDK_CLI_PINS" | awk -F'|' '{printf "%s=%s ", $1, $2}' | sed 's/ *$//')"
19443 if [ -n "$CDK_CLI_MISSING" ]; then
19459 cdk_missing_list="$(printf '%s\n' "$CDK_CLI_MISSING" | paste -sd',' -)"
19463 cdk_detail="${cdk_detail}; missing=${cdk_missing_list}"
1947 fi
19483 echo " - AWS CDK CLI pins disagree or are missing: ${cdk_detail}"
19493 echo "AWS CDK CLI (package / dev container)|${cdk_detail}" >> "$CONSISTENCY_RESULTS"
1950fi
1951
195270NPM_MANAGEMENT_PROBLEMS="$(check_npm_package_management . .github/dependabot.yml)"
195335if [ -n "$NPM_MANAGEMENT_PROBLEMS" ]; then
19544 while IFS='|' read -r manifest problem; do
19551 [ -n "$manifest" ] || continue
19561 echo " - npm dependency management: ${manifest}: ${problem}"
19571 echo "npm dependency management|${manifest}: ${problem}" >> "$CONSISTENCY_RESULTS"
1958 done <<< "$NPM_MANAGEMENT_PROBLEMS"
1959fi
1960
1961# Every tool pinned in more than one workflow (or more than one job) must
1962# agree. CALICO_* and METRICS_SERVER_* are here because the kind jobs install
1963# them per job — two jobs on different Calico builds would enforce
1964# NetworkPolicy with two different engines, and a version/checksum pair that
1965# disagrees fails the download as what looks like a flake. The PR-time half of
1966# this contract lives in
1967# tests/test_supply_chain_integrity.py::test_repeated_workflow_pins_agree_across_jobs.
1968# (Trivy is absent from this list on purpose: its pin is the install-trivy
1969# composite action's input default, a single declaration that cannot
1970# disagree with itself.)
1971# (Helm and kubectl are absent from this list on purpose: their pins live
1972# only in lambda/helm-installer/Dockerfile — workflows derive their env
1973# copies from it at runtime, so a checked-in workflow declaration that
1974# could disagree no longer exists. The guard below still catches a stray
1975# reintroduced copy.)
1976140for consistency_var in \
1977 CALICO_VERSION CALICO_SHA256 METRICS_SERVER_VERSION METRICS_SERVER_SHA256; do
1978280 cvals="$(extract_workflow_env_pin "$consistency_var")"
1979420 cnum="$(echo "$cvals" | grep -c .)"
1980140 if [ "$cnum" -gt 1 ]; then
19813 clist="$(echo "$cvals" | paste -sd',' -)"
19821 echo " - ${consistency_var} disagrees across workflows: ${clist}"
19831 echo "${consistency_var} (across workflows)|${clist}" >> "$CONSISTENCY_RESULTS"
1984 fi
1985done
1986
1987# helm / kubectl pins hardcoded in lambda/helm-installer/Dockerfile RUN-line
1988# URLs must agree with the workflow env pins (and, for kubectl, with the
1989# Dockerfile.dev ARG). These URL literals were previously invisible to every
1990# check — the integration-tests workflow even carries a comment noting the
1991# Lambda copy "isn't caught by the consistency check". Now it is.
199270INSTALLER_PINS="$(extract_helm_installer_pins lambda/helm-installer/Dockerfile)"
199335if [ -n "$INSTALLER_PINS" ]; then
199468 for tool_var in HELM_VERSION KUBECTL_VERSION; do
1995204 installer_val="$(printf '%s\n' "$INSTALLER_PINS" | awk -F'|' -v v="$tool_var" '$1==v{print $2}')"
199668 [ -n "$installer_val" ] || continue
199768 all_vals="$installer_val"
1998 # Workflows derive their copies from the installer Dockerfile at
1999 # runtime, so a checked-in workflow declaration is itself a finding:
2000 # it would shadow the derive step's GITHUB_ENV export and drift.
2001136 wf_vals="$(extract_workflow_env_pin "$tool_var")"
200268 if [ -n "$wf_vals" ]; then
20031 echo " - ${tool_var} is declared literally in a workflow again (should derive from the installer Dockerfile): ${wf_vals}"
20041 echo "${tool_var} (literal workflow copy reintroduced)|${wf_vals}" >> "$CONSISTENCY_RESULTS"
20052 all_vals="$(printf '%s\n%s' "$all_vals" "$wf_vals")"
2006 fi
200768 if [ "$tool_var" = "KUBECTL_VERSION" ]; then
2008102 dev_val="$(extract_dockerfile_pins Dockerfile.dev | awk -F'|' '$1=="KUBECTL_VERSION"{print $2}')"
2009100 [ -n "$dev_val" ] && all_vals="$(printf '%s\n%s' "$all_vals" "$dev_val")"
2010 fi
2011340 tool_distinct="$(printf '%s\n' "$all_vals" | sed '/^$/d' | sort -u | grep -c .)"
201268 if [ "$tool_distinct" -gt 1 ]; then
201310 tool_list="$(printf '%s\n' "$all_vals" | sed '/^$/d' | sort -u | paste -sd',' -)"
20142 echo " - ${tool_var} disagrees between helm-installer Dockerfile, workflows, and Dockerfile.dev: ${tool_list}"
20152 echo "${tool_var} (helm-installer Dockerfile / workflows / Dockerfile.dev)|${tool_list}" >> "$CONSISTENCY_RESULTS"
2016 fi
2017 done
2018fi
2019
2020# The kubernetes Python client is generated per Kubernetes minor, and the
2021# PR-time guard lets it trail cdk.json's minor by one so the cluster can move
2022# before upstream ships the matching client. Nothing else would remind anyone
2023# to close that gap, so it is a consistency finding until the client catches
2024# up (or, should the guard ever be bypassed, when it leads or lags further).
202570K8S_CLIENT_PIN="$(extract_kubernetes_client_pin pyproject.toml)"
2026105K8S_CLIENT_PROBLEM="$(check_kubernetes_client_skew "$K8S_CLIENT_PIN" "$(extract_k8s_version cdk.json)")"
202735if [ -n "$K8S_CLIENT_PROBLEM" ]; then
20283 echo " - kubernetes client vs cluster: ${K8S_CLIENT_PROBLEM}"
20293 echo "kubernetes Python client (pyproject.toml / cdk.json)|${K8S_CLIENT_PROBLEM}" >> "$CONSISTENCY_RESULTS"
2030fi
2031
2032# Build-backend pins must use the same exact ``==`` shape as every other
2033# Python dependency in pyproject.toml, or the version resolved inside
2034# pip's build isolation floats with upstream releases. An empty result is
2035# itself a finding — [build-system] always exists here, so nothing coming
2036# back means the table was removed or the TOML no longer parses, and a
2037# parse break must not silently drop the check.
203836BUILD_SYSTEM_PINS="${BUILD_SYSTEM_PINS:-$(extract_build_system_pins pyproject.toml)}"
203935if [ -z "$BUILD_SYSTEM_PINS" ]; then
20401 echo " - pyproject.toml [build-system] requires is missing or unparseable"
20411 echo "build-system requires (pyproject.toml)|missing or unparseable" >> "$CONSISTENCY_RESULTS"
2042else
2043136 while IFS='|' read -r bs_name bs_version bs_raw; do
204434 [ -n "$bs_raw" ] || continue
204534 if [ -z "$bs_version" ]; then
20461 echo " - build-system requires entry is not an exact ==X.Y.Z pin: ${bs_raw}"
20471 echo "build-system requires (pyproject.toml)|'${bs_raw}' must be an exact ==X.Y.Z pin" >> "$CONSISTENCY_RESULTS"
2048 fi
2049 done <<< "$BUILD_SYSTEM_PINS"
2050fi
2051
2052# Each Lambda is packaged independently and carries its own requirements.txt,
2053# so a library like boto3 is pinned centrally *and* in up to six Lambda copies.
2054# Nothing watched the copies: a bump applied to pyproject.toml and the lock left
2055# them behind silently, and the handlers shipped a boto3 that nothing in CI ever
2056# exercised. This is the one drift surface that reaches production directly, so
2057# the report names the file and the version it must move to. The PR-time half of
2058# this contract lives in
2059# tests/test_integration.py::TestDependencyVersionConsistency::test_lambda_requirements_match_pyproject.
206070LAMBDA_PIN_PROBLEMS="$(check_lambda_requirements_pins . pyproject.toml requirements-lock.txt)"
206135if [ -n "$LAMBDA_PIN_PROBLEMS" ]; then
20628 while IFS='|' read -r lambda_req lambda_problem; do
20632 [ -n "$lambda_req" ] || continue
20642 echo " - Lambda runtime pin: ${lambda_req}: ${lambda_problem}"
20652 echo "Lambda runtime pins|${lambda_req}: ${lambda_problem}" >> "$CONSISTENCY_RESULTS"
2066 done <<< "$LAMBDA_PIN_PROBLEMS"
2067fi
2068
2069# An immutable digest is only immutable where it is written down. When an
2070# upstream tag is re-pushed and the pin is refreshed in one file but restated as
2071# a literal in another, nothing reconciled the two until whichever test held the
2072# stale copy happened to run. Two digests under one tag is unambiguous drift.
207370IMAGE_DIGEST_PROBLEMS="$(check_image_digest_consistency .)"
207435if [ -n "$IMAGE_DIGEST_PROBLEMS" ]; then
20754 while IFS='|' read -r image_repo image_problem; do
20761 [ -n "$image_repo" ] || continue
20771 echo " - image digest: ${image_repo}: ${image_problem}"
20781 echo "Image digest copies|${image_repo}: ${image_problem}" >> "$CONSISTENCY_RESULTS"
2079 done <<< "$IMAGE_DIGEST_PROBLEMS"
2080fi
2081
2082105CONSISTENCY_COUNT="$(wc -l < "$CONSISTENCY_RESULTS" 2>/dev/null | tr -d ' ')"
208335[ -z "$CONSISTENCY_COUNT" ] && CONSISTENCY_COUNT=0
2084
2085# ---------------------------------------------------------------------------
2086# Base-image security epochs (no network)
2087#
2088# The service images (Debian) and the helm-installer Lambda (AL2023) pull OS
2089# security patches at build time behind a hand-bumped ``*_SECURITY_EPOCH``
2090# ARG that busts the CI layer cache. Nothing else reminds anyone to move the
2091# date, so a stale epoch silently reuses an old upgrade layer. Trivy's
2092# container scan is the backstop; this flags an epoch older than the window
2093# as the proactive nudge. Only the real Dockerfiles are scanned — the
2094# generated ``*-build`` staging copies are skipped.
2095# ---------------------------------------------------------------------------
209635echo ""
209735echo "=== Checking base-image security epochs ==="
209835
209970EPOCH_RESULTS="$(mktemp)"
210035EPOCH_FILES=(dockerfiles/Dockerfile.* Dockerfile.dev lambda/helm-installer/Dockerfile)
2101115for df in "${EPOCH_FILES[@]}"; do
2102118 [ -f "$df" ] || continue
2103558 extract_security_epochs "$df" | while IFS='|' read -r epoch_arg epoch_date; do
2104111 [ -z "$epoch_date" ] && continue
2105222 epoch_age="$(days_since "$epoch_date")"
2106111 [ -z "$epoch_age" ] && continue
2107111 if [ "$epoch_age" -gt "$SECURITY_EPOCH_STALE_DAYS" ]; then
21083 echo " - ${df} (${epoch_arg}): ${epoch_date} (${epoch_age} days old)"
21093 echo "${df}|${epoch_arg}|${epoch_date}|${epoch_age}" >> "$EPOCH_RESULTS"
2110 fi
2111 done
2112done
2113
2114105EPOCH_COUNT="$(wc -l < "$EPOCH_RESULTS" 2>/dev/null | tr -d ' ')"
211535[ -z "$EPOCH_COUNT" ] && EPOCH_COUNT=0
2116
2117# ---------------------------------------------------------------------------
2118# Suppression expiries (no network)
2119#
2120# ``.trivyignore`` / ``.pip-audit-ignore`` / ``.npm-audit-ignore`` entries
2121# carry an ``exp:YYYY-MM-DD`` marker. The CI validators hard-fail a PR on the
2122# day an entry expires; this surfaces entries expiring *soon* so they get
2123# re-evaluated (fixed upstream? extend with a new justification?) before they
2124# break a build.
2125# ---------------------------------------------------------------------------
212635echo ""
212735echo "=== Checking suppression expiries ==="
212835
212970SUPPRESSION_RESULTS="$(mktemp)"
2130105for supfile in .github/config/.trivyignore .github/config/.pip-audit-ignore .github/config/.npm-audit-ignore; do
2131108 [ -f "$supfile" ] || continue
2132204 supbase="$(basename "$supfile")"
2133518 parse_suppression_expiries "$supfile" | while IFS='|' read -r sup_id sup_date; do
2134106 [ -z "$sup_date" ] && continue
2135212 sup_left="$(days_until "$sup_date")"
2136106 [ -z "$sup_left" ] && continue
2137106 if [ "$sup_left" -le "$SUPPRESSION_EXPIRY_WARN_DAYS" ]; then
213821 echo " - ${supbase}: ${sup_id} expires ${sup_date} (${sup_left} days)"
213921 echo "${supbase}|${sup_id}|${sup_date}|${sup_left}" >> "$SUPPRESSION_RESULTS"
2140 fi
2141 done
2142done
2143
2144105SUPPRESSION_COUNT="$(wc -l < "$SUPPRESSION_RESULTS" 2>/dev/null | tr -d ' ')"
214535[ -z "$SUPPRESSION_COUNT" ] && SUPPRESSION_COUNT=0
2146
2147# ---------------------------------------------------------------------------
2148# Lockfile freshness (no network)
2149#
2150# ``requirements-lock.txt`` is compiled from ``pyproject.toml`` with
2151# ``pip-compile --all-extras``. Every direct dependency must use an exact,
2152# concrete pin and is matched to the lock by normalized name, canonical marker
2153# identity, and version. Missing or mismatched records mean the lock is stale;
2154# unrelated transitive pins are ignored.
2155# ---------------------------------------------------------------------------
215635echo ""
215735echo "=== Checking lockfile freshness ==="
215835
215970LOCKFILE_RESULTS="$(mktemp)"
216070LOCKFILE_RAW="$(mktemp)"
216135if check_lockfile_freshness pyproject.toml requirements-lock.txt > "$LOCKFILE_RAW"; then
216270 while IFS='|' read -r lock_name expected_version locked_version; do
21632 [ -z "$lock_name" ] && continue
21642 if [ "$locked_version" = "<missing>" ]; then
21651 echo " - direct dep missing from requirements-lock.txt: ${lock_name}==${expected_version}"
2166 else
21671 echo " - direct dep version mismatch: ${lock_name}==${expected_version} (lock has ${locked_version})"
2168 fi
21692 echo "${lock_name}|${expected_version}|${locked_version}" >> "$LOCKFILE_RESULTS"
2170 done < "$LOCKFILE_RAW"
2171else
21722 mark_scan_incomplete "Lockfile freshness validation failed; inspect its error above."
2173fi
217435rm -f "$LOCKFILE_RAW"
2175
2176105LOCKFILE_COUNT="$(wc -l < "$LOCKFILE_RESULTS" 2>/dev/null | tr -d ' ')"
217735[ -z "$LOCKFILE_COUNT" ] && LOCKFILE_COUNT=0
2178
2179# ---------------------------------------------------------------------------
2180# Accelerator catalog and Karpenter NodePools
2181#
2182# The deterministic check always runs and validates the reviewed catalog
2183# against every NodePool plus the exact cdk.json capacity-history watch list.
2184# With AWS credentials, the monthly job also compares the catalog against the
2185# union of NVIDIA GPU / AWS Neuron types returned across enabled commercial
2186# Regions. Ordinary policy/catalog drift joins the rolling dependency issue;
2187# execution or parser failures become one operational finding, never a
2188# false-clean result.
2189# ---------------------------------------------------------------------------
219035echo ""
219135echo "=== Checking accelerator catalog and Karpenter NodePools ==="
219235
219370ACCELERATOR_OFFLINE_REPORT="$(mktemp)"
219470ACCELERATOR_ONLINE_REPORT="$(mktemp)"
219570ACCELERATOR_ONLINE_SUMMARY="$(mktemp)"
219670ACCELERATOR_OFFLINE_ERROR="$(mktemp)"
219770ACCELERATOR_ONLINE_ERROR="$(mktemp)"
219835ACCELERATOR_OFFLINE_COUNT=0
219935ACCELERATOR_ONLINE_COUNT=0
220035ACCELERATOR_SKIP_REASON=""
220135ACCELERATOR_SUMMARY_SKIP_REASON=""
2202
2203write_accelerator_operational_report() {
22045 local report_path="$1" title="$2" detail="$3" error_path="$4"
2205 {
22065 echo "## ${title}"
22075 echo ""
22085 echo "**Status: OPERATIONAL ERROR.**"
22095 echo ""
22105 echo "### Accelerator maintenance check could not complete"
22115 echo ""
22125 echo "- **Why:** ${detail}"
22135 echo "- **Recommended change:** Re-run the command locally, repair the tool or credentials, and do not treat this scan as current until it succeeds."
22145 if [ -s "$error_path" ]; then
22152 echo "- **Tool output:**"
22162 sed 's/^/ /' "$error_path"
2217 fi
2218 } > "$report_path"
2219}
2220
2221record_accelerator_operational_error() {
22225 local report_path="$1" title="$2" detail="$3" error_path="$4"
22235 write_accelerator_operational_report "$report_path" "$title" "$detail" "$error_path"
22245 mark_scan_incomplete "${title}: ${detail}"
2225}
2226
222735python3 scripts/accelerator_catalog.py validate \
2228 --format markdown \
2229 --output "$ACCELERATOR_OFFLINE_REPORT" \
2230 2>"$ACCELERATOR_OFFLINE_ERROR"
223135ACCELERATOR_OFFLINE_STATUS=$?
223235if [ "$ACCELERATOR_OFFLINE_STATUS" -eq 0 ]; then
223332 echo " Offline NodePool/watch-list policy is current."
22343elif [ "$ACCELERATOR_OFFLINE_STATUS" -eq 1 ]; then
22355 ACCELERATOR_OFFLINE_COUNT="$(grep -c '^### ' "$ACCELERATOR_OFFLINE_REPORT" || true)"
22362 if ! [[ "$ACCELERATOR_OFFLINE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
22371 ACCELERATOR_OFFLINE_COUNT=1
22381 record_accelerator_operational_error \
2239 "$ACCELERATOR_OFFLINE_REPORT" \
2240 "Offline accelerator catalog validation" \
2241 "The validator reported drift but emitted no parseable actionable findings." \
2242 "$ACCELERATOR_OFFLINE_ERROR"
2243 else
22441 echo " Found ${ACCELERATOR_OFFLINE_COUNT} offline accelerator policy finding(s)."
2245 fi
2246else
22471 ACCELERATOR_OFFLINE_COUNT=1
22481 record_accelerator_operational_error \
2249 "$ACCELERATOR_OFFLINE_REPORT" \
2250 "Offline accelerator catalog validation" \
2251 "The deterministic validator exited with status ${ACCELERATOR_OFFLINE_STATUS}." \
2252 "$ACCELERATOR_OFFLINE_ERROR"
22531 echo " Offline accelerator validator failed operationally."
2254fi
2255
225635if ! aws sts get-caller-identity >/dev/null 2>&1; then
22573 ACCELERATOR_SKIP_REASON="No AWS credentials available for the online EC2 catalog check (needs ec2:DescribeRegions and ec2:DescribeInstanceTypes); offline policy validation still ran."
22583 echo " $ACCELERATOR_SKIP_REASON"
2259else
226032 python3 scripts/accelerator_catalog.py check-online \
2261 --report "$ACCELERATOR_ONLINE_REPORT" \
2262 --json-summary \
2263 >"$ACCELERATOR_ONLINE_SUMMARY" \
2264 2>"$ACCELERATOR_ONLINE_ERROR"
226532 ACCELERATOR_ONLINE_STATUS=$?
226635 if [ "$ACCELERATOR_ONLINE_STATUS" -eq 0 ] || [ "$ACCELERATOR_ONLINE_STATUS" -eq 1 ]; then
226762 if ACCELERATOR_ONLINE_COUNT="$(parse_accelerator_drift_count "$ACCELERATOR_ONLINE_SUMMARY")"; then
226858 if { [ "$ACCELERATOR_ONLINE_STATUS" -eq 0 ] && [ "$ACCELERATOR_ONLINE_COUNT" -ne 0 ]; } \
226931 || { [ "$ACCELERATOR_ONLINE_STATUS" -eq 1 ] && [ "$ACCELERATOR_ONLINE_COUNT" -eq 0 ]; }; then
22701 ACCELERATOR_ONLINE_COUNT=1
22711 record_accelerator_operational_error \
2272 "$ACCELERATOR_ONLINE_REPORT" \
2273 "Online EC2 accelerator catalog drift" \
2274 "The command exit status disagreed with its JSON drift summary." \
2275 "$ACCELERATOR_ONLINE_ERROR"
22761 echo " Online accelerator scan returned an inconsistent result."
227729 elif [ "$ACCELERATOR_ONLINE_COUNT" -eq 0 ]; then
227827 echo " Live EC2 accelerator catalog is current."
2279 else
22802 echo " Found ${ACCELERATOR_ONLINE_COUNT} live EC2 catalog drift finding(s)."
2281 fi
2282 else
22831 ACCELERATOR_ONLINE_COUNT=1
22841 record_accelerator_operational_error \
2285 "$ACCELERATOR_ONLINE_REPORT" \
2286 "Online EC2 accelerator catalog drift" \
2287 "The online scanner emitted a missing or malformed JSON summary." \
2288 "$ACCELERATOR_ONLINE_ERROR"
22891 echo " Online accelerator scan summary could not be parsed."
2290 fi
2291 else
22921 ACCELERATOR_ONLINE_COUNT=1
22931 record_accelerator_operational_error \
2294 "$ACCELERATOR_ONLINE_REPORT" \
2295 "Online EC2 accelerator catalog drift" \
2296 "The online scanner exited with status ${ACCELERATOR_ONLINE_STATUS}." \
2297 "$ACCELERATOR_ONLINE_ERROR"
22981 echo " Online accelerator scanner failed operationally."
2299 fi
2300fi
2301
230235ACCELERATOR_COUNT=$((ACCELERATOR_OFFLINE_COUNT + ACCELERATOR_ONLINE_COUNT))
230338if [ -n "$ACCELERATOR_SKIP_REASON" ] && [ "$ACCELERATOR_COUNT" -eq 0 ]; then
23043 ACCELERATOR_SUMMARY_SKIP_REASON="$ACCELERATOR_SKIP_REASON"
2305fi
2306
2307# ---------------------------------------------------------------------------
2308# Summary + Markdown report
2309# ---------------------------------------------------------------------------
231035echo ""
231135echo "=== Summary ==="
231235echo "Python packages outdated: $PYTHON_COUNT"
231335echo "Docker images outdated: $DOCKER_COUNT"
231435echo "Helm charts outdated: $HELM_COUNT"
231535if [ -n "$ADDON_SKIP_REASON" ]; then
23166 echo "EKS add-ons outdated: (skipped)"
2317else
231829 echo "EKS add-ons outdated: $ADDON_COUNT"
2319fi
232035if [ -n "$EKS_K8S_SKIP_REASON" ]; then
23216 echo "EKS Kubernetes version: (skipped)"
2322else
232329 echo "EKS Kubernetes version: $EKS_K8S_COUNT"
2324fi
232535if [ -n "$AURORA_SKIP_REASON" ]; then
23265 echo "Aurora PostgreSQL: (skipped)"
2327else
232830 echo "Aurora PostgreSQL: $AURORA_COUNT"
2329fi
233035if [ -n "$EMR_SKIP_REASON" ]; then
23316 echo "EMR Serverless release: (skipped)"
2332else
233329 echo "EMR Serverless release: $EMR_COUNT"
2334fi
233535if [ -n "$BEDROCK_MODEL_SKIP_REASON" ]; then
23366 echo "Bedrock default model: (skipped)"
2337else
233829 echo "Bedrock default model: $BEDROCK_MODEL_COUNT"
2339fi
234035if [ -n "$ACCELERATOR_SKIP_REASON" ]; then
23413 echo "Accelerator catalog: ${ACCELERATOR_COUNT} (online skipped)"
2342else
234332 echo "Accelerator catalog: $ACCELERATOR_COUNT"
2344fi
234535echo "Dockerfile.dev pins: $DOCKERFILE_COUNT"
234635echo "Pre-commit hooks: $PRECOMMIT_COUNT"
234735if [ -n "$CDK_ENUM_SKIP_REASON" ]; then
23482 echo "CDK enum constants: (skipped)"
2349else
235033 echo "CDK enum constants: $CDK_ENUM_COUNT"
2351fi
235235if [ -n "$PYTHON_RELEASE_SKIP_REASON" ]; then
23533 echo "Python release: (skipped)"
2354else
235532 echo "Python release: $PYTHON_RELEASE_COUNT"
2356fi
235735if [ -n "$RUBY_RELEASE_SKIP_REASON" ]; then
23583 echo "Ruby release: (skipped)"
2359else
236032 echo "Ruby release: $RUBY_RELEASE_COUNT"
2361fi
236235if [ -n "$UBUNTU_LTS_SKIP_REASON" ]; then
23634 echo "Ubuntu LTS base: (skipped)"
2364else
236531 echo "Ubuntu LTS base: $UBUNTU_LTS_COUNT"
2366fi
236735if [ -n "$RUNNER_IMAGE_SKIP_REASON" ]; then
23682 echo "Runner images: (skipped)"
2369else
237033 echo "Runner images: $RUNNER_IMAGE_COUNT"
2371fi
237235if [ -n "$AUTOPILOT_SKIP_REASON" ]; then
23735 echo "GCO autopilot pins: (skipped)"
2374else
237530 echo "GCO autopilot pins: $AUTOPILOT_COUNT"
2376fi
237735echo "CI tooling pins: $CI_TOOLING_COUNT"
237835echo "Version consistency: $CONSISTENCY_COUNT"
237935echo "Base-image epochs: $EPOCH_COUNT"
238035echo "Suppression expiries: $SUPPRESSION_COUNT"
238135echo "Lockfile freshness: $LOCKFILE_COUNT"
2382125INCOMPLETE_LOOKUP_COUNT="$(sort -u "$INCOMPLETE_REASONS_FILE" 2>/dev/null | grep -c . || true)"
238335echo "Incomplete lookups: ${INCOMPLETE_LOOKUP_COUNT:-0}"
2384
238535SCAN_COMPLETE=true
238635if ! dependency_scan_is_complete \
2387 "$INCOMPLETE_REASONS_FILE" \
2388 "$ADDON_SKIP_REASON" \
2389 "$EKS_K8S_SKIP_REASON" \
2390 "$AURORA_SKIP_REASON" \
2391 "$EMR_SKIP_REASON" \
2392 "$BEDROCK_MODEL_SKIP_REASON" \
2393 "$ACCELERATOR_SKIP_REASON" \
2394 "$AUTOPILOT_SKIP_REASON" \
2395 "$CDK_ENUM_SKIP_REASON" \
2396 "$PYTHON_RELEASE_SKIP_REASON" \
2397 "$RUBY_RELEASE_SKIP_REASON" \
2398 "$UBUNTU_LTS_SKIP_REASON" \
2399 "$RUNNER_IMAGE_SKIP_REASON"; then
240020 SCAN_COMPLETE=false
2401fi
2402
2403# ----- Machine-readable findings -----
2404# One record per surface, in the order of the summary table below, with the
2405# columns named the way the Markdown tables label them. Rows are the same
2406# pipe-delimited results the tables render; the Python surface is already
2407# JSON and the accelerator surface carries its two Markdown reports.
2408105PYTHON_FINDINGS="$(printf '%s' "${PYTHON_OUTDATED:-[]}" \
2409 | jq '[.[] | {name: .name, current: .version, latest: .latest_version}]')"
241035findings_surface_json "Python Packages" "routine" "$PYTHON_COUNT" "" "$PYTHON_FINDINGS"
241135findings_surface "npm Packages" "routine" "$NPM_COUNT" "" "graph|package|current|latest" "$NPM_RESULTS"
241235findings_surface "Docker Images" "routine" "$DOCKER_COUNT" "" "image|current|latest" "$DOCKER_RESULTS"
241335findings_surface "Helm Charts" "routine" "$HELM_COUNT" "" "chart|name|current|latest" "$HELM_RESULTS"
241435findings_surface "EKS Add-ons" "routine" "$ADDON_COUNT" "$ADDON_SKIP_REASON" "addon|current|latest" "$ADDON_RESULTS"
241535findings_surface "EKS Kubernetes Version" "act soon" "$EKS_K8S_COUNT" "$EKS_K8S_SKIP_REASON" "pin|current|latest|standard_support_ends" "$EKS_K8S_RESULTS"
241635findings_surface "Aurora PostgreSQL Engine" "routine" "$AURORA_COUNT" "$AURORA_SKIP_REASON" "engine|current|latest" "$AURORA_RESULTS"
241735findings_surface "EMR Serverless" "routine" "$EMR_COUNT" "$EMR_SKIP_REASON" "release|current|latest" "$EMR_RESULTS"
241835findings_surface "Bedrock Default Model" "routine" "$BEDROCK_MODEL_COUNT" "$BEDROCK_MODEL_SKIP_REASON" "key|current|latest" "$BEDROCK_MODEL_RESULTS"
241970ACCELERATOR_EXTRA="$(jq -n --rawfile offline "$ACCELERATOR_OFFLINE_REPORT" --rawfile online "$ACCELERATOR_ONLINE_REPORT" \
2420 '{offline_report_markdown: $offline, online_report_markdown: $online}')"
242135findings_surface_json "Accelerator Catalog and NodePools" "act soon" "$ACCELERATOR_COUNT" "$ACCELERATOR_SUMMARY_SKIP_REASON" "[]" "$ACCELERATOR_EXTRA"
242235findings_surface "Dockerfile.dev Pins" "routine" "$DOCKERFILE_COUNT" "" "pin|current|latest" "$DOCKERFILE_RESULTS"
242335findings_surface "GCO Autopilot Pins" "act soon" "$AUTOPILOT_COUNT" "$AUTOPILOT_SKIP_REASON" "surface|current|latest|url" "$AUTOPILOT_RESULTS"
242435findings_surface "Pre-commit Hooks" "routine" "$PRECOMMIT_COUNT" "" "repo|current|latest" "$PRECOMMIT_RESULTS"
242535findings_surface "CDK Enum Constants" "routine" "$CDK_ENUM_COUNT" "$CDK_ENUM_SKIP_REASON" "constant|enum_class|current|latest" "$CDK_ENUM_RESULTS"
242635findings_surface "Python Release" "informational" "$PYTHON_RELEASE_COUNT" "$PYTHON_RELEASE_SKIP_REASON" "surface|current|latest" "$PYTHON_RELEASE_RESULTS"
242735findings_surface "Ruby Release" "informational" "$RUBY_RELEASE_COUNT" "$RUBY_RELEASE_SKIP_REASON" "surface|current|latest" "$RUBY_RELEASE_RESULTS"
242835findings_surface "Ubuntu LTS Base" "informational" "$UBUNTU_LTS_COUNT" "$UBUNTU_LTS_SKIP_REASON" "surface|current|latest" "$UBUNTU_LTS_RESULTS"
242935findings_surface "Runner Images" "act soon" "$RUNNER_IMAGE_COUNT" "$RUNNER_IMAGE_SKIP_REASON" "label|current|latest" "$RUNNER_IMAGE_RESULTS"
243035findings_surface "CI Tooling" "act soon" "$CI_TOOLING_COUNT" "" "tool|current|latest|url" "$CI_TOOLING_RESULTS"
243135findings_surface "Version Consistency" "routine" "$CONSISTENCY_COUNT" "" "what|pinned_values" "$CONSISTENCY_RESULTS"
243235findings_surface "Base-image Security Epochs" "act soon" "$EPOCH_COUNT" "" "dockerfile|arg|epoch|age_days" "$EPOCH_RESULTS"
243335findings_surface "Suppression Expiries" "act soon" "$SUPPRESSION_COUNT" "" "file|id|expires|days_left" "$SUPPRESSION_RESULTS"
243435findings_surface "Lockfile Freshness" "routine" "$LOCKFILE_COUNT" "" "dependency|expected|locked" "$LOCKFILE_RESULTS"
2435
243689if [ "$PYTHON_COUNT" -eq 0 ] && [ "$NPM_COUNT" -eq 0 ] && [ "$DOCKER_COUNT" -eq 0 ] \
243750 && [ "$HELM_COUNT" -eq 0 ] && [ "$ADDON_COUNT" -eq 0 ] \
243825 && [ "$EKS_K8S_COUNT" -eq 0 ] \
243950 && [ "$AURORA_COUNT" -eq 0 ] && [ "$EMR_COUNT" -eq 0 ] \
244025 && [ "$DOCKERFILE_COUNT" -eq 0 ] \
244125 && [ "$AUTOPILOT_COUNT" -eq 0 ] \
244222 && [ "$PRECOMMIT_COUNT" -eq 0 ] \
244322 && [ "$CDK_ENUM_COUNT" -eq 0 ] \
244422 && [ "$PYTHON_RELEASE_COUNT" -eq 0 ] \
244522 && [ "$RUBY_RELEASE_COUNT" -eq 0 ] \
244622 && [ "$UBUNTU_LTS_COUNT" -eq 0 ] \
244722 && [ "$RUNNER_IMAGE_COUNT" -eq 0 ] \
244822 && [ "$BEDROCK_MODEL_COUNT" -eq 0 ] \
244922 && [ "$ACCELERATOR_COUNT" -eq 0 ] \
245018 && [ "$CI_TOOLING_COUNT" -eq 0 ] \
245118 && [ "$CONSISTENCY_COUNT" -eq 0 ] \
245215 && [ "$EPOCH_COUNT" -eq 0 ] \
245312 && [ "$SUPPRESSION_COUNT" -eq 0 ] \
245412 && [ "$LOCKFILE_COUNT" -eq 0 ]; then
245512 echo ""
245612 SKIP_NOTES=""
245712 if [ -n "$ADDON_SKIP_REASON" ]; then
24583 SKIP_NOTES="EKS add-ons skipped: $ADDON_SKIP_REASON"
2459 fi
246012 if [ -n "$EKS_K8S_SKIP_REASON" ]; then
24614 [ -n "$SKIP_NOTES" ] && SKIP_NOTES="$SKIP_NOTES; "
24622 SKIP_NOTES="${SKIP_NOTES}EKS Kubernetes skipped: $EKS_K8S_SKIP_REASON"
2463 fi
246412 if [ -n "$AURORA_SKIP_REASON" ]; then
24654 [ -n "$SKIP_NOTES" ] && SKIP_NOTES="$SKIP_NOTES; "
24662 SKIP_NOTES="${SKIP_NOTES}Aurora engine skipped: $AURORA_SKIP_REASON"
2467 fi
246812 if [ -n "$EMR_SKIP_REASON" ]; then
24694 [ -n "$SKIP_NOTES" ] && SKIP_NOTES="$SKIP_NOTES; "
24702 SKIP_NOTES="${SKIP_NOTES}EMR Serverless skipped: $EMR_SKIP_REASON"
2471 fi
247212 if [ -n "$BEDROCK_MODEL_SKIP_REASON" ]; then
24734 [ -n "$SKIP_NOTES" ] && SKIP_NOTES="$SKIP_NOTES; "
24742 SKIP_NOTES="${SKIP_NOTES}Bedrock model skipped: $BEDROCK_MODEL_SKIP_REASON"
2475 fi
247612 if [ -n "$ACCELERATOR_SKIP_REASON" ]; then
24774 [ -n "$SKIP_NOTES" ] && SKIP_NOTES="$SKIP_NOTES; "
24782 SKIP_NOTES="${SKIP_NOTES}Online accelerator catalog skipped: $ACCELERATOR_SKIP_REASON"
2479 fi
248012 if [ -n "$AUTOPILOT_SKIP_REASON" ]; then
24813 [ -n "$SKIP_NOTES" ] && SKIP_NOTES="$SKIP_NOTES; "
24822 SKIP_NOTES="${SKIP_NOTES}GCO autopilot pins skipped: $AUTOPILOT_SKIP_REASON"
2483 fi
248412 if [ -n "$CDK_ENUM_SKIP_REASON" ]; then
24852 [ -n "$SKIP_NOTES" ] && SKIP_NOTES="$SKIP_NOTES; "
24861 SKIP_NOTES="${SKIP_NOTES}CDK enums skipped: $CDK_ENUM_SKIP_REASON"
2487 fi
248812 if [ -n "$PYTHON_RELEASE_SKIP_REASON" ]; then
24892 [ -n "$SKIP_NOTES" ] && SKIP_NOTES="$SKIP_NOTES; "
24901 SKIP_NOTES="${SKIP_NOTES}Python release skipped: $PYTHON_RELEASE_SKIP_REASON"
2491 fi
249212 if [ -n "$RUBY_RELEASE_SKIP_REASON" ]; then
24932 [ -n "$SKIP_NOTES" ] && SKIP_NOTES="$SKIP_NOTES; "
24941 SKIP_NOTES="${SKIP_NOTES}Ruby release skipped: $RUBY_RELEASE_SKIP_REASON"
2495 fi
249612 if [ -n "$UBUNTU_LTS_SKIP_REASON" ]; then
24972 [ -n "$SKIP_NOTES" ] && SKIP_NOTES="$SKIP_NOTES; "
24981 SKIP_NOTES="${SKIP_NOTES}Ubuntu LTS base skipped: $UBUNTU_LTS_SKIP_REASON"
2499 fi
250012 if [ -n "$RUNNER_IMAGE_SKIP_REASON" ]; then
25012 [ -n "$SKIP_NOTES" ] && SKIP_NOTES="$SKIP_NOTES; "
25021 SKIP_NOTES="${SKIP_NOTES}Runner images skipped: $RUNNER_IMAGE_SKIP_REASON"
2503 fi
250412 if [ -s "$INCOMPLETE_REASONS_FILE" ]; then
25056 [ -n "$SKIP_NOTES" ] && SKIP_NOTES="$SKIP_NOTES; "
250610 SKIP_NOTES="${SKIP_NOTES}Incomplete checks: $(join_scan_incomplete_reasons)"
2507 fi
250812 if [ "$SCAN_COMPLETE" != true ]; then
25098 STATUS_MESSAGE="No drift was found in completed checks, but the scan is incomplete."
2510 else
25114 STATUS_MESSAGE="All dependencies are up to date."
2512 fi
251312 echo "$STATUS_MESSAGE"
251412 write_findings_document "$FINDINGS_FILE" "$SCAN_COMPLETE" false "$INCOMPLETE_REASONS_FILE"
251512 rm -f "$FINDINGS_SURFACES"
251612 rm -f "$NPM_RESULTS" "$DOCKER_RESULTS" "$HELM_RESULTS" "$ADDON_RESULTS" "$EKS_K8S_RESULTS" "$AURORA_RESULTS" "$EMR_RESULTS" "$DOCKERFILE_RESULTS" "$AUTOPILOT_RESULTS" "$PRECOMMIT_RESULTS" "$CDK_ENUM_RESULTS" "$PYTHON_RELEASE_RESULTS" "$RUBY_RELEASE_RESULTS" "$UBUNTU_LTS_RESULTS" "$RUNNER_IMAGE_RESULTS" "$RUNNER_IMAGE_NOTES" "$BEDROCK_MODEL_RESULTS" "$CI_TOOLING_RESULTS" "$CONSISTENCY_RESULTS" "$EPOCH_RESULTS" "$SUPPRESSION_RESULTS" "$LOCKFILE_RESULTS" "$ACCELERATOR_OFFLINE_REPORT" "$ACCELERATOR_ONLINE_REPORT" "$ACCELERATOR_ONLINE_SUMMARY" "$ACCELERATOR_OFFLINE_ERROR" "$ACCELERATOR_ONLINE_ERROR" "$INCOMPLETE_REASONS_FILE"
251712 if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
2518 {
251912 echo "# Dependency Update Report"
252012 echo ""
252112 echo "$STATUS_MESSAGE"
252212 if [ -n "$SKIP_NOTES" ]; then
25238 echo ""
25248 echo "_Incomplete or skipped checks: ${SKIP_NOTES}_"
2525 fi
2526 } >> "$GITHUB_STEP_SUMMARY"
2527 fi
252812 if [ -n "${GITHUB_OUTPUT:-}" ]; then
2529 {
253012 echo "has_drift=false"
253112 echo "scan_complete=$SCAN_COMPLETE"
253212 echo "findings_path=$FINDINGS_FILE"
2533 } >> "$GITHUB_OUTPUT"
2534 fi
253512 echo "Wrote findings to $FINDINGS_FILE"
253612 exit 0
2537fi
2538
2539# summary_row <title> <count> <skip_reason> <urgency>
2540# Emits one row of the top-of-report TL;DR table. Surfaces with drift link to
2541# their detailed section; skipped surfaces are marked and get no urgency.
2542summary_row() {
2543529 local title="$1" count="$2" skip="$3" urgency="$4"
2544529 local anchor label
25451058 anchor="$(md_anchor "$title")"
2546529 if [ -n "$skip" ]; then
254731 label="skipped"
254831 urgency="—"
2549498 elif [ "$count" -gt 0 ]; then
2550159 label="${count} update(s)"
2551159 title="[${title}](#${anchor})"
2552339 elif [ "$SCAN_COMPLETE" != true ]; then
2553172 label="no drift found (incomplete scan)"
2554172 urgency="—"
2555 else
2556167 label="up to date"
2557167 urgency="—"
2558 fi
2559529 echo "| ${title} | ${label} | ${urgency} |"
2560}
2561
256223write_findings_document "$FINDINGS_FILE" "$SCAN_COMPLETE" true "$INCOMPLETE_REASONS_FILE"
256323rm -f "$FINDINGS_SURFACES"
2564
2565{
256623 echo "# Dependency Update Report"
256723 echo ""
256846 echo "_Generated $(date -u '+%Y-%m-%d %H:%M UTC') by the \`deps-scan\` workflow._"
256923 echo ""
257023 echo "> [!TIP]"
257123 echo "> Reproduce this update list programmatically, on demand: run"
257223 echo "> \`gco deps scan\` from a checkout (add \`--nodepools-only\` for just the"
257323 echo "> accelerator-catalog / NodePool freshness check, \`-o json\` for a"
257423 echo "> machine-readable envelope), or call the \`deps_scan\` MCP tool from an"
257523 echo "> agent session. Surfaces needing AWS credentials or missing host tools"
257623 echo "> are skipped and flagged as incomplete, exactly as in this workflow."
257723 echo ""
257823 if [ "$SCAN_COMPLETE" != true ]; then
257912 echo "> [!WARNING]"
258012 echo "> **Incomplete scan.** Zero-count surfaces are provisional, not confirmed current."
258112 if [ -s "$INCOMPLETE_REASONS_FILE" ]; then
258220 echo "> Recorded failures: $(join_scan_incomplete_reasons)"
2583 else
25842 echo "> One or more credential-dependent or optional checks were skipped; see the workflow log."
2585 fi
258612 echo ""
2587 fi
2588
2589 # ----- TL;DR summary -----
259023 echo "## Summary"
259123 echo ""
259223 echo "| Surface | Status | Urgency |"
259323 echo "|---------|--------|---------|"
259423 summary_row "Python Packages" "$PYTHON_COUNT" "" "routine"
259523 summary_row "npm Packages" "$NPM_COUNT" "" "routine"
259623 summary_row "Docker Images" "$DOCKER_COUNT" "" "routine"
259723 summary_row "Helm Charts" "$HELM_COUNT" "" "routine"
259823 summary_row "EKS Add-ons" "$ADDON_COUNT" "$ADDON_SKIP_REASON" "routine"
259923 summary_row "EKS Kubernetes Version" "$EKS_K8S_COUNT" "$EKS_K8S_SKIP_REASON" "act soon"
260023 summary_row "Aurora PostgreSQL Engine" "$AURORA_COUNT" "$AURORA_SKIP_REASON" "routine"
260123 summary_row "EMR Serverless" "$EMR_COUNT" "$EMR_SKIP_REASON" "routine"
260223 summary_row "Bedrock Default Model" "$BEDROCK_MODEL_COUNT" "$BEDROCK_MODEL_SKIP_REASON" "routine"
260323 summary_row "Accelerator Catalog and NodePools" "$ACCELERATOR_COUNT" "$ACCELERATOR_SUMMARY_SKIP_REASON" "act soon"
260423 summary_row "Dockerfile.dev Pins" "$DOCKERFILE_COUNT" "" "routine"
260523 summary_row "GCO Autopilot Pins" "$AUTOPILOT_COUNT" "$AUTOPILOT_SKIP_REASON" "act soon"
260623 summary_row "Pre-commit Hooks" "$PRECOMMIT_COUNT" "" "routine"
260723 summary_row "CDK Enum Constants" "$CDK_ENUM_COUNT" "$CDK_ENUM_SKIP_REASON" "routine"
260823 summary_row "Python Release" "$PYTHON_RELEASE_COUNT" "$PYTHON_RELEASE_SKIP_REASON" "informational"
260923 summary_row "Ruby Release" "$RUBY_RELEASE_COUNT" "$RUBY_RELEASE_SKIP_REASON" "informational"
261023 summary_row "Ubuntu LTS Base" "$UBUNTU_LTS_COUNT" "$UBUNTU_LTS_SKIP_REASON" "informational"
261123 summary_row "Runner Images" "$RUNNER_IMAGE_COUNT" "$RUNNER_IMAGE_SKIP_REASON" "act soon"
261223 summary_row "CI Tooling" "$CI_TOOLING_COUNT" "" "act soon"
261323 summary_row "Version Consistency" "$CONSISTENCY_COUNT" "" "routine"
261423 summary_row "Base-image Security Epochs" "$EPOCH_COUNT" "" "act soon"
261523 summary_row "Suppression Expiries" "$SUPPRESSION_COUNT" "" "act soon"
261623 summary_row "Lockfile Freshness" "$LOCKFILE_COUNT" "" "routine"
261723 echo ""
261823 echo "_Urgency is a hint: **act soon** = security or a support/cost deadline;"
261923 echo "**routine** = bump at leisure; **informational** = no action yet. Only"
262023 echo "surfaces with drift have a detailed section below._"
262123 echo ""
2622
262323 if [ "$PYTHON_COUNT" -gt 0 ]; then
26248 echo "## Python Packages"
26258 echo ""
26268 echo "Direct dependencies pinned in \`pyproject.toml\` (transitive-only drift"
26278 echo "is excluded — those versions are controlled by upstream pins and bumping"
26288 echo "them ourselves either no-ops or breaks the resolver)."
26298 echo ""
26308 echo "| Package | Current | Latest | Ref |"
26318 echo "|---------|---------|--------|-----|"
263216 echo "$PYTHON_OUTDATED" | jq -r '.[] | "| \(.name) | \(.version) | \(.latest_version) | [PyPI](https://pypi.org/project/\(.name)/) |"'
26338 echo ""
2634 fi
2635
263623 if [ "$NPM_COUNT" -gt 0 ]; then
26378 echo "## npm Packages"
26388 echo ""
26398 echo "Exact direct pins in every repository-owned \`package.json\` (the root"
26408 echo "tooling graph and \`lambda/inference-streaming-proxy\`), compared against"
26418 echo "each package's \`latest\` dist-tag. Transitives are excluded — those are"
26428 echo "controlled by the lockfiles. Bump the pin, then regenerate the graph's"
26438 echo "\`package-lock.json\` with the pinned npm."
26448 echo ""
264516 npm_disp="$(mktemp)"
264644 while IFS='|' read -r graph pkg cur lat; do
264714 echo "\`${graph}\`|${pkg}|${cur}|${lat}|[npm](https://www.npmjs.com/package/${pkg})"
2648 done < "$NPM_RESULTS" > "$npm_disp"
26498 emit_md_table "Graph|Package|Current|Latest|Ref" "$npm_disp"
26508 rm -f "$npm_disp"
26518 echo ""
2652 fi
2653
265423 if [ "$DOCKER_COUNT" -gt 0 ]; then
265510 echo "## Docker Images"
265610 echo ""
265710 emit_md_table "Image|Current|Latest" "$DOCKER_RESULTS"
265810 echo ""
2659 fi
2660
266123 if [ "$HELM_COUNT" -gt 0 ]; then
26628 echo "## Helm Charts"
26638 echo ""
266416 helm_disp="$(mktemp)"
266592 while IFS='|' read -r cname chart cur lat; do
266638 echo "${cname}|${chart}|${cur}|${lat}|[ArtifactHub](https://artifacthub.io/packages/search?ts_query_web=${chart})"
2667 done < "$HELM_RESULTS" > "$helm_disp"
26688 emit_md_table "Chart|Name|Current|Latest|Ref" "$helm_disp"
26698 rm -f "$helm_disp"
26708 echo ""
2671 fi
2672
267323 if [ "$ADDON_COUNT" -gt 0 ]; then
26748 echo "## EKS Add-ons"
26758 echo ""
26768 emit_md_table "Add-on|Current|Latest" "$ADDON_RESULTS"
26778 echo ""
2678 fi
2679
268023 if [ "$EKS_K8S_COUNT" -gt 0 ]; then
26817 echo "## EKS Kubernetes Version"
26827 echo ""
26837 echo "The Kubernetes minor pinned in \`cdk.json::kubernetes_version\` is behind"
26847 echo "the latest release still in EKS **standard support**. Upgrade before"
26857 echo "standard support ends to avoid the extended-support pricing uplift."
26867 echo ""
268714 eks_disp="$(mktemp)"
268828 while IFS='|' read -r pin cur lat eos; do
26897 echo "\`${pin}\`|${cur}|${lat}|${eos}"
2690 done < "$EKS_K8S_RESULTS" > "$eks_disp"
26917 emit_md_table "Pin|Current|Latest (standard support)|Std support ends" "$eks_disp"
26927 rm -f "$eks_disp"
26937 echo ""
2694 fi
2695
269623 if [ "$AURORA_COUNT" -gt 0 ]; then
26978 echo "## Aurora PostgreSQL Engine"
26988 echo ""
26998 emit_md_table "Engine|Current|Latest" "$AURORA_RESULTS"
27008 echo ""
2701 fi
2702
270323 if [ "$EMR_COUNT" -gt 0 ]; then
27047 echo "## EMR Serverless"
27057 echo ""
27067 emit_md_table "Release|Current|Latest" "$EMR_RESULTS"
27077 echo ""
2708 fi
2709
271023 if [ "$BEDROCK_MODEL_COUNT" -gt 0 ]; then
27117 echo "## Bedrock Default Model"
27127 echo ""
27137 echo "A Bedrock model default configured in \`cdk.json\` is behind a newer"
27147 echo "release in the same model family. For"
27157 echo "\`bedrock.mission_default_model_id\`, \`bedrock.codex_default_model_id\`"
27167 echo "and \`bedrock.opencode_default_model_id\`, update the value and"
27177 echo "re-capture the matching scaffold fixture"
27187 echo "(\`scripts/capture_scaffold_fixtures.py\`). For"
27197 echo "\`bedrock.capacity_advisor_default_model_id\` and"
27207 echo "\`bedrock.claude_code_default_model_id\`, updating the value is enough."
27217 echo "For the embedding keys — \`bedrock.embedding_model_id\` (Mission memory)"
27227 echo "and \`vector_store.embedding_model_id\` (workload RAG corpus) — stored"
27237 echo "vectors are only comparable to vectors from the same model: plan to"
27247 echo "re-embed (for the vector store, re-ingest the corpus) or segregate"
27257 echo "existing data before adopting the newer model."
27267 echo ""
27277 emit_md_table "Configuration key|Current|Latest" "$BEDROCK_MODEL_RESULTS"
27287 echo ""
2729 fi
2730
273123 if [ "$ACCELERATOR_COUNT" -gt 0 ]; then
27325 echo "## Accelerator Catalog and NodePools"
27335 echo ""
27345 echo "The offline check keeps reviewed lifecycle/generation policy, Karpenter"
27355 echo "NodePools, \`historical.watch_instance_types\`, and the Spot Placement"
27365 echo "Score instance pools synchronized. The online check compares the catalog"
27375 echo "with EC2 across enabled commercial Regions."
27385 echo "Follow each recommended change; review family metadata before refreshing"
27395 echo "the checked-in catalog."
27405 echo ""
27415 if [ -s "$ACCELERATOR_OFFLINE_REPORT" ]; then
27423 sed -E 's/^### /#### /; s/^## /### /' "$ACCELERATOR_OFFLINE_REPORT"
27433 echo ""
2744 fi
27455 if [ -s "$ACCELERATOR_ONLINE_REPORT" ]; then
27465 sed -E 's/^### /#### /; s/^## /### /' "$ACCELERATOR_ONLINE_REPORT"
27475 echo ""
2748 fi
2749 fi
2750
275123 if [ "$DOCKERFILE_COUNT" -gt 0 ]; then
27528 echo "## Dockerfile.dev Pins"
27538 echo ""
27548 echo "Tooling versions pinned as build-time ARGs in \`Dockerfile.dev\`."
27558 echo ""
275616 dockerfile_disp="$(mktemp)"
2757160 while IFS='|' read -r pin cur lat; do
275872 echo "\`${pin}\`|${cur}|${lat}"
2759 done < "$DOCKERFILE_RESULTS" > "$dockerfile_disp"
27608 emit_md_table "Pin|Current|Latest" "$dockerfile_disp"
27618 rm -f "$dockerfile_disp"
27628 echo ""
2763 fi
2764
276523 if [ "$AUTOPILOT_COUNT" -gt 0 ]; then
276611 echo "## GCO Autopilot Pins"
276711 echo ""
276811 echo "\`gco autopilot\`'s dependency surfaces in \`cli/autopilot.py\`: the"
276911 echo "pinned \`CLAUDE_CODE_VERSION\`, \`CODEX_VERSION\` and \`OPENCODE_VERSION\`"
277011 echo "agent CLIs (each compared against its npm \`latest\` dist-tag) and the"
277111 echo "launch-time"
277211 echo "companion MCP servers"
277311 echo "(reported when a package is missing, deprecated, or yanked on its"
277411 echo "registry — an unhealthy companion breaks every new session, so treat"
277511 echo "it like the removals documented in \`gco_mcp/README.md\`). When"
277611 echo "changing the companion set, update \`cli/autopilot.py\` and the"
277711 echo "\`gco_mcp/README.md\` tables together; \`tests/test_cli_autopilot.py\`"
277811 echo "enforces the lockstep."
277911 echo ""
278022 autopilot_disp="$(mktemp)"
278184 while IFS='|' read -r surface cur stat url; do
278231 echo "${surface}|\`${cur}\`|${stat}|[registry](${url})"
2783 done < "$AUTOPILOT_RESULTS" > "$autopilot_disp"
278411 emit_md_table "Surface|Current|Latest / status|Ref" "$autopilot_disp"
278511 rm -f "$autopilot_disp"
278611 echo ""
2787 fi
2788
278923 if [ "$PRECOMMIT_COUNT" -gt 0 ]; then
27909 echo "## Pre-commit Hooks"
27919 echo ""
27929 echo "Hook \`rev:\` pins in \`.pre-commit-config.yaml\` are behind the latest"
27939 echo "tag published by their upstream repos. Bump in \`.pre-commit-config.yaml\`,"
27949 echo "then run \`pre-commit autoupdate\` locally (or edit by hand) and verify"
27959 echo "the hooks still pass."
27969 echo ""
279718 precommit_disp="$(mktemp)"
279858 while IFS='|' read -r repo cur lat; do
279920 echo "${repo}|\`${cur}\`|\`${lat}\`|[releases](${repo}/releases)"
2800 done < "$PRECOMMIT_RESULTS" > "$precommit_disp"
28019 emit_md_table "Repo|Current|Latest|Ref" "$precommit_disp"
28029 rm -f "$precommit_disp"
28039 echo ""
2804 fi
2805
280623 if [ "$CDK_ENUM_COUNT" -gt 0 ]; then
28077 echo "## CDK Enum Constants"
28087 echo ""
28097 echo "Enum-name constants in \`gco/stacks/constants.py\` are behind the highest"
28107 echo "enum member exposed by the installed \`aws-cdk-lib\`. Update the constant"
28117 echo "in \`constants.py\` (and any related deployment notes) so new stacks"
28127 echo "construct the latest CDK enum."
28137 echo ""
28147 emit_md_table "Constant|CDK enum class|Current|Latest" "$CDK_ENUM_RESULTS" code
28157 echo ""
2816 fi
2817
281823 if [ "$PYTHON_RELEASE_COUNT" -gt 0 ]; then
28198 echo "## Python Release"
28208 echo ""
28218 echo "A newer stable Python release is available on python.org than the version"
28228 echo "encoded by \`LAMBDA_PYTHON_RUNTIME\`. AWS Lambda may lag the upstream"
28238 echo "release by a few months — wait for the matching \`Runtime.PYTHON_X_Y\`"
28248 echo "enum to appear in \`aws-cdk-lib\` (tracked by the **CDK Enum Constants**"
28258 echo "section above) before bumping. See <https://www.python.org/downloads/>."
28268 echo ""
28278 emit_md_table "Surface|Current|Latest" "$PYTHON_RELEASE_RESULTS"
28288 echo ""
2829 fi
2830
283123 if [ "$RUNNER_IMAGE_COUNT" -gt 0 ]; then
28321 echo "## Runner Images"
28331 echo ""
28341 echo "A \`runs-on:\` label below is behind a newer **generally-available** image,"
28351 echo "or names one upstream has deprecated (a deprecated image is a removal"
28361 echo "notice with a date attached). Images still in *preview* are deliberately"
28371 echo "not listed here — see the notes under the skipped/collapsed section for"
28381 echo "those, since moving to a preview trades a stable CI platform for an"
28391 echo "unannounced one. Update the \`runs-on:\` value in the named workflow job."
28401 echo "See <https://github.com/actions/runner-images#available-images>."
28411 echo ""
28421 emit_md_table "Label|Current image|Recommended" "$RUNNER_IMAGE_RESULTS"
28431 echo ""
2844 fi
2845
284623 if [ "$RUBY_RELEASE_COUNT" -gt 0 ]; then
28478 echo "## Ruby Release"
28488 echo ""
28498 echo "A newer stable Ruby series is supported upstream than the one pinned in"
28508 echo "\`.ruby-version\`. Ruby is CI-only — \`bundle exec bashcov\` measures shell"
28518 echo "coverage in the \`unit:bats:shell\` job — so bumping is low risk, but it is"
28528 echo "still a deliberate move: check that \`ruby/setup-ruby\` publishes a prebuilt"
28538 echo "binary for the new series and that \`bashcov\` supports it, then update"
28548 echo "\`.ruby-version\` and re-resolve \`Gemfile.lock\`."
28558 echo "See <https://endoflife.date/ruby>."
28568 echo ""
28578 emit_md_table "Surface|Current|Latest" "$RUBY_RELEASE_RESULTS"
28588 echo ""
2859 fi
2860
286123 if [ "$UBUNTU_LTS_COUNT" -gt 0 ]; then
28625 echo "## Ubuntu LTS Base"
28635 echo ""
28645 echo "The Slinky Slurm images are pinned to an older Ubuntu LTS base than the"
28655 echo "newest one that has shipped its first point release, and the registry"
28665 echo "already publishes the pinned Slurm version on the newer base. The image"
28675 echo "sweep never suggests this move because it compares within a tag's variant"
28685 echo "family; changing base is a deliberate step: update \`SLURM_IMAGE_TAG\` in"
28695 echo "\`gco/slurm_config.py\` and every image tag in \`lambda/helm-installer/charts.yaml\`"
28705 echo "together (the unit suite holds them in lockstep), then re-run the live Slurm"
28715 echo "validation before shipping. See <https://endoflife.date/ubuntu> and"
28725 echo "<https://github.com/orgs/SlinkyProject/packages>."
28735 echo ""
28745 emit_md_table "Surface|Current|Latest" "$UBUNTU_LTS_RESULTS"
28755 echo ""
2876 fi
2877
2878 # ----- New coverage surfaces -----
2879
288023 if [ "$CI_TOOLING_COUNT" -gt 0 ]; then
28819 echo "## CI Tooling"
28829 echo ""
28839 echo "Tool versions the workflows install by hand — not \`uses:\` refs or"
28849 echo "Dockerfile \`FROM\` lines, so Dependabot doesn't watch them. A stale"
28859 echo "**Trivy** in particular means the CVE scan silently misses newer"
28869 echo "detections; bump the \`*_VERSION\` env / kind-action inputs in lockstep."
28879 echo ""
288818 ci_disp="$(mktemp)"
2889186 while IFS='|' read -r name cur lat url; do
289084 echo "${name}|\`${cur}\`|\`${lat}\`|[releases](${url})"
2891 done < "$CI_TOOLING_RESULTS" > "$ci_disp"
28929 emit_md_table "Tool|Current|Latest|Ref" "$ci_disp"
28939 rm -f "$ci_disp"
28949 echo ""
2895 fi
2896
289723 if [ "$CONSISTENCY_COUNT" -gt 0 ]; then
28987 echo "## Version Consistency"
28997 echo ""
29007 echo "These versions and dependency-management surfaces must move together."
29017 echo "The rows below identify missing coverage or disagreement across runtime,"
29027 echo "package, dev-container, pre-commit, and CI pins."
29037 echo ""
29047 emit_md_table "What|Pinned values" "$CONSISTENCY_RESULTS"
29057 echo ""
2906 fi
2907
290823 if [ "$EPOCH_COUNT" -gt 0 ]; then
29093 echo "## Base-image Security Epochs"
29103 echo ""
29113 echo "The build-time \`*_SECURITY_EPOCH\` ARGs bust the CI layer cache so a"
29123 echo "fresh OS-security-upgrade layer is built. An epoch older than"
29133 echo "${SECURITY_EPOCH_STALE_DAYS} days may be reusing a stale upgrade layer —"
29143 echo "bump the date to force a rebuild that pulls current patches."
29153 echo ""
29166 epoch_disp="$(mktemp)"
291712 while IFS='|' read -r df arg edate eage; do
29183 echo "\`${df}\`|\`${arg}\`|${edate}|${eage}"
2919 done < "$EPOCH_RESULTS" > "$epoch_disp"
29203 emit_md_table "Dockerfile|ARG|Epoch|Age (days)" "$epoch_disp"
29213 rm -f "$epoch_disp"
29223 echo ""
2923 fi
2924
292523 if [ "$SUPPRESSION_COUNT" -gt 0 ]; then
29265 echo "## Suppression Expiries"
29275 echo ""
29285 echo "\`.trivyignore\` / \`.pip-audit-ignore\` / \`.npm-audit-ignore\` entries"
29295 echo "expiring within ${SUPPRESSION_EXPIRY_WARN_DAYS} days (the CI validator hard-fails a PR on"
29305 echo "the expiry date). Re-evaluate each: drop it if the CVE is fixed upstream,"
29315 echo "or extend with a fresh justification if not."
29325 echo ""
293310 sup_disp="$(mktemp)"
293452 while IFS='|' read -r sbase sid sdate sleft; do
293521 echo "\`${sbase}\`|\`${sid}\`|${sdate}|${sleft}"
2936 done < "$SUPPRESSION_RESULTS" > "$sup_disp"
29375 emit_md_table "File|ID|Expires|Days left" "$sup_disp"
29385 rm -f "$sup_disp"
29395 echo ""
2940 fi
2941
294223 if [ "$LOCKFILE_COUNT" -gt 0 ]; then
29432 echo "## Lockfile Freshness"
29442 echo ""
29452 echo "Direct dependencies whose exact pyproject.toml pin is missing from or"
29462 echo "different in requirements-lock.txt. Regenerate the lock with"
29472 echo "pip-compile --all-extras --strip-extras -o requirements-lock.txt pyproject.toml."
29482 echo ""
29492 emit_md_table "Dependency|Expected|Locked" "$LOCKFILE_RESULTS" code
29502 echo ""
2951 fi
2952
2953 # ----- Skipped checks (collapsed) -----
295423 if [ -n "${ADDON_SKIP_REASON}${EKS_K8S_SKIP_REASON}${AURORA_SKIP_REASON}${EMR_SKIP_REASON}${BEDROCK_MODEL_SKIP_REASON}${ACCELERATOR_SKIP_REASON}${AUTOPILOT_SKIP_REASON}${CDK_ENUM_SKIP_REASON}${PYTHON_RELEASE_SKIP_REASON}${RUBY_RELEASE_SKIP_REASON}${UBUNTU_LTS_SKIP_REASON}${RUNNER_IMAGE_SKIP_REASON}" ] \
295516 || [ -s "$INCOMPLETE_REASONS_FILE" ]; then
295612 echo "<details>"
295712 echo "<summary>Skipped checks</summary>"
295812 echo ""
295915 [ -n "$ADDON_SKIP_REASON" ] && echo "- **EKS Add-ons:** $ADDON_SKIP_REASON"
296016 [ -n "$EKS_K8S_SKIP_REASON" ] && echo "- **EKS Kubernetes Version:** $EKS_K8S_SKIP_REASON"
296115 [ -n "$AURORA_SKIP_REASON" ] && echo "- **Aurora PostgreSQL Engine:** $AURORA_SKIP_REASON"
296216 [ -n "$EMR_SKIP_REASON" ] && echo "- **EMR Serverless:** $EMR_SKIP_REASON"
296316 [ -n "$BEDROCK_MODEL_SKIP_REASON" ] && echo "- **Bedrock Default Model:** $BEDROCK_MODEL_SKIP_REASON"
296413 [ -n "$ACCELERATOR_SKIP_REASON" ] && echo "- **Online Accelerator Catalog:** $ACCELERATOR_SKIP_REASON"
296515 [ -n "$AUTOPILOT_SKIP_REASON" ] && echo "- **GCO Autopilot Pins:** $AUTOPILOT_SKIP_REASON"
296613 [ -n "$CDK_ENUM_SKIP_REASON" ] && echo "- **CDK Enum Constants:** $CDK_ENUM_SKIP_REASON"
296714 [ -n "$PYTHON_RELEASE_SKIP_REASON" ] && echo "- **Python Release:** $PYTHON_RELEASE_SKIP_REASON"
296814 [ -n "$RUBY_RELEASE_SKIP_REASON" ] && echo "- **Ruby Release:** $RUBY_RELEASE_SKIP_REASON"
296915 [ -n "$UBUNTU_LTS_SKIP_REASON" ] && echo "- **Ubuntu LTS Base:** $UBUNTU_LTS_SKIP_REASON"
297013 [ -n "$RUNNER_IMAGE_SKIP_REASON" ] && echo "- **Runner Images:** $RUNNER_IMAGE_SKIP_REASON"
297112 if [ -s "$INCOMPLETE_REASONS_FILE" ]; then
2972258 while IFS= read -r incomplete_reason; do
2973119 echo "- **Incomplete lookup or parse:** ${incomplete_reason}"
2974 done < <(sort -u "$INCOMPLETE_REASONS_FILE")
2975 fi
297612 echo ""
297712 echo "</details>"
297812 echo ""
2979 fi
2980
298123 echo "## Action Required"
298223 echo ""
298323 echo "1. Review changelogs for breaking changes (see the per-row **Ref** links)"
298423 echo "2. Follow accelerator findings exactly; review lifecycle/generation metadata"
298523 echo " before running \`python scripts/accelerator_catalog.py refresh\`"
298623 echo "3. Update versions in \`pyproject.toml\`, manifests, \`charts.yaml\`, or the"
298723 echo " pinned \`*_VERSION\` env / ARG values"
298823 echo "4. Regenerate \`requirements-lock.txt\` if Python deps changed"
298923 echo "5. Reconcile any **Version Consistency** rows so every copy of a pin agrees"
299023 echo "6. Run tests locally to verify compatibility, then open a PR"
299123 echo ""
299223 echo "The findings below are the same report as JSON, for a script or an agent"
299323 echo "session that acts on them: \`gco deps scan -o json\` returns the document"
299423 echo "under \`findings\`, and every run uploads it as the"
299523 echo "\`dependency-scan-findings\` artifact."
299623 echo ""
299723 emit_findings_embed "$FINDINGS_FILE"
299823 echo ""
299923 echo "---"
300023 echo "_Automatically created by the \`deps-scan\` workflow._"
3001} > "$REPORT_FILE"
3002
300323rm -f "$NPM_RESULTS" "$DOCKER_RESULTS" "$HELM_RESULTS" "$ADDON_RESULTS" "$EKS_K8S_RESULTS" "$AURORA_RESULTS" "$EMR_RESULTS" "$DOCKERFILE_RESULTS" "$AUTOPILOT_RESULTS" "$PRECOMMIT_RESULTS" "$CDK_ENUM_RESULTS" "$PYTHON_RELEASE_RESULTS" "$RUBY_RELEASE_RESULTS" "$UBUNTU_LTS_RESULTS" "$RUNNER_IMAGE_RESULTS" "$RUNNER_IMAGE_NOTES" "$BEDROCK_MODEL_RESULTS" "$CI_TOOLING_RESULTS" "$CONSISTENCY_RESULTS" "$EPOCH_RESULTS" "$SUPPRESSION_RESULTS" "$LOCKFILE_RESULTS" "$ACCELERATOR_OFFLINE_REPORT" "$ACCELERATOR_ONLINE_REPORT" "$ACCELERATOR_ONLINE_SUMMARY" "$ACCELERATOR_OFFLINE_ERROR" "$ACCELERATOR_ONLINE_ERROR" "$INCOMPLETE_REASONS_FILE"
3004
300523if [ -n "${GITHUB_OUTPUT:-}" ]; then
3006 {
300723 echo "has_drift=true"
300823 echo "scan_complete=$SCAN_COMPLETE"
300923 echo "report_path=$REPORT_FILE"
301023 echo "findings_path=$FINDINGS_FILE"
3011 } >> "$GITHUB_OUTPUT"
3012fi
3013
3014# Mirror the report into the workflow run's job summary so results are visible
3015# on the Actions run page even for workflow_dispatch runs and regardless of
3016# whether an issue is opened.
301723if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
301823 cat "$REPORT_FILE" >> "$GITHUB_STEP_SUMMARY"
3019fi
3020
302123echo ""
302223echo "Wrote report to $REPORT_FILE"
302323echo "Wrote findings to $FINDINGS_FILE"