@sentry/junior-github 0.91.0 → 0.92.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SETUP.md +4 -4
- package/dist/index.js +139 -30
- package/package.json +2 -2
- package/skills/github-code/references/api-surface.md +1 -1
package/SETUP.md
CHANGED
|
@@ -98,9 +98,9 @@ Use `additionalUserScopes` only when an integration flow requires specific GitHu
|
|
|
98
98
|
## 3) Runtime behavior
|
|
99
99
|
|
|
100
100
|
- When either GitHub skill is active, authenticated `gh` and `git` commands cause the runtime to inject GitHub credentials automatically for the current turn.
|
|
101
|
-
- The plugin classifies GitHub traffic from the forwarded HTTP request. Safe app-readable API methods, GraphQL `GET`/`HEAD`/`OPTIONS` requests, GraphQL `POST` bodies that prove the operation is a query, and `git-upload-pack` use the `installation-read` grant. `GET /user` uses the `user-read` grant so account identity checks use the
|
|
102
|
-
- `user-read` and `user-write` require the
|
|
103
|
-
- Git commits use the
|
|
101
|
+
- The plugin classifies GitHub traffic from the forwarded HTTP request. Safe app-readable API methods, GraphQL `GET`/`HEAD`/`OPTIONS` requests, GraphQL `POST` bodies that prove the operation is a query, and `git-upload-pack` use the `installation-read` grant. `GET /user` uses the `user-read` grant so account identity checks use the actor token. Write-specific REST URLs, GraphQL mutations/subscriptions, unknown GraphQL `POST` bodies, other non-read API methods, and `git-receive-pack` use the `user-write` grant.
|
|
102
|
+
- `user-read` and `user-write` require the actor, or an explicitly delegated user subject from an allowed system run, to authorize the GitHub App through the private OAuth flow. Missing or expired user authorization pauses interactive turns, sends a private authorization link, and resumes after approval.
|
|
103
|
+
- Git commits use the actor as the commit author, Junior as committer, and a Junior `Co-Authored-By` trailer.
|
|
104
104
|
- Issued credentials are reused only within the current turn, credential leases are cached separately by plugin grant name, and upstream 403 permission denials clear the cached lease before the next retry.
|
|
105
105
|
- Sandbox does not receive raw tokens via env; host applies Authorization header transforms for GitHub API calls.
|
|
106
106
|
|
|
@@ -132,7 +132,7 @@ The plugin uses `installation-read` for read-only GitHub traffic and `user-write
|
|
|
132
132
|
|
|
133
133
|
Committing and pushing code uses more than one GitHub surface:
|
|
134
134
|
|
|
135
|
-
- Creating the local Git commit does not call GitHub. Junior sets the
|
|
135
|
+
- Creating the local Git commit does not call GitHub. Junior sets the actor as author and the GitHub App bot as committer in the sandbox.
|
|
136
136
|
- Pushing a branch with Git smart HTTP (`git push`) uses the `user-write` grant and requires the GitHub App to have `Contents: write` on the target repository. The requesting user must also have write access to that repository.
|
|
137
137
|
- REST Git database writes used by some `gh` flows also require `Contents: write`: create blob (`POST /git/blobs`), create tree (`POST /git/trees`), create commit (`POST /git/commits`), and create/update refs (`POST /git/refs`, `PATCH /git/refs/{ref}`). Changes to workflow files may also require `Workflows: write`.
|
|
138
138
|
- Opening the PR after the branch exists is separate: `github_createPullRequest` needs pull-request write permission, but it should not create or push commits itself.
|
package/dist/index.js
CHANGED
|
@@ -887,24 +887,62 @@ function cleanIdentityPart(value) {
|
|
|
887
887
|
function isSlackUserId(value) {
|
|
888
888
|
return /^[UW][A-Z0-9]{5,}$/.test(value);
|
|
889
889
|
}
|
|
890
|
-
function
|
|
890
|
+
function isUserActor(actor) {
|
|
891
|
+
return Boolean(actor && "userId" in actor);
|
|
892
|
+
}
|
|
893
|
+
function actorDisplayName(value, actor) {
|
|
891
894
|
const name = cleanIdentityPart(value);
|
|
892
|
-
if (!name || name.toLowerCase() === "unknown" || name === cleanIdentityPart(
|
|
895
|
+
if (!name || name.toLowerCase() === "unknown" || name === cleanIdentityPart(isUserActor(actor) ? actor.userId : void 0)) {
|
|
893
896
|
return void 0;
|
|
894
897
|
}
|
|
895
898
|
return isSlackUserId(name) ? void 0 : name;
|
|
896
899
|
}
|
|
897
|
-
function
|
|
898
|
-
|
|
900
|
+
function actorName(actor) {
|
|
901
|
+
if (!isUserActor(actor)) {
|
|
902
|
+
return void 0;
|
|
903
|
+
}
|
|
904
|
+
return actorDisplayName(actor?.fullName, actor) || actorDisplayName(actor?.userName, actor) || void 0;
|
|
899
905
|
}
|
|
900
|
-
function
|
|
901
|
-
|
|
906
|
+
function actorEmail(actor) {
|
|
907
|
+
if (!isUserActor(actor)) {
|
|
908
|
+
return void 0;
|
|
909
|
+
}
|
|
910
|
+
const email = cleanIdentityPart(actor?.email);
|
|
902
911
|
return /^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(email) ? email : void 0;
|
|
903
912
|
}
|
|
904
|
-
function
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
913
|
+
function actorIdentityKey(actor) {
|
|
914
|
+
if (actor.platform === "system") {
|
|
915
|
+
return `system ${actor.name}`;
|
|
916
|
+
}
|
|
917
|
+
return actor.platform === "slack" ? `slack ${actor.teamId} ${actor.userId}` : `${actor.platform} ${actor.userId}`;
|
|
918
|
+
}
|
|
919
|
+
function additionalActorCoauthorTrailers(args) {
|
|
920
|
+
if (!args.actors || args.actors.length === 0) {
|
|
921
|
+
return [];
|
|
922
|
+
}
|
|
923
|
+
const runActorKey = args.runActor ? actorIdentityKey(args.runActor) : void 0;
|
|
924
|
+
const seenEmails = /* @__PURE__ */ new Set([args.botEmail.toLowerCase()]);
|
|
925
|
+
if (args.authorEmail) {
|
|
926
|
+
seenEmails.add(args.authorEmail.toLowerCase());
|
|
927
|
+
}
|
|
928
|
+
const trailers = [];
|
|
929
|
+
for (const candidate of args.actors) {
|
|
930
|
+
if (runActorKey && actorIdentityKey(candidate) === runActorKey) {
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
const name = actorName(candidate);
|
|
934
|
+
const email = actorEmail(candidate);
|
|
935
|
+
if (!name || !email) {
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
const emailKey = email.toLowerCase();
|
|
939
|
+
if (seenEmails.has(emailKey)) {
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
seenEmails.add(emailKey);
|
|
943
|
+
trailers.push(`Co-Authored-By: ${name} <${email}>`);
|
|
944
|
+
}
|
|
945
|
+
return trailers;
|
|
908
946
|
}
|
|
909
947
|
function prepareCommitMsgHook() {
|
|
910
948
|
return `#!/usr/bin/env bash
|
|
@@ -916,12 +954,12 @@ if [ -z "$message_file" ]; then
|
|
|
916
954
|
fi
|
|
917
955
|
|
|
918
956
|
if [ -z "\${JUNIOR_GIT_AUTHOR_NAME:-}" ] || [ -z "\${JUNIOR_GIT_AUTHOR_EMAIL:-}" ]; then
|
|
919
|
-
echo "Junior GitHub plugin internal error:
|
|
957
|
+
echo "Junior GitHub plugin internal error: actor commit attribution was not injected by the host runtime. Do not set Git author env vars manually; report this configuration error." >&2
|
|
920
958
|
exit 1
|
|
921
959
|
fi
|
|
922
960
|
|
|
923
961
|
if [ "\${GIT_AUTHOR_NAME:-}" != "$JUNIOR_GIT_AUTHOR_NAME" ] || [ "\${GIT_AUTHOR_EMAIL:-}" != "$JUNIOR_GIT_AUTHOR_EMAIL" ]; then
|
|
924
|
-
echo "Junior GitHub plugin internal error: Git author was not set to the resolved
|
|
962
|
+
echo "Junior GitHub plugin internal error: Git author was not set to the resolved actor identity. Do not override Git author manually; report this configuration error." >&2
|
|
925
963
|
exit 1
|
|
926
964
|
fi
|
|
927
965
|
|
|
@@ -930,12 +968,86 @@ if [ -z "\${JUNIOR_GIT_COAUTHOR_NAME:-}" ] || [ -z "\${JUNIOR_GIT_COAUTHOR_EMAIL
|
|
|
930
968
|
exit 1
|
|
931
969
|
fi
|
|
932
970
|
|
|
933
|
-
|
|
934
|
-
|
|
971
|
+
# Git and GitHub only interpret the final contiguous paragraph as the trailer
|
|
972
|
+
# block, so all missing trailers are collected and appended as one block:
|
|
973
|
+
# actor trailers in order, Junior's agent trailer last.
|
|
974
|
+
desired_trailers=""
|
|
975
|
+
add_trailer() {
|
|
976
|
+
desired_trailers="$desired_trailers$1"$'\\n'
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if [ -n "\${JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS:-}" ]; then
|
|
980
|
+
while IFS= read -r actor_trailer; do
|
|
981
|
+
if [ -n "$actor_trailer" ]; then
|
|
982
|
+
add_trailer "$actor_trailer"
|
|
983
|
+
fi
|
|
984
|
+
done <<< "$JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS"
|
|
985
|
+
fi
|
|
986
|
+
|
|
987
|
+
add_trailer "Co-Authored-By: $JUNIOR_GIT_COAUTHOR_NAME <$JUNIOR_GIT_COAUTHOR_EMAIL>"
|
|
988
|
+
|
|
989
|
+
final_trailer_block=$(awk '
|
|
990
|
+
{ lines[NR] = $0 }
|
|
991
|
+
END {
|
|
992
|
+
line = NR
|
|
993
|
+
while (line > 0 && lines[line] == "") {
|
|
994
|
+
line--
|
|
995
|
+
}
|
|
996
|
+
if (line == 0) {
|
|
997
|
+
exit 0
|
|
998
|
+
}
|
|
999
|
+
start = line
|
|
1000
|
+
while (start > 0 && lines[start] != "") {
|
|
1001
|
+
start--
|
|
1002
|
+
}
|
|
1003
|
+
for (i = start + 1; i <= line; i++) {
|
|
1004
|
+
if (lines[i] !~ /^[[:alnum:]-]+: .+/) {
|
|
1005
|
+
exit 0
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
for (i = start + 1; i <= line; i++) {
|
|
1009
|
+
print lines[i]
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
' "$message_file")
|
|
1013
|
+
|
|
1014
|
+
missing_trailers=""
|
|
1015
|
+
collect_missing_trailer() {
|
|
1016
|
+
if ! printf '%s\\n' "$final_trailer_block" | grep -Fqx -- "$1"; then
|
|
1017
|
+
missing_trailers="\${missing_trailers}\${1}"$'\\n'
|
|
1018
|
+
fi
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
while IFS= read -r desired_trailer; do
|
|
1022
|
+
if [ -n "$desired_trailer" ]; then
|
|
1023
|
+
collect_missing_trailer "$desired_trailer"
|
|
1024
|
+
fi
|
|
1025
|
+
done <<< "$desired_trailers"
|
|
1026
|
+
|
|
1027
|
+
if [ -z "$missing_trailers" ]; then
|
|
935
1028
|
exit 0
|
|
936
1029
|
fi
|
|
937
1030
|
|
|
938
|
-
|
|
1031
|
+
if [ -n "$(tail -c 1 "$message_file")" ]; then
|
|
1032
|
+
printf '\\n' >> "$message_file"
|
|
1033
|
+
fi
|
|
1034
|
+
|
|
1035
|
+
# When the message already ends with Junior's bot trailer, insert missing
|
|
1036
|
+
# actor trailers before it so the bot trailer stays last.
|
|
1037
|
+
last_line=$(tail -n 1 "$message_file")
|
|
1038
|
+
bot_trailer="Co-Authored-By: $JUNIOR_GIT_COAUTHOR_NAME <$JUNIOR_GIT_COAUTHOR_EMAIL>"
|
|
1039
|
+
if [ "$last_line" = "$bot_trailer" ]; then
|
|
1040
|
+
tmp_file=$(mktemp)
|
|
1041
|
+
trap 'rm -f "$tmp_file"' EXIT
|
|
1042
|
+
sed '$d' "$message_file" > "$tmp_file"
|
|
1043
|
+
printf '%s' "$missing_trailers" >> "$tmp_file"
|
|
1044
|
+
printf '%s\\n' "$bot_trailer" >> "$tmp_file"
|
|
1045
|
+
cat "$tmp_file" > "$message_file"
|
|
1046
|
+
elif [ -n "$final_trailer_block" ]; then
|
|
1047
|
+
printf '%s' "$missing_trailers" >> "$message_file"
|
|
1048
|
+
else
|
|
1049
|
+
printf '\\n%s' "$missing_trailers" >> "$message_file"
|
|
1050
|
+
fi
|
|
939
1051
|
`;
|
|
940
1052
|
}
|
|
941
1053
|
async function configureGit(ctx, key, value) {
|
|
@@ -1816,26 +1928,13 @@ function githubPlugin(options = {}) {
|
|
|
1816
1928
|
if (ctx.tool.name !== "bash") {
|
|
1817
1929
|
return;
|
|
1818
1930
|
}
|
|
1819
|
-
const command = typeof ctx.tool.input === "object" && ctx.tool.input && "command" in ctx.tool.input ? String(ctx.tool.input.command ?? "") : "";
|
|
1820
1931
|
const botName = readEnv(botNameEnv);
|
|
1821
1932
|
const botEmail = readEnv(botEmailEnv);
|
|
1822
|
-
if ((!botName || !botEmail) && isGitCommitCommand(command)) {
|
|
1823
|
-
ctx.decision.deny(
|
|
1824
|
-
`Junior GitHub plugin is misconfigured: host env vars ${botNameEnv} and ${botEmailEnv} are missing. This is an internal deployment configuration error; do not set them in the sandbox.`
|
|
1825
|
-
);
|
|
1826
|
-
return;
|
|
1827
|
-
}
|
|
1828
1933
|
if (!botName || !botEmail) {
|
|
1829
1934
|
return;
|
|
1830
1935
|
}
|
|
1831
|
-
const authorName =
|
|
1832
|
-
const authorEmail =
|
|
1833
|
-
if ((!authorName || !authorEmail) && isGitCommitCommand(command)) {
|
|
1834
|
-
ctx.decision.deny(
|
|
1835
|
-
"Junior GitHub plugin could not determine a resolved requester name and email for commit attribution. This is an internal request-context error; do not set author env vars manually."
|
|
1836
|
-
);
|
|
1837
|
-
return;
|
|
1838
|
-
}
|
|
1936
|
+
const authorName = actorName(ctx.actor);
|
|
1937
|
+
const authorEmail = actorEmail(ctx.actor);
|
|
1839
1938
|
if (authorName && authorEmail) {
|
|
1840
1939
|
ctx.env.set("GIT_AUTHOR_NAME", authorName);
|
|
1841
1940
|
ctx.env.set("GIT_AUTHOR_EMAIL", authorEmail);
|
|
@@ -1846,6 +1945,16 @@ function githubPlugin(options = {}) {
|
|
|
1846
1945
|
ctx.env.set("GIT_COMMITTER_EMAIL", botEmail);
|
|
1847
1946
|
ctx.env.set("JUNIOR_GIT_COAUTHOR_NAME", botName);
|
|
1848
1947
|
ctx.env.set("JUNIOR_GIT_COAUTHOR_EMAIL", botEmail);
|
|
1948
|
+
const actorTrailers = additionalActorCoauthorTrailers({
|
|
1949
|
+
actors: ctx.actors,
|
|
1950
|
+
authorEmail,
|
|
1951
|
+
botEmail,
|
|
1952
|
+
runActor: ctx.actor
|
|
1953
|
+
});
|
|
1954
|
+
ctx.env.set(
|
|
1955
|
+
"JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS",
|
|
1956
|
+
actorTrailers.join("\n")
|
|
1957
|
+
);
|
|
1849
1958
|
},
|
|
1850
1959
|
grantForEgress(ctx) {
|
|
1851
1960
|
return githubGrantForEgress(ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.92.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@sinclair/typebox": "^0.34.49",
|
|
27
27
|
"zod": "^4.4.3",
|
|
28
|
-
"@sentry/junior-plugin-api": "0.
|
|
28
|
+
"@sentry/junior-plugin-api": "0.92.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^25.9.1",
|
|
@@ -62,7 +62,7 @@ jr-rpc config set github.repo owner/repo
|
|
|
62
62
|
|
|
63
63
|
- Prefer `--json` output for machine-readable parsing where available.
|
|
64
64
|
- Pass extra `git clone` flags after `--` (e.g. `gh repo clone owner/repo -- --depth=1`).
|
|
65
|
-
- A local `git commit` does not call GitHub. Pushing that commit does: `git push` requires `github.contents.write` on the target repo and
|
|
65
|
+
- A local `git commit` does not call GitHub. Pushing that commit does: `git push` requires `github.contents.write` on the target repo and actor write access.
|
|
66
66
|
- REST Git commit construction also requires `github.contents.write`: `POST /git/blobs`, `POST /git/trees`, `POST /git/commits`, `POST /git/refs`, and `PATCH /git/refs/{ref}`.
|
|
67
67
|
- If the commit changes workflow files under `.github/workflows`, expect `github.workflows.write` in addition to contents write.
|
|
68
68
|
- Before `github_createPullRequest`, push the head branch explicitly and resolve the target repo's default branch for `base`. That push requires GitHub write access to the remote.
|