Skip to content

Commit c40f2d1

Browse files
authored
Merge pull request #338 from xenoscopic/update-pins
ci: add update-pins tool and review workflows
2 parents 528199f + 0d7746c commit c40f2d1

25 files changed

+3216
-0
lines changed

.github/workflows/security-review-changes.yaml

Lines changed: 446 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
name: Security Review (Manual)
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
servers:
7+
description: "Comma-separated list of local server names to audit (leave blank for all)."
8+
required: false
9+
default: ""
10+
agent:
11+
description: "Optional reviewer agent (claude or codex)."
12+
required: false
13+
default: ""
14+
model:
15+
description: "Optional reviewer model override."
16+
required: false
17+
default: ""
18+
timeout_secs:
19+
description: "Optional reviewer timeout in seconds (defaults to 1800)."
20+
required: false
21+
default: ""
22+
23+
concurrency:
24+
group: security-review-manual-${{ github.run_id }}
25+
cancel-in-progress: false
26+
27+
jobs:
28+
full-audit:
29+
name: Execute Full Audit
30+
runs-on: ubuntu-24.04
31+
permissions:
32+
contents: read
33+
34+
steps:
35+
- name: Checkout repository
36+
uses: actions/checkout@v4
37+
with:
38+
fetch-depth: 0
39+
40+
- name: Install Go
41+
uses: actions/setup-go@v5
42+
with:
43+
go-version-file: go.mod
44+
45+
- name: Install Task
46+
uses: arduino/setup-task@v2
47+
with:
48+
version: 3.x
49+
repo-token: ${{ secrets.GITHUB_TOKEN }}
50+
51+
- name: Collect audit targets
52+
id: collect
53+
run: |
54+
set -euo pipefail
55+
task ci -- collect-full-audit \
56+
--workspace "${{ github.workspace }}" \
57+
--servers "${{ github.event.inputs.servers }}" \
58+
--output-json audit-targets.json
59+
60+
if jq -e '. | length > 0' audit-targets.json >/dev/null; then
61+
echo "has_targets=true" >> "$GITHUB_OUTPUT"
62+
echo "targets=audit-targets.json" >> "$GITHUB_OUTPUT"
63+
else
64+
echo "No audit targets identified; exiting." >&2
65+
echo "has_targets=false" >> "$GITHUB_OUTPUT"
66+
fi
67+
68+
- name: Run security reviews
69+
if: steps.collect.outputs.has_targets == 'true'
70+
env:
71+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
72+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
73+
REVIEW_AGENT_INPUT: ${{ github.event.inputs.agent }}
74+
REVIEW_MODEL_INPUT: ${{ github.event.inputs.model }}
75+
REVIEW_TIMEOUT_INPUT: ${{ github.event.inputs.timeout_secs }}
76+
run: |
77+
set -uo pipefail
78+
agent="${REVIEW_AGENT_INPUT:-}"
79+
if [ -z "$agent" ]; then
80+
agent="claude"
81+
fi
82+
83+
model="${REVIEW_MODEL_INPUT:-}"
84+
85+
timeout_secs="${REVIEW_TIMEOUT_INPUT:-1800}"
86+
87+
mkdir -p reports
88+
89+
while read -r target; do
90+
server=$(echo "$target" | jq -r '.server')
91+
project=$(echo "$target" | jq -r '.project')
92+
head_commit=$(echo "$target" | jq -r '.commit')
93+
94+
if [ -z "$project" ] || [ "$project" = "null" ]; then
95+
echo "Skipping $server: missing project URL." >&2
96+
continue
97+
fi
98+
if [ -z "$head_commit" ] || [ "$head_commit" = "null" ]; then
99+
echo "Skipping $server: missing commit information." >&2
100+
continue
101+
fi
102+
103+
report_path="reports/${server}.md"
104+
labels_path="reports/${server}-labels.txt"
105+
cmd=(task security-reviewer -- \
106+
--agent "$agent" \
107+
--repo "$project" \
108+
--head "$head_commit" \
109+
--target-label "$server" \
110+
--output "$report_path" \
111+
--labels-output "$labels_path" \
112+
--timeout "$timeout_secs")
113+
114+
if [ -n "$model" ]; then
115+
cmd+=(--model "$model")
116+
fi
117+
118+
if ! "${cmd[@]}"; then
119+
echo "Security review failed for $server" >&2
120+
fi
121+
done < <(jq -c '.[]' "${{ steps.collect.outputs.targets }}")
122+
123+
- name: Upload security reports
124+
if: steps.collect.outputs.has_targets == 'true'
125+
uses: actions/upload-artifact@v4
126+
with:
127+
name: security-reports
128+
path: reports/
129+
if-no-files-found: warn

.github/workflows/update-pins.yaml

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
name: Update MCP Server Version Pins
2+
3+
on:
4+
# schedule:
5+
# - cron: "0 5 * * *"
6+
workflow_dispatch:
7+
inputs:
8+
max_new_prs:
9+
description: "Maximum number of new pull requests to create (leave blank for unlimited)."
10+
required: false
11+
default: ""
12+
servers:
13+
description: "Comma-separated list of servers to update (leave blank for all)."
14+
required: false
15+
default: ""
16+
17+
concurrency:
18+
group: update-pins
19+
cancel-in-progress: false
20+
21+
permissions:
22+
contents: write
23+
pull-requests: write
24+
25+
jobs:
26+
update-pins:
27+
runs-on: ubuntu-24.04
28+
steps:
29+
- name: Checkout repository
30+
uses: actions/checkout@v4
31+
with:
32+
fetch-depth: 0
33+
34+
- name: Configure Git user
35+
run: |
36+
git config user.name "docker-mcp-bot"
37+
git config user.email "[email protected]"
38+
39+
- name: Install Go
40+
uses: actions/setup-go@v5
41+
with:
42+
go-version-file: go.mod
43+
44+
- name: Install Task
45+
uses: arduino/setup-task@v2
46+
with:
47+
version: 3.x
48+
repo-token: ${{ secrets.GITHUB_TOKEN }}
49+
50+
- name: Update pinned commits
51+
env:
52+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53+
run: |
54+
task ci -- update-pins
55+
56+
- name: Collect per-server patches
57+
id: prepare
58+
env:
59+
ALLOWED_SERVERS: ${{ github.event.inputs.servers || '' }}
60+
run: |
61+
# Gather the diff for each modified server YAML and store it as an
62+
# individual patch file so we can open one PR per server.
63+
mkdir -p patches
64+
changed_files=$(git status --porcelain | awk '$2 ~ /^servers\/.*\/server.yaml$/ {print $2}')
65+
if [ -z "$changed_files" ]; then
66+
echo "changed=false" >> "$GITHUB_OUTPUT"
67+
exit 0
68+
fi
69+
70+
allowed_servers=$(echo "$ALLOWED_SERVERS" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
71+
server_list=()
72+
for file in $changed_files; do
73+
server=$(basename "$(dirname "$file")")
74+
server_lc=$(echo "$server" | tr '[:upper:]' '[:lower:]')
75+
76+
if [ -n "$allowed_servers" ]; then
77+
if ! echo ",$allowed_servers," | grep -q ",$server_lc,"; then
78+
continue
79+
fi
80+
fi
81+
82+
git diff -- "$file" > "patches/${server}.patch"
83+
server_list+=("$server")
84+
done
85+
86+
if [ ${#server_list[@]} -eq 0 ]; then
87+
echo "No servers matched the provided filter; exiting." >&2
88+
git checkout -- servers
89+
echo "changed=false" >> "$GITHUB_OUTPUT"
90+
exit 0
91+
fi
92+
93+
# Reset the working tree so we can apply patches one-at-a-time.
94+
git checkout -- servers
95+
96+
# Expose the server list to later steps.
97+
printf '%s\n' "${server_list[@]}" | paste -sd',' - > patches/servers.txt
98+
echo "changed=true" >> "$GITHUB_OUTPUT"
99+
echo "servers=$(cat patches/servers.txt)" >> "$GITHUB_OUTPUT"
100+
101+
- name: Create or update pull requests
102+
if: steps.prepare.outputs.changed == 'true'
103+
env:
104+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
105+
MAX_NEW_PRS: ${{ github.event.inputs.max_new_prs || '' }}
106+
run: |
107+
IFS=',' read -ra SERVERS <<< "${{ steps.prepare.outputs.servers }}"
108+
new_pr_limit=$(echo "$MAX_NEW_PRS" | tr -d ' ')
109+
if [ -n "$new_pr_limit" ] && ! [[ "$new_pr_limit" =~ ^[0-9]+$ ]]; then
110+
echo "Invalid max_new_prs value: $new_pr_limit" >&2
111+
exit 1
112+
fi
113+
new_pr_count=0
114+
115+
for server in "${SERVERS[@]}"; do
116+
patch="patches/${server}.patch"
117+
if [ ! -s "$patch" ]; then
118+
echo "No patch found for $server, skipping."
119+
continue
120+
fi
121+
122+
# Look up the new commit hash in the patch so we can decide whether
123+
# an existing automation branch already covers it.
124+
new_commit=$(awk '/^\+.*commit:/{print $2}' "$patch" | tail -n1)
125+
branch="automation/update-pin-${server}"
126+
127+
# Start from a clean copy of main for each server so branches do not
128+
# interfere with one another.
129+
git checkout main
130+
git fetch origin main
131+
git reset --hard origin/main
132+
133+
# Check if we've hit the new PR limit before doing any work.
134+
branch_exists=false
135+
if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
136+
branch_exists=true
137+
git fetch origin "$branch"
138+
existing_commit=$(git show "origin/${branch}:servers/${server}/server.yaml" 2>/dev/null | awk '/commit:/{print $2}' | tail -n1)
139+
if [ -n "$existing_commit" ] && [ "$existing_commit" = "$new_commit" ]; then
140+
echo "Existing PR for $server already pins ${existing_commit}; skipping."
141+
continue
142+
fi
143+
fi
144+
145+
# Check PR limit for new branches only.
146+
if [ "$branch_exists" = false ] && [ -n "$new_pr_limit" ] && [ "$new_pr_count" -ge "$new_pr_limit" ]; then
147+
echo "New PR quota reached ($new_pr_limit); skipping $server."
148+
continue
149+
fi
150+
151+
# Apply the patch onto a fresh branch for this server.
152+
git checkout -B "$branch" origin/main
153+
if ! git apply "$patch"; then
154+
echo "Failed to apply patch for $server, skipping."
155+
continue
156+
fi
157+
158+
if git diff --quiet; then
159+
echo "No changes after applying patch for $server, skipping."
160+
continue
161+
fi
162+
163+
# Commit the server YAML change and force-push the automation branch.
164+
git add "servers/${server}/server.yaml"
165+
git commit -m "chore: update pin for ${server}"
166+
if ! git push --force origin "$branch"; then
167+
echo "Failed to push branch for $server, skipping." >&2
168+
continue
169+
fi
170+
171+
# Create or update the PR dedicated to this server.
172+
if gh pr view --head "$branch" >/dev/null 2>&1; then
173+
if ! gh pr edit "$branch" \
174+
--title "chore: update pin for ${server}" \
175+
--body "Automated commit pin update for ${server}." 2>&1; then
176+
echo "Failed to update PR for $server" >&2
177+
fi
178+
else
179+
if gh pr create \
180+
--title "chore: update pin for ${server}" \
181+
--body "Automated commit pin update for ${server}." \
182+
--base main \
183+
--head "$branch" 2>&1; then
184+
new_pr_count=$((new_pr_count + 1))
185+
else
186+
echo "Failed to create PR for $server" >&2
187+
fi
188+
fi
189+
done
190+
191+
# Leave the repository in a clean state.
192+
git checkout main

Taskfile.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ tasks:
2929
desc: Clean build artifacts for servers
3030
cmd: go run ./cmd/clean {{.CLI_ARGS}}
3131

32+
ci:
33+
desc: Run CI helper utilities
34+
cmd: go run ./cmd/ci {{.CLI_ARGS}}
35+
3236
import:
3337
desc: Import a server into the registry
3438
cmd: docker mcp catalog import ./catalogs/{{.CLI_ARGS}}/catalog.yaml
@@ -42,3 +46,7 @@ tasks:
4246
unittest:
4347
desc: Run Go unit tests
4448
cmd: go test ./...
49+
50+
security-reviewer:
51+
desc: Run the security reviewer agent
52+
cmd: go run ./cmd/security-reviewer {{.CLI_ARGS}}

0 commit comments

Comments
 (0)