chore: import upstream snapshot with attribution
This commit is contained in:
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# Build the MCP App UIs
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../ui"
|
||||
|
||||
# Install dependencies if needed
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "Installing UI dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
echo "Building UI..."
|
||||
npm run build
|
||||
|
||||
echo "UI build complete. Output:"
|
||||
ls -la ../pkg/github/ui_dist/*.html
|
||||
Executable
+348
@@ -0,0 +1,348 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Conformance test script for comparing MCP server behavior between branches
|
||||
# Builds both main and current branch, runs various flag combinations,
|
||||
# and produces a conformance report with timing and diffs.
|
||||
#
|
||||
# Output:
|
||||
# - Progress/status messages go to stderr (for visibility in CI)
|
||||
# - Final report summary goes to stdout (for piping/capture)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
REPORT_DIR="$PROJECT_DIR/conformance-report"
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# Colors for output (only used on stderr)
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper to print to stderr
|
||||
log() {
|
||||
echo -e "$@" >&2
|
||||
}
|
||||
|
||||
log "${BLUE}=== MCP Server Conformance Test ===${NC}"
|
||||
log "Current branch: $CURRENT_BRANCH"
|
||||
log "Report directory: $REPORT_DIR"
|
||||
|
||||
# Find the common ancestor
|
||||
MERGE_BASE=$(git merge-base HEAD origin/main)
|
||||
log "Comparing against merge-base: $MERGE_BASE"
|
||||
log ""
|
||||
|
||||
# Create report directory
|
||||
rm -rf "$REPORT_DIR"
|
||||
mkdir -p "$REPORT_DIR"/{main,branch,diffs}
|
||||
|
||||
# Build binaries
|
||||
log "${YELLOW}Building binaries...${NC}"
|
||||
|
||||
log "Building current branch ($CURRENT_BRANCH)..."
|
||||
go build -o "$REPORT_DIR/branch/github-mcp-server" ./cmd/github-mcp-server
|
||||
BRANCH_BUILD_OK=$?
|
||||
|
||||
log "Building main branch (using temp worktree at merge-base)..."
|
||||
TEMP_WORKTREE=$(mktemp -d)
|
||||
git worktree add --quiet "$TEMP_WORKTREE" "$MERGE_BASE"
|
||||
(cd "$TEMP_WORKTREE" && go build -o "$REPORT_DIR/main/github-mcp-server" ./cmd/github-mcp-server)
|
||||
MAIN_BUILD_OK=$?
|
||||
git worktree remove --force "$TEMP_WORKTREE"
|
||||
|
||||
if [ $BRANCH_BUILD_OK -ne 0 ] || [ $MAIN_BUILD_OK -ne 0 ]; then
|
||||
log "${RED}Build failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "${GREEN}Both binaries built successfully${NC}"
|
||||
log ""
|
||||
|
||||
# MCP JSON-RPC messages
|
||||
INIT_MSG='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"conformance-test","version":"1.0.0"}}}'
|
||||
INITIALIZED_MSG='{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'
|
||||
LIST_TOOLS_MSG='{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
|
||||
LIST_RESOURCES_MSG='{"jsonrpc":"2.0","id":3,"method":"resources/listTemplates","params":{}}'
|
||||
LIST_PROMPTS_MSG='{"jsonrpc":"2.0","id":4,"method":"prompts/list","params":{}}'
|
||||
|
||||
# Function to normalize JSON for comparison
|
||||
# Sorts all arrays (including nested ones) and formats consistently
|
||||
# Also handles embedded JSON strings in "text" fields (from tool call responses)
|
||||
normalize_json() {
|
||||
local file="$1"
|
||||
if [ -s "$file" ]; then
|
||||
# First, try to parse and re-serialize any JSON embedded in text fields
|
||||
# This handles tool call responses where the result is JSON-in-a-string
|
||||
jq -S '
|
||||
# Function to sort arrays recursively
|
||||
def deep_sort:
|
||||
if type == "array" then
|
||||
[.[] | deep_sort] | sort_by(tostring)
|
||||
elif type == "object" then
|
||||
to_entries | map(.value |= deep_sort) | from_entries
|
||||
else
|
||||
.
|
||||
end;
|
||||
|
||||
# Walk the structure, and for any "text" field that looks like JSON array/object, parse and sort it
|
||||
walk(
|
||||
if type == "object" and .text and (.text | type == "string") and ((.text | startswith("[")) or (.text | startswith("{"))) then
|
||||
.text = ((.text | fromjson | deep_sort) | tojson)
|
||||
else
|
||||
.
|
||||
end
|
||||
) | deep_sort
|
||||
' "$file" 2>/dev/null > "${file}.tmp" && mv "${file}.tmp" "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to run MCP server and capture output with timing
|
||||
run_mcp_test() {
|
||||
local binary="$1"
|
||||
local name="$2"
|
||||
local flags="$3"
|
||||
local output_prefix="$4"
|
||||
|
||||
local start_time end_time duration
|
||||
start_time=$(date +%s.%N)
|
||||
|
||||
# Run the server with all list commands - each response is on its own line
|
||||
output=$(
|
||||
(
|
||||
echo "$INIT_MSG"
|
||||
echo "$INITIALIZED_MSG"
|
||||
echo "$LIST_TOOLS_MSG"
|
||||
echo "$LIST_RESOURCES_MSG"
|
||||
echo "$LIST_PROMPTS_MSG"
|
||||
sleep 0.5
|
||||
) | GITHUB_PERSONAL_ACCESS_TOKEN=1 $binary stdio $flags 2>/dev/null
|
||||
)
|
||||
|
||||
end_time=$(date +%s.%N)
|
||||
duration=$(echo "$end_time - $start_time" | bc)
|
||||
|
||||
# Parse and save each response by matching JSON-RPC id
|
||||
# Each line is a separate JSON response
|
||||
echo "$output" | while IFS= read -r line; do
|
||||
id=$(echo "$line" | jq -r '.id // empty' 2>/dev/null)
|
||||
case "$id" in
|
||||
1) echo "$line" | jq -S '.' > "${output_prefix}_initialize.json" 2>/dev/null ;;
|
||||
2) echo "$line" | jq -S '.' > "${output_prefix}_tools.json" 2>/dev/null ;;
|
||||
3) echo "$line" | jq -S '.' > "${output_prefix}_resources.json" 2>/dev/null ;;
|
||||
4) echo "$line" | jq -S '.' > "${output_prefix}_prompts.json" 2>/dev/null ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Create empty files if not created (in case of errors or missing responses)
|
||||
touch "${output_prefix}_initialize.json" "${output_prefix}_tools.json" \
|
||||
"${output_prefix}_resources.json" "${output_prefix}_prompts.json"
|
||||
|
||||
# Normalize all JSON files for consistent comparison (sorts arrays, keys)
|
||||
for endpoint in initialize tools resources prompts; do
|
||||
normalize_json "${output_prefix}_${endpoint}.json"
|
||||
done
|
||||
|
||||
echo "$duration"
|
||||
}
|
||||
|
||||
# Test configurations - array of "name|flags"
|
||||
declare -a TEST_CONFIGS=(
|
||||
"default|"
|
||||
"read-only|--read-only"
|
||||
"toolsets-repos|--toolsets=repos"
|
||||
"toolsets-issues|--toolsets=issues"
|
||||
"toolsets-pull_requests|--toolsets=pull_requests"
|
||||
"toolsets-repos,issues|--toolsets=repos,issues"
|
||||
"toolsets-all|--toolsets=all"
|
||||
"tools-get_me|--tools=get_me"
|
||||
"tools-get_me,list_issues|--tools=get_me,list_issues"
|
||||
"toolsets-repos+read-only|--toolsets=repos --read-only"
|
||||
)
|
||||
|
||||
# Summary arrays
|
||||
declare -a TEST_NAMES
|
||||
declare -a MAIN_TIMES
|
||||
declare -a BRANCH_TIMES
|
||||
declare -a DIFF_STATUS
|
||||
|
||||
log "${YELLOW}Running conformance tests...${NC}"
|
||||
log ""
|
||||
|
||||
for config in "${TEST_CONFIGS[@]}"; do
|
||||
IFS='|' read -r test_name flags <<< "$config"
|
||||
|
||||
log "${BLUE}Test: ${test_name}${NC}"
|
||||
log " Flags: ${flags:-<none>}"
|
||||
|
||||
# Create output directories
|
||||
mkdir -p "$REPORT_DIR/main/$test_name"
|
||||
mkdir -p "$REPORT_DIR/branch/$test_name"
|
||||
mkdir -p "$REPORT_DIR/diffs/$test_name"
|
||||
|
||||
# Run standard test
|
||||
main_time=$(run_mcp_test "$REPORT_DIR/main/github-mcp-server" "main" "$flags" "$REPORT_DIR/main/$test_name/output")
|
||||
log " Main: ${main_time}s"
|
||||
|
||||
branch_time=$(run_mcp_test "$REPORT_DIR/branch/github-mcp-server" "branch" "$flags" "$REPORT_DIR/branch/$test_name/output")
|
||||
log " Branch: ${branch_time}s"
|
||||
|
||||
endpoints="initialize tools resources prompts"
|
||||
|
||||
# Calculate time difference
|
||||
time_diff=$(echo "$branch_time - $main_time" | bc)
|
||||
if (( $(echo "$time_diff > 0" | bc -l) )); then
|
||||
log " Δ Time: ${RED}+${time_diff}s (slower)${NC}"
|
||||
else
|
||||
log " Δ Time: ${GREEN}${time_diff}s (faster)${NC}"
|
||||
fi
|
||||
|
||||
# Generate diffs for each endpoint
|
||||
has_diff=false
|
||||
for endpoint in $endpoints; do
|
||||
main_file="$REPORT_DIR/main/$test_name/output_${endpoint}.json"
|
||||
branch_file="$REPORT_DIR/branch/$test_name/output_${endpoint}.json"
|
||||
diff_file="$REPORT_DIR/diffs/$test_name/${endpoint}.diff"
|
||||
|
||||
if ! diff -u "$main_file" "$branch_file" > "$diff_file" 2>/dev/null; then
|
||||
has_diff=true
|
||||
lines=$(wc -l < "$diff_file" | tr -d ' ')
|
||||
log " ${YELLOW}${endpoint}: DIFF (${lines} lines)${NC}"
|
||||
else
|
||||
rm -f "$diff_file" # No diff, remove empty file
|
||||
log " ${GREEN}${endpoint}: OK${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Store results
|
||||
TEST_NAMES+=("$test_name")
|
||||
MAIN_TIMES+=("$main_time")
|
||||
BRANCH_TIMES+=("$branch_time")
|
||||
if [ "$has_diff" = true ]; then
|
||||
DIFF_STATUS+=("DIFF")
|
||||
else
|
||||
DIFF_STATUS+=("OK")
|
||||
fi
|
||||
|
||||
log ""
|
||||
done
|
||||
|
||||
# Generate summary report
|
||||
REPORT_FILE="$REPORT_DIR/CONFORMANCE_REPORT.md"
|
||||
|
||||
cat > "$REPORT_FILE" << EOF
|
||||
# MCP Server Conformance Report
|
||||
|
||||
Generated: $(date)
|
||||
Current Branch: $CURRENT_BRANCH
|
||||
Compared Against: merge-base ($MERGE_BASE)
|
||||
|
||||
## Summary
|
||||
|
||||
| Test | Main Time | Branch Time | Δ Time | Status |
|
||||
|------|-----------|-------------|--------|--------|
|
||||
EOF
|
||||
|
||||
total_main=0
|
||||
total_branch=0
|
||||
diff_count=0
|
||||
ok_count=0
|
||||
|
||||
for i in "${!TEST_NAMES[@]}"; do
|
||||
name="${TEST_NAMES[$i]}"
|
||||
main_t="${MAIN_TIMES[$i]}"
|
||||
branch_t="${BRANCH_TIMES[$i]}"
|
||||
status="${DIFF_STATUS[$i]}"
|
||||
|
||||
delta=$(echo "$branch_t - $main_t" | bc)
|
||||
if (( $(echo "$delta > 0" | bc -l) )); then
|
||||
delta_str="+${delta}s"
|
||||
else
|
||||
delta_str="${delta}s"
|
||||
fi
|
||||
|
||||
if [ "$status" = "DIFF" ]; then
|
||||
status_str="⚠️ DIFF"
|
||||
((diff_count++)) || true
|
||||
else
|
||||
status_str="✅ OK"
|
||||
((ok_count++)) || true
|
||||
fi
|
||||
|
||||
total_main=$(echo "$total_main + $main_t" | bc)
|
||||
total_branch=$(echo "$total_branch + $branch_t" | bc)
|
||||
|
||||
echo "| $name | ${main_t}s | ${branch_t}s | $delta_str | $status_str |" >> "$REPORT_FILE"
|
||||
done
|
||||
|
||||
total_delta=$(echo "$total_branch - $total_main" | bc)
|
||||
if (( $(echo "$total_delta > 0" | bc -l) )); then
|
||||
total_delta_str="+${total_delta}s"
|
||||
else
|
||||
total_delta_str="${total_delta}s"
|
||||
fi
|
||||
|
||||
cat >> "$REPORT_FILE" << EOF
|
||||
| **TOTAL** | **${total_main}s** | **${total_branch}s** | **$total_delta_str** | **$ok_count OK / $diff_count DIFF** |
|
||||
|
||||
## Statistics
|
||||
|
||||
- **Tests Passed (no diff):** $ok_count
|
||||
- **Tests with Differences:** $diff_count
|
||||
- **Total Main Time:** ${total_main}s
|
||||
- **Total Branch Time:** ${total_branch}s
|
||||
- **Overall Time Delta:** $total_delta_str
|
||||
|
||||
## Detailed Diffs
|
||||
|
||||
EOF
|
||||
|
||||
# Add diff details to report
|
||||
for i in "${!TEST_NAMES[@]}"; do
|
||||
name="${TEST_NAMES[$i]}"
|
||||
status="${DIFF_STATUS[$i]}"
|
||||
|
||||
if [ "$status" = "DIFF" ]; then
|
||||
echo "### $name" >> "$REPORT_FILE"
|
||||
echo "" >> "$REPORT_FILE"
|
||||
|
||||
# Check all possible endpoints
|
||||
for endpoint in initialize tools resources prompts; do
|
||||
diff_file="$REPORT_DIR/diffs/$name/${endpoint}.diff"
|
||||
if [ -f "$diff_file" ] && [ -s "$diff_file" ]; then
|
||||
echo "#### ${endpoint}" >> "$REPORT_FILE"
|
||||
echo '```diff' >> "$REPORT_FILE"
|
||||
cat "$diff_file" >> "$REPORT_FILE"
|
||||
echo '```' >> "$REPORT_FILE"
|
||||
echo "" >> "$REPORT_FILE"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
log "${BLUE}=== Conformance Test Complete ===${NC}"
|
||||
log ""
|
||||
log "Report: ${GREEN}$REPORT_FILE${NC}"
|
||||
log ""
|
||||
|
||||
# Output summary to stdout (for CI capture)
|
||||
echo "=== Conformance Test Summary ==="
|
||||
echo "Tests passed: $ok_count"
|
||||
echo "Tests with diffs: $diff_count"
|
||||
echo "Total main time: ${total_main}s"
|
||||
echo "Total branch time: ${total_branch}s"
|
||||
echo "Time delta: $total_delta_str"
|
||||
|
||||
if [ $diff_count -gt 0 ]; then
|
||||
log ""
|
||||
log "${YELLOW}⚠️ Some tests have differences. Review the diffs in:${NC}"
|
||||
log " $REPORT_DIR/diffs/"
|
||||
echo ""
|
||||
echo "RESULT: DIFFERENCES FOUND"
|
||||
# Don't exit with error - diffs may be intentional improvements
|
||||
else
|
||||
echo ""
|
||||
echo "RESULT: ALL TESTS PASSED"
|
||||
fi
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# Fetch Octicon icons and convert them to PNG for embedding in the MCP server.
|
||||
# Generates both light theme (dark icons) and dark theme (white icons) variants.
|
||||
# Uses sed to modify SVG fill color before converting to PNG.
|
||||
# Requires: rsvg-convert (from librsvg2-bin on Ubuntu/Debian)
|
||||
#
|
||||
# Usage:
|
||||
# script/fetch-icons # Fetch all required icons
|
||||
# script/fetch-icons icon1 icon2 # Fetch specific icons
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
ICONS_DIR="$REPO_ROOT/pkg/octicons/icons"
|
||||
REQUIRED_ICONS_FILE="$REPO_ROOT/pkg/octicons/required_icons.txt"
|
||||
OCTICONS_BASE="https://raw.githubusercontent.com/primer/octicons/main/icons"
|
||||
|
||||
# Check for rsvg-convert
|
||||
if ! command -v rsvg-convert &> /dev/null; then
|
||||
echo "Error: rsvg-convert not found. Install with:"
|
||||
echo " Ubuntu/Debian: sudo apt-get install librsvg2-bin"
|
||||
echo " macOS: brew install librsvg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load icons from required_icons.txt or use command-line arguments
|
||||
if [ $# -gt 0 ]; then
|
||||
ICONS=("$@")
|
||||
else
|
||||
if [ ! -f "$REQUIRED_ICONS_FILE" ]; then
|
||||
echo "Error: Required icons file not found: $REQUIRED_ICONS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
# Read icons from file, skipping comments and empty lines
|
||||
mapfile -t ICONS < <(grep -v '^#' "$REQUIRED_ICONS_FILE" | grep -v '^$')
|
||||
fi
|
||||
|
||||
# Ensure icons directory exists
|
||||
mkdir -p "$ICONS_DIR"
|
||||
|
||||
echo "Fetching ${#ICONS[@]} icons (24px, light + dark themes)..."
|
||||
|
||||
for icon in "${ICONS[@]}"; do
|
||||
svg_url="${OCTICONS_BASE}/${icon}-24.svg"
|
||||
light_file="${ICONS_DIR}/${icon}-light.png"
|
||||
dark_file="${ICONS_DIR}/${icon}-dark.png"
|
||||
|
||||
echo " ${icon} (light + dark)"
|
||||
|
||||
# Download SVG
|
||||
svg_content=$(curl -sfL "$svg_url" 2>/dev/null) || {
|
||||
echo " Warning: Failed to fetch ${icon}-24.svg (may not exist)"
|
||||
continue
|
||||
}
|
||||
|
||||
# Light theme: dark icons (#24292f) for light backgrounds
|
||||
# Add fill attribute to the svg tag
|
||||
light_svg=$(echo "$svg_content" | sed 's/<svg /<svg fill="#24292f" /')
|
||||
echo "$light_svg" | rsvg-convert -o "$light_file"
|
||||
|
||||
# Dark theme: white icons (#ffffff) for dark backgrounds
|
||||
dark_svg=$(echo "$svg_content" | sed 's/<svg /<svg fill="#ffffff" /')
|
||||
echo "$dark_svg" | rsvg-convert -o "$dark_file"
|
||||
done
|
||||
|
||||
echo "Done. Icons saved to $ICONS_DIR"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Run 'go test ./pkg/octicons/...' to verify icons are embedded"
|
||||
echo " 2. Run 'go test ./pkg/github/...' to verify toolset icons are valid"
|
||||
echo " 3. Commit the new icon files"
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script generates documentation for the GitHub MCP server.
|
||||
# It needs to be run after tool updates to ensure the latest changes are reflected in the documentation.
|
||||
go run ./cmd/github-mcp-server generate-docs
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# echo '{"jsonrpc":"2.0","id":3,"params":{"name":"list_discussions","arguments": {"owner": "github", "repo": "securitylab", "first": 10, "since": "2025-04-01T00:00:00Z"}},"method":"tools/call"}' | go run cmd/github-mcp-server/main.go stdio | jq .
|
||||
echo '{"jsonrpc":"2.0","id":3,"params":{"name":"list_discussions","arguments": {"owner": "github", "repo": "securitylab", "first": 10, "since": "2025-04-01T00:00:00Z", "sort": "CREATED_AT", "direction": "DESC"}},"method":"tools/call"}' | go run cmd/github-mcp-server/main.go stdio | jq .
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
# MCP requires initialize -> notifications/initialized -> tools/call
|
||||
output=$(
|
||||
(
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"get-me-script","version":"1.0.0"}}}'
|
||||
echo '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'
|
||||
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_me","arguments":{}}}'
|
||||
sleep 3
|
||||
) | go run cmd/github-mcp-server/main.go stdio "$@" 2>/dev/null | grep '"id":2'
|
||||
)
|
||||
|
||||
if command -v jq &> /dev/null; then
|
||||
echo "$output" | jq '{_meta: .result._meta, content: (.result.content[0].text | fromjson)}'
|
||||
else
|
||||
echo "$output"
|
||||
fi
|
||||
Executable
+172
@@ -0,0 +1,172 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Generate license files for all platform/arch combinations.
|
||||
# This script handles architecture-specific dependency differences by:
|
||||
# 1. Generating separate license reports per GOOS/GOARCH combination
|
||||
# 2. Grouping identical reports together (comma-separated arch names)
|
||||
# 3. Creating an index at the top of each platform file
|
||||
# 4. Copying all license files to third-party/
|
||||
#
|
||||
# Note: third-party/ is a union of all license files across all architectures.
|
||||
# This means that license files for dependencies present in only some architectures
|
||||
# may still appear in third-party/. This is intentional and ensures compliance.
|
||||
#
|
||||
# Note: we ignore warnings because we want the command to succeed, however the output should be checked
|
||||
# for any new warnings, and potentially we may need to add license information.
|
||||
#
|
||||
# Normally these warnings are packages containing non go code, which may or may not require explicit attribution,
|
||||
# depending on the license.
|
||||
set -e
|
||||
|
||||
# Pinned version for reproducibility
|
||||
# See: https://github.com/cli/cli/pull/11161
|
||||
go install github.com/google/go-licenses/v2@v2.0.1
|
||||
|
||||
# actions/setup-go does not setup the installed toolchain to be preferred over the system install,
|
||||
# which causes go-licenses to raise "Package ... does not have module info" errors in CI.
|
||||
# For more information, https://github.com/google/go-licenses/issues/244#issuecomment-1885098633
|
||||
if [ "$CI" = "true" ]; then
|
||||
export GOROOT=$(go env GOROOT)
|
||||
export PATH=${GOROOT}/bin:$PATH
|
||||
fi
|
||||
|
||||
# actions/setup-go does not setup the installed toolchain to be preferred over the system install,
|
||||
# which causes go-licenses to raise "Package ... does not have module info" errors in CI.
|
||||
# For more information, https://github.com/google/go-licenses/issues/244#issuecomment-1885098633
|
||||
if [ "$CI" = "true" ]; then
|
||||
export GOROOT=$(go env GOROOT)
|
||||
export PATH=${GOROOT}/bin:$PATH
|
||||
fi
|
||||
|
||||
rm -rf third-party
|
||||
mkdir -p third-party
|
||||
export TEMPDIR="$(mktemp -d)"
|
||||
|
||||
trap "rm -fr ${TEMPDIR}" EXIT
|
||||
|
||||
# Cross-platform hash function (works on both Linux and macOS)
|
||||
compute_hash() {
|
||||
if command -v md5sum >/dev/null 2>&1; then
|
||||
md5sum | cut -d' ' -f1
|
||||
elif command -v md5 >/dev/null 2>&1; then
|
||||
md5 -q
|
||||
else
|
||||
# Fallback to cksum if neither is available
|
||||
cksum | cut -d' ' -f1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get architectures for a given OS
|
||||
get_archs() {
|
||||
case "$1" in
|
||||
linux) echo "386 amd64 arm64" ;;
|
||||
darwin) echo "amd64 arm64" ;;
|
||||
windows) echo "386 amd64 arm64" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Generate reports for each platform/arch combination
|
||||
for goos in darwin linux windows; do
|
||||
echo "Processing ${goos}..."
|
||||
|
||||
archs=$(get_archs "$goos")
|
||||
|
||||
for goarch in $archs; do
|
||||
echo " Generating for ${goos}/${goarch}..."
|
||||
|
||||
# Generate the license report for this arch
|
||||
report_file="${TEMPDIR}/${goos}_${goarch}_report.md"
|
||||
GOOS="${goos}" GOARCH="${goarch}" GOFLAGS=-mod=mod go-licenses report ./... --template .github/licenses.tmpl > "${report_file}" 2>/dev/null || echo " (warnings ignored for ${goos}/${goarch})"
|
||||
|
||||
# Save licenses to temp directory
|
||||
GOOS="${goos}" GOARCH="${goarch}" GOFLAGS=-mod=mod go-licenses save ./... --save_path="${TEMPDIR}/${goos}_${goarch}" --force 2>/dev/null || echo " (warnings ignored for ${goos}/${goarch})"
|
||||
|
||||
# Copy to third-party (accumulate all - union of all architectures for compliance)
|
||||
if [ -d "${TEMPDIR}/${goos}_${goarch}" ]; then
|
||||
cp -fR "${TEMPDIR}/${goos}_${goarch}"/* third-party/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Extract just the package list (skip header), sort it, and hash it
|
||||
# Use LC_ALL=C for consistent sorting across different systems
|
||||
packages_file="${TEMPDIR}/${goos}_${goarch}_packages.txt"
|
||||
if [ -s "${report_file}" ] && grep -qE '^ - \[' "${report_file}" 2>/dev/null; then
|
||||
grep -E '^ - \[' "${report_file}" | LC_ALL=C sort > "${packages_file}"
|
||||
hash=$(cat "${packages_file}" | compute_hash)
|
||||
else
|
||||
echo "(FAILED TO GENERATE LICENSE REPORT FOR ${goos}/${goarch})" > "${packages_file}"
|
||||
hash="FAILED_${goos}_${goarch}"
|
||||
fi
|
||||
|
||||
# Store hash for grouping
|
||||
echo "${hash}" > "${TEMPDIR}/${goos}_${goarch}_hash.txt"
|
||||
done
|
||||
|
||||
# Group architectures with identical reports (deterministic order)
|
||||
# Create groups file: hash -> comma-separated archs
|
||||
groups_file="${TEMPDIR}/${goos}_groups.txt"
|
||||
rm -f "${groups_file}"
|
||||
|
||||
# Process architectures in order to build groups
|
||||
for goarch in $archs; do
|
||||
hash=$(cat "${TEMPDIR}/${goos}_${goarch}_hash.txt")
|
||||
# Check if we've seen this hash before
|
||||
if grep -q "^${hash}:" "${groups_file}" 2>/dev/null; then
|
||||
# Append to existing group
|
||||
existing=$(grep "^${hash}:" "${groups_file}" | cut -d: -f2)
|
||||
sed -i.bak "s/^${hash}:.*/${hash}:${existing}, ${goarch}/" "${groups_file}"
|
||||
rm -f "${groups_file}.bak"
|
||||
else
|
||||
# New group
|
||||
echo "${hash}:${goarch}" >> "${groups_file}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Generate the combined report for this platform
|
||||
output_file="third-party-licenses.${goos}.md"
|
||||
|
||||
cat > "${output_file}" << 'EOF'
|
||||
# GitHub MCP Server dependencies
|
||||
|
||||
The following open source dependencies are used to build the [github/github-mcp-server][] GitHub Model Context Protocol Server.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
EOF
|
||||
|
||||
# Build table of contents (sorted for determinism)
|
||||
# Use LC_ALL=C for consistent sorting across different systems
|
||||
LC_ALL=C sort "${groups_file}" | while IFS=: read -r hash group_archs; do
|
||||
# Create anchor-friendly name
|
||||
anchor=$(echo "${group_archs}" | tr ', ' '-' | tr -s '-')
|
||||
echo "- [${group_archs}](#${anchor})" >> "${output_file}"
|
||||
done
|
||||
|
||||
echo "" >> "${output_file}"
|
||||
echo "---" >> "${output_file}"
|
||||
echo "" >> "${output_file}"
|
||||
|
||||
# Add each unique report section (sorted for determinism)
|
||||
# Use LC_ALL=C for consistent sorting across different systems
|
||||
LC_ALL=C sort "${groups_file}" | while IFS=: read -r hash group_archs; do
|
||||
# Get the packages from the first arch in this group
|
||||
first_arch=$(echo "${group_archs}" | cut -d',' -f1 | tr -d ' ')
|
||||
packages=$(cat "${TEMPDIR}/${goos}_${first_arch}_packages.txt")
|
||||
|
||||
cat >> "${output_file}" << EOF
|
||||
## ${group_archs}
|
||||
|
||||
The following packages are included for the ${group_archs} architectures.
|
||||
|
||||
${packages}
|
||||
|
||||
EOF
|
||||
done
|
||||
|
||||
# Add footer
|
||||
echo "[github/github-mcp-server]: https://github.com/github/github-mcp-server" >> "${output_file}"
|
||||
|
||||
echo "Generated ${output_file}"
|
||||
done
|
||||
|
||||
echo "Done! License files generated."
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Check that license files are up to date.
|
||||
# This script regenerates the license files and compares them with the committed versions.
|
||||
# If there are differences, it exits with an error.
|
||||
|
||||
set -e
|
||||
|
||||
# Store original files for comparison
|
||||
TEMPDIR="$(mktemp -d)"
|
||||
trap "rm -fr ${TEMPDIR}" EXIT
|
||||
|
||||
# Save original license markdown files
|
||||
for goos in darwin linux windows; do
|
||||
cp "third-party-licenses.${goos}.md" "${TEMPDIR}/"
|
||||
done
|
||||
|
||||
# Save the state of third-party directory
|
||||
cp -r third-party "${TEMPDIR}/third-party.orig"
|
||||
|
||||
# Regenerate using the same script
|
||||
./script/licenses
|
||||
|
||||
# Check for any differences in workspace
|
||||
if ! git diff --exit-code --quiet third-party-licenses.*.md third-party/; then
|
||||
echo "License files are out of date:"
|
||||
git diff third-party-licenses.*.md third-party/
|
||||
echo ""
|
||||
printf "\nLicense check failed.\n\nPlease update the license files by running \`./script/licenses\` and committing the output.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "License check passed for all platforms."
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
set -eu
|
||||
|
||||
# first run go fmt
|
||||
gofmt -s -w .
|
||||
|
||||
BINDIR="$(git rev-parse --show-toplevel)"/bin
|
||||
BINARY=$BINDIR/golangci-lint
|
||||
# sync with .github/workflows/lint.yml
|
||||
GOLANGCI_LINT_VERSION=v2.9.0
|
||||
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b "$BINDIR" "$GOLANGCI_LINT_VERSION"
|
||||
fi
|
||||
|
||||
$BINARY run
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# List required OAuth scopes for enabled tools.
|
||||
#
|
||||
# Usage:
|
||||
# script/list-scopes [--toolsets=...] [--output=text|json|summary]
|
||||
#
|
||||
# Examples:
|
||||
# script/list-scopes
|
||||
# script/list-scopes --toolsets=all --output=json
|
||||
# script/list-scopes --toolsets=repos,issues --output=summary
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Build the server if it doesn't exist or is outdated
|
||||
if [ ! -f github-mcp-server ] || [ cmd/github-mcp-server/list_scopes.go -nt github-mcp-server ]; then
|
||||
echo "Building github-mcp-server..." >&2
|
||||
go build -o github-mcp-server ./cmd/github-mcp-server
|
||||
fi
|
||||
|
||||
exec ./github-mcp-server list-scopes "$@"
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to pretty print the output of the github-mcp-server
|
||||
# log.
|
||||
#
|
||||
# It uses colored output when running on a terminal.
|
||||
|
||||
# show script help
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [file]
|
||||
|
||||
If [file] is provided, input is read from that file.
|
||||
If no argument is given, input is read from stdin.
|
||||
|
||||
Options:
|
||||
-h, --help Show this help message and exit
|
||||
EOF
|
||||
}
|
||||
|
||||
# choose color for stdin or stdout if we are printing to
|
||||
# an actual terminal
|
||||
color(){
|
||||
io="$1"
|
||||
if [[ "$io" == "stdin" ]]; then
|
||||
color="\033[0;32m" # green
|
||||
else
|
||||
color="\033[0;36m" # cyan
|
||||
fi
|
||||
if [ ! $is_terminal = "1" ]; then
|
||||
color=""
|
||||
fi
|
||||
echo -e "${color}[$io]"
|
||||
}
|
||||
|
||||
# reset code if we are printing to an actual terminal
|
||||
reset(){
|
||||
if [ ! $is_terminal = "1" ]; then
|
||||
return
|
||||
fi
|
||||
echo -e "\033[0m"
|
||||
}
|
||||
|
||||
|
||||
# Handle -h or --help
|
||||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Determine input source
|
||||
if [[ -n "$1" ]]; then
|
||||
if [[ ! -r "$1" ]]; then
|
||||
echo "Error: File '$1' not found or not readable." >&2
|
||||
exit 1
|
||||
fi
|
||||
input="$1"
|
||||
else
|
||||
input="/dev/stdin"
|
||||
fi
|
||||
|
||||
# check if we are in a terminal for showing colors
|
||||
if test -t 1; then
|
||||
is_terminal="1"
|
||||
else
|
||||
is_terminal="0"
|
||||
fi
|
||||
|
||||
# Processs each log line, print whether is stdin or stdout, using different
|
||||
# colors if we output to a terminal, and pretty print json data using jq
|
||||
sed -nE 's/^.*\[(stdin|stdout)\]:.* ([0-9]+) bytes: (.*)\\n"$/\1 \2 \3/p' $input |
|
||||
while read -r io bytes json; do
|
||||
# Unescape the JSON string safely
|
||||
unescaped=$(echo "$json" | awk '{ print "echo -e \"" $0 "\" | jq ." }' | bash)
|
||||
echo "$(color $io)($bytes bytes):$(reset)"
|
||||
echo "$unescaped" | jq .
|
||||
echo
|
||||
done
|
||||
@@ -0,0 +1,217 @@
|
||||
// Command print-mcp-diff-configs emits the configuration matrix consumed by
|
||||
// the mcp-server-diff GitHub Action. The matrix is composed of three parts:
|
||||
//
|
||||
// 1. Hand-curated baseline configs (default, read-only, common toolset combos)
|
||||
// 2. Insiders configs (--insiders, --insiders --read-only) — meta flag that
|
||||
// expands to the curated insiders feature set
|
||||
// 3. One config per entry in github.AllowedFeatureFlags — automatically kept
|
||||
// in sync with the Go source so any new user-controllable feature flag
|
||||
// gets diffed without touching the workflow
|
||||
//
|
||||
// The same logical matrix is rendered for two transports, selected by
|
||||
// -transport:
|
||||
//
|
||||
// stdio Default. Args are appended to the action's top-level
|
||||
//
|
||||
// start_command (one stdio process per config).
|
||||
//
|
||||
// http-headers streamable-http transport against a shared HTTP server. The
|
||||
//
|
||||
// server is started once with no extra flags and every config
|
||||
// provides its settings via X-MCP-* request headers, mirroring
|
||||
// how the remote server is invoked in production (server-side
|
||||
// defaults + per-user header overrides).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./script/print-mcp-diff-configs
|
||||
// go run ./script/print-mcp-diff-configs -transport http-headers
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/github/github-mcp-server/pkg/github"
|
||||
mcphdr "github.com/github/github-mcp-server/pkg/http/headers"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Name string `json:"name"`
|
||||
Args string `json:"args,omitempty"`
|
||||
Transport string `json:"transport,omitempty"`
|
||||
ServerURL string `json:"server_url,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
}
|
||||
|
||||
// baseEntry describes one logical configuration in transport-agnostic form.
|
||||
// settings are translated to either CLI flags or X-MCP-* headers depending on
|
||||
// the target transport.
|
||||
type baseEntry struct {
|
||||
name string
|
||||
settings settings
|
||||
}
|
||||
|
||||
type settings struct {
|
||||
toolsets string // comma-separated, "" for defaults
|
||||
tools string
|
||||
excludeTools string
|
||||
features string
|
||||
readOnly bool
|
||||
insiders bool
|
||||
lockdown bool
|
||||
}
|
||||
|
||||
const httpServerURL = "http://localhost:8082/mcp"
|
||||
|
||||
func main() {
|
||||
transport := flag.String("transport", "stdio", "Transport to target: stdio or http-headers")
|
||||
flag.Parse()
|
||||
|
||||
entries := baseEntries()
|
||||
|
||||
var out []config
|
||||
switch *transport {
|
||||
case "stdio":
|
||||
for _, e := range entries {
|
||||
out = append(out, config{Name: e.name, Args: e.settings.toArgs()})
|
||||
}
|
||||
case "http-headers":
|
||||
for _, e := range entries {
|
||||
h := e.settings.toHeaders()
|
||||
if h == nil {
|
||||
h = map[string]string{}
|
||||
}
|
||||
// The action's top-level headers may be replaced (not merged) by
|
||||
// per-config headers, so always include the bearer token here.
|
||||
// The token must match a recognized GitHub prefix so the server's
|
||||
// Authorization parser accepts it without contacting the API.
|
||||
h[mcphdr.AuthorizationHeader] = "Bearer ghp_test"
|
||||
out = append(out, config{
|
||||
Name: e.name,
|
||||
Transport: "streamable-http",
|
||||
ServerURL: httpServerURL,
|
||||
Headers: h,
|
||||
})
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown transport %q (want stdio or http-headers)\n", *transport)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(out); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func baseEntries() []baseEntry {
|
||||
entries := []baseEntry{
|
||||
{name: "default"},
|
||||
{name: "read-only", settings: settings{readOnly: true}},
|
||||
{name: "toolsets-repos", settings: settings{toolsets: "repos"}},
|
||||
{name: "toolsets-issues", settings: settings{toolsets: "issues"}},
|
||||
{name: "toolsets-context", settings: settings{toolsets: "context"}},
|
||||
{name: "toolsets-pull_requests", settings: settings{toolsets: "pull_requests"}},
|
||||
{name: "toolsets-repos,issues", settings: settings{toolsets: "repos,issues"}},
|
||||
{name: "toolsets-issues,context", settings: settings{toolsets: "issues,context"}},
|
||||
{name: "toolsets-all", settings: settings{toolsets: "all"}},
|
||||
{name: "tools-get_me", settings: settings{tools: "get_me"}},
|
||||
{name: "tools-get_me,list_issues", settings: settings{tools: "get_me,list_issues"}},
|
||||
{name: "toolsets-repos+read-only", settings: settings{toolsets: "repos", readOnly: true}},
|
||||
{name: "insiders", settings: settings{insiders: true}},
|
||||
{name: "insiders+read-only", settings: settings{insiders: true, readOnly: true}},
|
||||
// Combined entries: exercise multiple settings together so we catch
|
||||
// regressions when several X-MCP-* headers (or CLI flags) are merged.
|
||||
{name: "combined-toolsets+exclude+readonly", settings: settings{
|
||||
toolsets: "repos,issues",
|
||||
excludeTools: "delete_file",
|
||||
readOnly: true,
|
||||
}},
|
||||
{name: "combined-insiders+toolsets+features", settings: settings{
|
||||
insiders: true,
|
||||
toolsets: "repos",
|
||||
features: firstFeatureFlag(),
|
||||
}},
|
||||
}
|
||||
|
||||
flags := append([]string(nil), github.AllowedFeatureFlags...)
|
||||
sort.Strings(flags)
|
||||
for _, f := range flags {
|
||||
entries = append(entries, baseEntry{
|
||||
name: "feature-" + f,
|
||||
settings: settings{features: f},
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (s settings) toArgs() string {
|
||||
var parts []string
|
||||
if s.toolsets != "" {
|
||||
parts = append(parts, "--toolsets="+s.toolsets)
|
||||
}
|
||||
if s.tools != "" {
|
||||
parts = append(parts, "--tools="+s.tools)
|
||||
}
|
||||
if s.excludeTools != "" {
|
||||
parts = append(parts, "--exclude-tools="+s.excludeTools)
|
||||
}
|
||||
if s.features != "" {
|
||||
parts = append(parts, "--features="+s.features)
|
||||
}
|
||||
if s.readOnly {
|
||||
parts = append(parts, "--read-only")
|
||||
}
|
||||
if s.insiders {
|
||||
parts = append(parts, "--insiders")
|
||||
}
|
||||
if s.lockdown {
|
||||
parts = append(parts, "--lockdown-mode")
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func (s settings) toHeaders() map[string]string {
|
||||
h := map[string]string{}
|
||||
if s.toolsets != "" {
|
||||
h[mcphdr.MCPToolsetsHeader] = s.toolsets
|
||||
}
|
||||
if s.tools != "" {
|
||||
h[mcphdr.MCPToolsHeader] = s.tools
|
||||
}
|
||||
if s.excludeTools != "" {
|
||||
h[mcphdr.MCPExcludeToolsHeader] = s.excludeTools
|
||||
}
|
||||
if s.features != "" {
|
||||
h[mcphdr.MCPFeaturesHeader] = s.features
|
||||
}
|
||||
if s.readOnly {
|
||||
h[mcphdr.MCPReadOnlyHeader] = "true"
|
||||
}
|
||||
if s.insiders {
|
||||
h[mcphdr.MCPInsidersHeader] = "true"
|
||||
}
|
||||
if s.lockdown {
|
||||
h[mcphdr.MCPLockdownHeader] = "true"
|
||||
}
|
||||
if len(h) == 0 {
|
||||
return nil
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func firstFeatureFlag() string {
|
||||
flags := append([]string(nil), github.AllowedFeatureFlags...)
|
||||
if len(flags) == 0 {
|
||||
return ""
|
||||
}
|
||||
sort.Strings(flags)
|
||||
return flags[0]
|
||||
}
|
||||
Executable
+151
@@ -0,0 +1,151 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit immediately if a command exits with a non-zero status.
|
||||
set -e
|
||||
|
||||
# Initialize variables
|
||||
TAG=""
|
||||
DRY_RUN=false
|
||||
|
||||
# Parse arguments
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
;;
|
||||
*)
|
||||
# The first non-flag argument is the tag
|
||||
if [[ ! $arg == --* ]]; then
|
||||
if [ -z "$TAG" ]; then
|
||||
TAG=$arg
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "DRY RUN: No changes will be pushed to the remote repository."
|
||||
echo
|
||||
fi
|
||||
|
||||
# 1. Validate input
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "Error: No tag specified."
|
||||
echo "Usage: ./script/tag-release vX.Y.Z [--dry-run]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Regular expression for semantic versioning (vX.Y.Z or vX.Y.Z-suffix)
|
||||
if [[ ! $TAG =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-.*)?$ ]]; then
|
||||
echo "Error: Tag must be in format vX.Y.Z or vX.Y.Z-suffix (e.g., v1.0.0 or v1.0.0-rc1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Check current branch
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$CURRENT_BRANCH" != "main" ]; then
|
||||
echo "Error: You must be on the 'main' branch to create a release."
|
||||
echo "Current branch is '$CURRENT_BRANCH'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Fetch latest from origin
|
||||
echo "Fetching latest changes from origin..."
|
||||
git fetch origin main
|
||||
|
||||
# 4. Check if the working directory is clean
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
echo "Error: Working directory is not clean. Please commit or stash your changes."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 5. Check if main is up-to-date with origin/main
|
||||
LOCAL_SHA=$(git rev-parse @)
|
||||
REMOTE_SHA=$(git rev-parse @{u})
|
||||
|
||||
if [ "$LOCAL_SHA" != "$REMOTE_SHA" ]; then
|
||||
echo "Error: Your local 'main' branch is not up-to-date with 'origin/main'. Please pull the latest changes."
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Local 'main' branch is up-to-date with 'origin/main'."
|
||||
|
||||
# 6. Check if tag already exists
|
||||
if git tag -l | grep -q "^${TAG}$"; then
|
||||
echo "Error: Tag ${TAG} already exists locally."
|
||||
exit 1
|
||||
fi
|
||||
if git ls-remote --tags origin | grep -q "refs/tags/${TAG}$"; then
|
||||
echo "Error: Tag ${TAG} already exists on remote 'origin'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 7. Confirm release with user
|
||||
echo
|
||||
LATEST_TAG=$(git tag --sort=-version:refname | head -n 1)
|
||||
if [ -n "$LATEST_TAG" ]; then
|
||||
echo "Current latest release: $LATEST_TAG"
|
||||
fi
|
||||
echo "Proposed new release: $TAG"
|
||||
echo
|
||||
read -p "Do you want to proceed with the release? (y/n) " -n 1 -r
|
||||
echo # Move to a new line
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Release cancelled."
|
||||
exit 1
|
||||
fi
|
||||
echo
|
||||
|
||||
# 8. Create the new release tag
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "DRY RUN: Skipping creation of tag $TAG."
|
||||
else
|
||||
echo "Creating new release tag: $TAG"
|
||||
git tag -a "$TAG" -m "Release $TAG"
|
||||
fi
|
||||
|
||||
# 9. Push the new tag to the remote repository
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "DRY RUN: Skipping push of tag $TAG to origin."
|
||||
else
|
||||
echo "Pushing tag $TAG to origin..."
|
||||
git push origin "$TAG"
|
||||
fi
|
||||
|
||||
# 10. Update and push the 'latest-release' tag
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "DRY RUN: Skipping update and push of 'latest-release' tag."
|
||||
else
|
||||
echo "Updating 'latest-release' tag to point to $TAG..."
|
||||
git tag -f latest-release "$TAG"
|
||||
echo "Pushing 'latest-release' tag to origin..."
|
||||
git push origin latest-release --force
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "✅ DRY RUN complete. No tags were created or pushed."
|
||||
else
|
||||
echo "✅ Successfully tagged and pushed release $TAG."
|
||||
echo "✅ 'latest-release' tag has been updated."
|
||||
fi
|
||||
|
||||
# 11. Post-release instructions
|
||||
REPO_URL=$(git remote get-url origin)
|
||||
REPO_SLUG=$(echo "$REPO_URL" | sed -e 's/.*github.com[:\/]//' -e 's/\.git$//')
|
||||
|
||||
cat << EOF
|
||||
|
||||
## 🎉 Release $TAG has been initiated!
|
||||
|
||||
### Next steps:
|
||||
1. 📋 Check https://github.com/$REPO_SLUG/releases and wait for the draft release to show up (after the goreleaser workflow completes)
|
||||
2. ✏️ Edit the new release, delete the existing notes and click the auto-generate button GitHub provides
|
||||
3. ✨ Add a section at the top calling out the main features
|
||||
4. 🚀 Publish the release
|
||||
5. 📢 Post message in #gh-mcp-releases channel in Slack and then share to the other mcp channels
|
||||
|
||||
### Resources:
|
||||
- 📦 Draft Release: https://github.com/$REPO_SLUG/releases/tag/$TAG
|
||||
|
||||
The release process is now ready for your review and completion!
|
||||
EOF
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
set -eu
|
||||
|
||||
go test -race ./...
|
||||
Reference in New Issue
Block a user