Skip to content

Commit 420ccc2

Browse files
Merge branch 'main' into actions-consolidation-backport
2 parents 9049908 + a48e306 commit 420ccc2

File tree

8 files changed

+262
-98
lines changed

8 files changed

+262
-98
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,7 @@ The following sets of tools are available:
10471047

10481048
- **get_file_contents** - Get file or directory contents
10491049
- `owner`: Repository owner (username or organization) (string, required)
1050-
- `path`: Path to file/directory (directories must end with a slash '/') (string, optional)
1050+
- `path`: Path to file/directory (string, optional)
10511051
- `ref`: Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head` (string, optional)
10521052
- `repo`: Repository name (string, required)
10531053
- `sha`: Accepts optional commit SHA. If specified, it will be used instead of ref (string, optional)

pkg/github/__toolsnaps__/get_file_contents.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
},
1818
"path": {
1919
"type": "string",
20-
"description": "Path to file/directory (directories must end with a slash '/')",
20+
"description": "Path to file/directory",
2121
"default": "/"
2222
},
2323
"ref": {

pkg/github/repositories.go

Lines changed: 60 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
560560
},
561561
"path": {
562562
Type: "string",
563-
Description: "Path to file/directory (directories must end with a slash '/')",
563+
Description: "Path to file/directory",
564564
Default: json.RawMessage(`"/"`),
565565
},
566566
"ref": {
@@ -608,28 +608,26 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
608608
return utils.NewToolResultError(fmt.Sprintf("failed to resolve git reference: %s", err)), nil, nil
609609
}
610610

611-
// If the path is (most likely) not to be a directory, we will
612-
// first try to get the raw content from the GitHub raw content API.
611+
if rawOpts.SHA != "" {
612+
ref = rawOpts.SHA
613+
}
613614

614-
var rawAPIResponseCode int
615-
if path != "" && !strings.HasSuffix(path, "/") {
616-
// First, get file info from Contents API to retrieve SHA
617-
var fileSHA string
618-
opts := &github.RepositoryContentGetOptions{Ref: ref}
619-
fileContent, _, respContents, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
620-
if respContents != nil {
621-
defer func() { _ = respContents.Body.Close() }()
622-
}
623-
if err != nil {
624-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
625-
"failed to get file SHA",
626-
respContents,
627-
err,
628-
), nil, nil
629-
}
630-
if fileContent == nil || fileContent.SHA == nil {
631-
return utils.NewToolResultError("file content SHA is nil, if a directory was requested, path parameters should end with a trailing slash '/'"), nil, nil
632-
}
615+
var fileSHA string
616+
opts := &github.RepositoryContentGetOptions{Ref: ref}
617+
618+
// Always call GitHub Contents API first to get metadata including SHA and determine if it's a file or directory
619+
fileContent, dirContent, respContents, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
620+
if respContents != nil {
621+
defer func() { _ = respContents.Body.Close() }()
622+
}
623+
624+
// The path does not point to a file or directory.
625+
// Instead let's try to find it in the Git Tree by matching the end of the path.
626+
if err != nil || (fileContent == nil && dirContent == nil) {
627+
return matchFiles(ctx, client, owner, repo, ref, path, rawOpts, 0)
628+
}
629+
630+
if fileContent != nil && fileContent.SHA != nil {
633631
fileSHA = *fileContent.SHA
634632

635633
rawClient, err := getRawClient(ctx)
@@ -702,55 +700,19 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
702700
}
703701
return utils.NewToolResultResource("successfully downloaded binary file", result), nil, nil
704702
}
705-
rawAPIResponseCode = resp.StatusCode
706-
}
707703

708-
if rawOpts.SHA != "" {
709-
ref = rawOpts.SHA
710-
}
711-
if strings.HasSuffix(path, "/") {
712-
opts := &github.RepositoryContentGetOptions{Ref: ref}
713-
_, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
714-
if err == nil && resp.StatusCode == http.StatusOK {
715-
defer func() { _ = resp.Body.Close() }()
716-
r, err := json.Marshal(dirContent)
717-
if err != nil {
718-
return utils.NewToolResultError("failed to marshal response"), nil, nil
719-
}
720-
return utils.NewToolResultText(string(r)), nil, nil
721-
}
722-
}
723-
724-
// The path does not point to a file or directory.
725-
// Instead let's try to find it in the Git Tree by matching the end of the path.
726-
727-
// Step 1: Get Git Tree recursively
728-
tree, resp, err := client.Git.GetTree(ctx, owner, repo, ref, true)
729-
if err != nil {
730-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
731-
"failed to get git tree",
732-
resp,
733-
err,
734-
), nil, nil
735-
}
736-
defer func() { _ = resp.Body.Close() }()
737-
738-
// Step 2: Filter tree for matching paths
739-
const maxMatchingFiles = 3
740-
matchingFiles := filterPaths(tree.Entries, path, maxMatchingFiles)
741-
if len(matchingFiles) > 0 {
742-
matchingFilesJSON, err := json.Marshal(matchingFiles)
704+
// Raw API call failed
705+
return matchFiles(ctx, client, owner, repo, ref, path, rawOpts, resp.StatusCode)
706+
} else if dirContent != nil {
707+
// file content or file SHA is nil which means it's a directory
708+
r, err := json.Marshal(dirContent)
743709
if err != nil {
744-
return utils.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s", err)), nil, nil
710+
return utils.NewToolResultError("failed to marshal response"), nil, nil
745711
}
746-
resolvedRefs, err := json.Marshal(rawOpts)
747-
if err != nil {
748-
return utils.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil, nil
749-
}
750-
return utils.NewToolResultError(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s), but the raw content API returned an unexpected status code %d.", string(resolvedRefs), string(matchingFilesJSON), rawAPIResponseCode)), nil, nil
712+
return utils.NewToolResultText(string(r)), nil, nil
751713
}
752714

753-
return utils.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), nil, nil
715+
return utils.NewToolResultError("failed to get file contents"), nil, nil
754716
})
755717

756718
return tool, handler
@@ -2115,3 +2077,35 @@ func UnstarRepository(getClient GetClientFn, t translations.TranslationHelperFun
21152077

21162078
return tool, handler
21172079
}
2080+
2081+
func matchFiles(ctx context.Context, client *github.Client, owner, repo, ref, path string, rawOpts *raw.ContentOpts, rawAPIResponseCode int) (*mcp.CallToolResult, any, error) {
2082+
// Step 1: Get Git Tree recursively
2083+
tree, response, err := client.Git.GetTree(ctx, owner, repo, ref, true)
2084+
if err != nil {
2085+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
2086+
"failed to get git tree",
2087+
response,
2088+
err,
2089+
), nil, nil
2090+
}
2091+
defer func() { _ = response.Body.Close() }()
2092+
2093+
// Step 2: Filter tree for matching paths
2094+
const maxMatchingFiles = 3
2095+
matchingFiles := filterPaths(tree.Entries, path, maxMatchingFiles)
2096+
if len(matchingFiles) > 0 {
2097+
matchingFilesJSON, err := json.Marshal(matchingFiles)
2098+
if err != nil {
2099+
return utils.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s", err)), nil, nil
2100+
}
2101+
resolvedRefs, err := json.Marshal(rawOpts)
2102+
if err != nil {
2103+
return utils.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil, nil
2104+
}
2105+
if rawAPIResponseCode > 0 {
2106+
return utils.NewToolResultText(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s), but the content API returned an unexpected status code %d.", string(resolvedRefs), string(matchingFilesJSON), rawAPIResponseCode)), nil, nil
2107+
}
2108+
return utils.NewToolResultText(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s).", string(resolvedRefs), string(matchingFilesJSON))), nil, nil
2109+
}
2110+
return utils.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), nil, nil
2111+
}

script/licenses

Lines changed: 151 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,163 @@
11
#!/bin/bash
2+
#
3+
# Generate license files for all platform/arch combinations.
4+
# This script handles architecture-specific dependency differences by:
5+
# 1. Generating separate license reports per GOOS/GOARCH combination
6+
# 2. Grouping identical reports together (comma-separated arch names)
7+
# 3. Creating an index at the top of each platform file
8+
# 4. Copying all license files to third-party/
9+
#
10+
# Note: third-party/ is a union of all license files across all architectures.
11+
# This means that license files for dependencies present in only some architectures
12+
# may still appear in third-party/. This is intentional and ensures compliance.
13+
#
14+
# Note: we ignore warnings because we want the command to succeed, however the output should be checked
15+
# for any new warnings, and potentially we may need to add license information.
16+
#
17+
# Normally these warnings are packages containing non go code, which may or may not require explicit attribution,
18+
# depending on the license.
19+
20+
set -e
221

322
go install github.com/google/go-licenses@latest
423

24+
# actions/setup-go does not setup the installed toolchain to be preferred over the system install,
25+
# which causes go-licenses to raise "Package ... does not have module info" errors in CI.
26+
# For more information, https://github.com/google/go-licenses/issues/244#issuecomment-1885098633
27+
if [ "$CI" = "true" ]; then
28+
export GOROOT=$(go env GOROOT)
29+
export PATH=${GOROOT}/bin:$PATH
30+
fi
31+
532
rm -rf third-party
633
mkdir -p third-party
734
export TEMPDIR="$(mktemp -d)"
835

936
trap "rm -fr ${TEMPDIR}" EXIT
1037

11-
for goos in linux darwin windows ; do
12-
# Note: we ignore warnings because we want the command to succeed, however the output should be checked
13-
# for any new warnings, and potentially we may need to add license information.
14-
#
15-
# Normally these warnings are packages containing non go code, which may or may not require explicit attribution,
16-
# depending on the license.
17-
GOOS="${goos}" GOFLAGS=-mod=mod go-licenses save ./... --save_path="${TEMPDIR}/${goos}" --force || echo "Ignore warnings"
18-
GOOS="${goos}" GOFLAGS=-mod=mod go-licenses report ./... --template .github/licenses.tmpl > third-party-licenses.${goos}.md || echo "Ignore warnings"
19-
cp -fR "${TEMPDIR}/${goos}"/* third-party/
38+
# Cross-platform hash function (works on both Linux and macOS)
39+
compute_hash() {
40+
if command -v md5sum >/dev/null 2>&1; then
41+
md5sum | cut -d' ' -f1
42+
elif command -v md5 >/dev/null 2>&1; then
43+
md5 -q
44+
else
45+
# Fallback to cksum if neither is available
46+
cksum | cut -d' ' -f1
47+
fi
48+
}
49+
50+
# Function to get architectures for a given OS
51+
get_archs() {
52+
case "$1" in
53+
linux) echo "386 amd64 arm64" ;;
54+
darwin) echo "amd64 arm64" ;;
55+
windows) echo "386 amd64 arm64" ;;
56+
esac
57+
}
58+
59+
# Generate reports for each platform/arch combination
60+
for goos in darwin linux windows; do
61+
echo "Processing ${goos}..."
62+
63+
archs=$(get_archs "$goos")
64+
65+
for goarch in $archs; do
66+
echo " Generating for ${goos}/${goarch}..."
67+
68+
# Generate the license report for this arch
69+
report_file="${TEMPDIR}/${goos}_${goarch}_report.md"
70+
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})"
71+
72+
# Save licenses to temp directory
73+
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})"
74+
75+
# Copy to third-party (accumulate all - union of all architectures for compliance)
76+
if [ -d "${TEMPDIR}/${goos}_${goarch}" ]; then
77+
cp -fR "${TEMPDIR}/${goos}_${goarch}"/* third-party/ 2>/dev/null || true
78+
fi
79+
80+
# Extract just the package list (skip header), sort it, and hash it
81+
# Use LC_ALL=C for consistent sorting across different systems
82+
packages_file="${TEMPDIR}/${goos}_${goarch}_packages.txt"
83+
if [ -s "${report_file}" ] && grep -qE '^ - \[' "${report_file}" 2>/dev/null; then
84+
grep -E '^ - \[' "${report_file}" | LC_ALL=C sort > "${packages_file}"
85+
hash=$(cat "${packages_file}" | compute_hash)
86+
else
87+
echo "(FAILED TO GENERATE LICENSE REPORT FOR ${goos}/${goarch})" > "${packages_file}"
88+
hash="FAILED_${goos}_${goarch}"
89+
fi
90+
91+
# Store hash for grouping
92+
echo "${hash}" > "${TEMPDIR}/${goos}_${goarch}_hash.txt"
93+
done
94+
95+
# Group architectures with identical reports (deterministic order)
96+
# Create groups file: hash -> comma-separated archs
97+
groups_file="${TEMPDIR}/${goos}_groups.txt"
98+
rm -f "${groups_file}"
99+
100+
# Process architectures in order to build groups
101+
for goarch in $archs; do
102+
hash=$(cat "${TEMPDIR}/${goos}_${goarch}_hash.txt")
103+
# Check if we've seen this hash before
104+
if grep -q "^${hash}:" "${groups_file}" 2>/dev/null; then
105+
# Append to existing group
106+
existing=$(grep "^${hash}:" "${groups_file}" | cut -d: -f2)
107+
sed -i.bak "s/^${hash}:.*/${hash}:${existing}, ${goarch}/" "${groups_file}"
108+
rm -f "${groups_file}.bak"
109+
else
110+
# New group
111+
echo "${hash}:${goarch}" >> "${groups_file}"
112+
fi
113+
done
114+
115+
# Generate the combined report for this platform
116+
output_file="third-party-licenses.${goos}.md"
117+
118+
cat > "${output_file}" << 'EOF'
119+
# GitHub MCP Server dependencies
120+
121+
The following open source dependencies are used to build the [github/github-mcp-server][] GitHub Model Context Protocol Server.
122+
123+
## Table of Contents
124+
125+
EOF
126+
127+
# Build table of contents (sorted for determinism)
128+
# Use LC_ALL=C for consistent sorting across different systems
129+
LC_ALL=C sort "${groups_file}" | while IFS=: read -r hash group_archs; do
130+
# Create anchor-friendly name
131+
anchor=$(echo "${group_archs}" | tr ', ' '-' | tr -s '-')
132+
echo "- [${group_archs}](#${anchor})" >> "${output_file}"
133+
done
134+
135+
echo "" >> "${output_file}"
136+
echo "---" >> "${output_file}"
137+
echo "" >> "${output_file}"
138+
139+
# Add each unique report section (sorted for determinism)
140+
# Use LC_ALL=C for consistent sorting across different systems
141+
LC_ALL=C sort "${groups_file}" | while IFS=: read -r hash group_archs; do
142+
# Get the packages from the first arch in this group
143+
first_arch=$(echo "${group_archs}" | cut -d',' -f1 | tr -d ' ')
144+
packages=$(cat "${TEMPDIR}/${goos}_${first_arch}_packages.txt")
145+
146+
cat >> "${output_file}" << EOF
147+
## ${group_archs}
148+
149+
The following packages are included for the ${group_archs} architectures.
150+
151+
${packages}
152+
153+
EOF
154+
done
155+
156+
# Add footer
157+
echo "[github/github-mcp-server]: https://github.com/github/github-mcp-server" >> "${output_file}"
158+
159+
echo "Generated ${output_file}"
20160
done
21161

162+
echo "Done! License files generated."
163+

script/licenses-check

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
11
#!/bin/bash
2+
#
3+
# Check that license files are up to date.
4+
# This script regenerates the license files and compares them with the committed versions.
5+
# If there are differences, it exits with an error.
26

3-
go install github.com/google/go-licenses@latest
4-
5-
for goos in linux darwin windows ; do
6-
# Note: we ignore warnings because we want the command to succeed, however the output should be checked
7-
# for any new warnings, and potentially we may need to add license information.
8-
#
9-
# Normally these warnings are packages containing non go code, which may or may not require explicit attribution,
10-
# depending on the license.
11-
GOOS="${goos}" GOFLAGS=-mod=mod go-licenses report ./... --template .github/licenses.tmpl > third-party-licenses.${goos}.copy.md || echo "Ignore warnings"
12-
if ! diff -s third-party-licenses.${goos}.copy.md third-party-licenses.${goos}.md; then
13-
printf "License check failed.\n\nPlease update the license file by running \`.script/licenses\` and committing the output."
14-
rm -f third-party-licenses.${goos}.copy.md
15-
exit 1
16-
fi
17-
rm -f third-party-licenses.${goos}.copy.md
7+
set -e
8+
9+
# Store original files for comparison
10+
TEMPDIR="$(mktemp -d)"
11+
trap "rm -fr ${TEMPDIR}" EXIT
12+
13+
# Save original license markdown files
14+
for goos in darwin linux windows; do
15+
cp "third-party-licenses.${goos}.md" "${TEMPDIR}/"
1816
done
1917

18+
# Save the state of third-party directory
19+
cp -r third-party "${TEMPDIR}/third-party.orig"
20+
21+
# Regenerate using the same script
22+
./script/licenses
23+
24+
# Check for any differences in workspace
25+
if ! git diff --exit-code --quiet third-party-licenses.*.md third-party/; then
26+
echo "License files are out of date:"
27+
git diff third-party-licenses.*.md third-party/
28+
echo ""
29+
printf "\nLicense check failed.\n\nPlease update the license files by running \`./script/licenses\` and committing the output.\n"
30+
exit 1
31+
fi
2032

33+
echo "License check passed for all platforms."
2134

0 commit comments

Comments
 (0)