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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions .github/aw/actions-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,6 @@
"repo": "actions/setup-python",
"version": "v7.0.0",
"sha": "5fda3b95a4ea91299a34e894583c3862153e4b97"
},
"github/gh-aw-actions/setup-cli@v0.87.2": {
"repo": "github/gh-aw-actions/setup-cli",
"version": "v0.87.2",
"sha": "b304200a0ef4b3998673bfc7945acb08ab8c88b7"
},
"github/gh-aw-actions/setup@v0.87.2": {
"repo": "github/gh-aw-actions/setup",
"version": "v0.87.2",
"sha": "b304200a0ef4b3998673bfc7945acb08ab8c88b7"
}
},
"containers": {
Expand Down
161 changes: 161 additions & 0 deletions .github/graders/dependabot-release-train-updater-operational-value.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#!/usr/bin/env bash

set -euo pipefail
export LC_ALL=C

REPOSITORY=githubnext/central-agentic-ops
WORKFLOW_NAME="Dependabot / Release Train Updater"
MATURATION_SECONDS=1209600

tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/dependabot-release-train-value.XXXXXX")
trap 'rm -rf "$tmp_dir"' EXIT HUP INT TERM

definition() {
cat <<'JSON'
{
"schemaVersion": 4, "grader": "operational-value",
"repository": "githubnext/central-agentic-ops", "workflowName": "Dependabot / Release Train Updater",
"sourcePath": ".github/workflows/dependabot-release-train-updater.md",
"adoption": {"commit": "35c7c3cbd319632f85784cce196e57c0f61db9a0", "adoptedAt": "2026-08-18T17:54:55Z"},
"operationalValue": "Resolve the dispatched target's matured dependency-update opportunities with validated merges.",
"evidence": {
"opportunity": "Dependency pull requests open in the dispatched target during the 30 days before the run.",
"assignment": "Bind targetRepo from workflow_dispatch inputs and freeze eligible pull-request numbers at the run creation time; key dependency-set:<targetRepo>:<runId>.",
"accepted": "An assigned pull request is merged by the evidence cutoff, changes a dependency manifest or lockfile, and satisfies every configured required status check.",
"repositories": ["githubnext/central-agentic-ops"],
"collection": "Query target pull requests and changed files once for assignment, then query immutable merge commits and required-check results through the capped cutoff.",
"maturation": "Fourteen days after the workflow run starts.",
"zeroRule": "Complete evidence for eligible assigned pull requests with no validated resolutions scores 0.",
"missingRule": "Missing target assignment, no eligible opportunity, inaccessible pull-request evidence, or unavailable required-check configuration scores null."
},
"primaryMetric": {"id": "validated-resolution-share", "formula": "validated assigned dependency pull requests / eligible assigned dependency pull requests", "direction": "higher_is_better"},
"baseline": {"mode": "attainment-only", "value": null, "evidenceCutoff": null, "provenance": []},
"validationExamples": {
"targetAttained": {"valid": true, "eligible": 4, "validated": 4},
"targetMissed": {"valid": true, "eligible": 4, "validated": 0},
"missing": {"valid": false, "eligible": null, "validated": null},
"malformed": {"valid": true, "eligible": 1, "validated": 2}
}
}
JSON
}

metric() {
jq 'if .valid != true or (.eligible|type)!="number" or (.validated|type)!="number"
or .eligible<=0 or .validated<0 or .validated>.eligible then null
else ((.validated/.eligible)*1000000|round)/1000000 end'
}

normalize_timestamp() {
jq -nr --arg value "$1" '($value|sub("\\.[0-9]+Z$";"Z")) as $v
| if ($v|test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"))
and (try (($v|fromdateiso8601|todateiso8601)==$v) catch false) then $v else error("invalid") end' 2>/dev/null
}

time_shift() { jq -nr --arg value "$1" --argjson seconds "$2" '$value|fromdateiso8601+$seconds|todateiso8601'; }
earlier() { jq -nr --arg left "$1" --arg right "$2" 'if ($left|fromdateiso8601)<($right|fromdateiso8601) then $left else $right end'; }

emit_missing() {
jq -cn --arg key "$1" --argjson case "$2" --arg cutoff "$3" --arg matures "$4" --arg reason "$5" \
'{value:null,opportunityKey:$key,case:$case,evidenceCutoff:$cutoff,maturesAt:$matures,provenance:[],diagnostics:{missingReason:$reason}}'
}

dependency_file_regex='(^|/)(package(-lock)?\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|deno\.lock|requirements[^/]*\.txt|constraints[^/]*\.txt|Pipfile(\.lock)?|poetry\.lock|pyproject\.toml|uv\.lock|setup\.(py|cfg)|Gemfile(\.lock)?|go\.(mod|sum|work)|Cargo\.(toml|lock)|composer\.(json|lock)|mix\.(exs|lock)|pubspec\.(yaml|lock)|Package\.swift|Podfile(\.lock)?|packages?\.lock\.json|Directory\.Packages\.props|pom\.xml|build\.gradle(\.kts)?|gradle\.lockfile|MODULE\.bazel(\.lock)?|WORKSPACE|flake\.(nix|lock)|dependabot\.ya?ml)$'

required_checks() {
target_repo=$1; branch=$2; output=$3
if gh api "repos/$target_repo/branches/$branch/protection/required_status_checks" >"$tmp_dir/required-raw.json" 2>/dev/null; then
jq '[
(.checks[]? | {context:.context,appId:(.app_id // null)}),
(.contexts[]? | {context:.,appId:null})
] | unique_by([.context,.appId])' "$tmp_dir/required-raw.json" >"$output"
return
fi
protected=$(gh api "repos/$target_repo/branches/$branch" --jq .protected 2>/dev/null) || return 1
[[ $protected == false ]] || return 1
printf '%s\n' '[]' >"$output"
}

assign_case() {
request_file=$1
target_repo=$(jq -r '.event.inputs.target_repo // empty' "$request_file")
[[ $target_repo =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || return 1
created_at=$(jq -r .run.createdAt "$request_file")
window_start=$(time_shift "$created_at" -2592000)
gh api --paginate --method GET "repos/$target_repo/pulls" -f state=all -f sort=created -f direction=desc -f per_page=100 \
| jq -s 'add // []' >"$tmp_dir/pulls.json" 2>/dev/null || return 1
: >"$tmp_dir/opportunities.ndjson"
while IFS=$'\t' read -r number author created closed branch; do
[[ $created < $window_start || $created > $created_at ]] && continue
[[ -n $closed && $closed < $created_at ]] && continue
files="$tmp_dir/files-$number.json"
gh api --paginate "repos/$target_repo/pulls/$number/files?per_page=100" --jq '.[].filename' \
| jq -Rsc 'split("\n")|map(select(length>0))' >"$files" 2>/dev/null || return 1
is_dependency=$(jq --arg author "$author" --arg regex "$dependency_file_regex" \
'($author|ascii_downcase|startswith("dependabot")) or any(.[];test($regex;"i"))' "$files")
[[ $is_dependency == true ]] || continue
required_file="$tmp_dir/required-$number.json"
required_checks "$target_repo" "$branch" "$required_file" || return 1
jq -cn --argjson number "$number" --arg createdAt "$created" --arg baseRef "$branch" \
--slurpfile required "$required_file" \
'{number:$number,createdAt:$createdAt,baseRef:$baseRef,requiredChecks:$required[0]}' >>"$tmp_dir/opportunities.ndjson"
done < <(jq -r '.[]|[.number,(.user.login//""),.created_at,(.closed_at//""),.base.ref]|@tsv' "$tmp_dir/pulls.json")
opportunities=$(jq -s '.' "$tmp_dir/opportunities.ndjson")
jq -cn --arg targetRepo "$target_repo" --arg runId "$(jq -r .run.id "$request_file")" \
--arg assignedAt "$created_at" --argjson opportunities "$opportunities" \
'{targetRepo:$targetRepo,runId:$runId,assignedAt:$assignedAt,opportunities:$opportunities}'
}

validated_pull() {
target_repo=$1; number=$2; cutoff=$3; required=$4
gh api "repos/$target_repo/pulls/$number" >"$tmp_dir/pull-$number.json" 2>/dev/null || return 2
merged_at=$(jq -r '.merged_at // empty' "$tmp_dir/pull-$number.json")
merge_sha=$(jq -r '.merge_commit_sha // empty' "$tmp_dir/pull-$number.json")
[[ -n $merged_at && -n $merge_sha ]] || return 1
[[ $merged_at > $cutoff ]] && return 1
[[ $(printf '%s\n' "$required" | jq -r 'type') == array ]] || return 2
[[ $(printf '%s\n' "$required" | jq length) -gt 0 ]] || return 0
gh api --paginate "repos/$target_repo/commits/$merge_sha/check-runs?per_page=100" --jq '.check_runs[]' | jq -s '.' >"$tmp_dir/checks-$number.json" 2>/dev/null || return 2
gh api "repos/$target_repo/commits/$merge_sha/status" >"$tmp_dir/status-$number.json" 2>/dev/null || return 2
jq -en --arg cutoff "$cutoff" --argjson required "$required" --slurpfile checks "$tmp_dir/checks-$number.json" --slurpfile statuses "$tmp_dir/status-$number.json" '
[$checks[0][]|select(.conclusion=="success" and .completed_at <= $cutoff)|{context:.name,appId:(.app.id//null)}] as $checks
| [$statuses[0].statuses[]?|select(.state=="success" and .created_at <= $cutoff)|{context:.context,appId:null}] as $statuses
| all($required[] as $wanted; any(($checks+$statuses)[];
.context==$wanted.context and ($wanted.appId==null or .appId==$wanted.appId)))' >/dev/null
}

grade_run() {
request_file="$tmp_dir/request.json"; cat >"$request_file"
if ! jq -e '.schemaVersion==1 and (.run.id|type)=="string" and (.run.createdAt|type)=="string" and (.evidenceAt|type)=="string"' "$request_file" >/dev/null 2>&1; then
printf '%s\n' '{"value":null,"opportunityKey":"invalid-request","case":{"invalidRequest":true},"evidenceCutoff":"1970-01-01T00:00:00Z","maturesAt":"1970-01-01T00:00:00Z","provenance":[],"diagnostics":{"missingReason":"invalid request"}}'; return
fi
created_at=$(normalize_timestamp "$(jq -r .run.createdAt "$request_file")") || created_at=1970-01-01T00:00:00Z
evidence_at=$(normalize_timestamp "$(jq -r .evidenceAt "$request_file")") || evidence_at=$created_at
matures_at=$(time_shift "$created_at" "$MATURATION_SECONDS"); cutoff=$(earlier "$evidence_at" "$matures_at")
case_json=$(jq -c '.case' "$request_file")
if [[ $case_json == null ]]; then case_json=$(assign_case "$request_file") || case_json='{"assignmentMissing":true}'; fi
key="run:$(jq -r .run.id "$request_file")"
if [[ $(printf '%s\n' "$case_json"|jq -r '.assignmentMissing//false') == true ]]; then emit_missing "$key" "$case_json" "$cutoff" "$matures_at" assignment-unavailable; return; fi
target_repo=$(printf '%s\n' "$case_json"|jq -r .targetRepo); key="dependency-set:${target_repo}:$(printf '%s\n' "$case_json"|jq -r .runId)"
eligible=$(printf '%s\n' "$case_json"|jq '.opportunities|length')
[[ $eligible -gt 0 ]] || { emit_missing "$key" "$case_json" "$cutoff" "$matures_at" no-eligible-opportunities; return; }
validated=0; unavailable=false; : >"$tmp_dir/provenance.ndjson"
while IFS=$'\t' read -r number required; do
result=0; validated_pull "$target_repo" "$number" "$cutoff" "$required" || result=$?
[[ $result -eq 0 ]] && validated=$((validated+1))
[[ $result -eq 2 ]] && unavailable=true
printf '{"repository":"%s","kind":"pull-request","ref":"%s"}\n' "$target_repo" "$number" >>"$tmp_dir/provenance.ndjson"
done < <(printf '%s\n' "$case_json"|jq -r '.opportunities[]|[.number,(.requiredChecks|tojson)]|@tsv')
[[ $unavailable == false ]] || { emit_missing "$key" "$case_json" "$cutoff" "$matures_at" required-check-evidence-unavailable; return; }
evidence=$(jq -cn --argjson eligible "$eligible" --argjson validated "$validated" '{valid:true,eligible:$eligible,validated:$validated}')
value=$(printf '%s\n' "$evidence"|metric); provenance=$(jq -s '.' "$tmp_dir/provenance.ndjson")
jq -cn --argjson value "$value" --arg key "$key" --argjson case "$case_json" --arg cutoff "$cutoff" --arg matures "$matures_at" --argjson provenance "$provenance" \
'{value:$value,opportunityKey:$key,case:$case,evidenceCutoff:$cutoff,maturesAt:$matures,provenance:$provenance,diagnostics:{eligible:($case.opportunities|length),validated:($value*($case.opportunities|length))}}'
}

case ${1:-} in
--definition) [[ $# -eq 1 ]] || exit 2; definition ;;
--metric) [[ $# -eq 1 ]] || exit 2; metric ;;
--grade-run) [[ $# -eq 1 ]] || exit 2; grade_run ;;
*) printf 'usage: %s --definition|--metric|--grade-run\n' "$0" >&2; exit 2 ;;
esac
Loading
Loading