@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 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 requester 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 requester, 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 requester as the commit author, Junior as committer, and a Junior `Co-Authored-By` trailer.
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 requester as author and the GitHub App bot as committer in the sandbox.
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 requesterDisplayName(value, requester) {
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(requester?.userId)) {
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 requesterName(requester) {
898
- return requesterDisplayName(requester?.fullName, requester) || requesterDisplayName(requester?.userName, requester) || void 0;
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 requesterEmail(requester) {
901
- const email = cleanIdentityPart(requester?.email);
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 isGitCommitCommand(command) {
905
- return /(?:^|[\s;|&])git(?:\s+(?:-C\s+\S+|-c\s+\S+|--git-dir(?:=\S+|\s+\S+)|--work-tree(?:=\S+|\s+\S+)|--namespace(?:=\S+|\s+\S+)))*\s+commit(?:\s|$)/.test(
906
- command
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: requester commit attribution was not injected by the host runtime. Do not set Git author env vars manually; report this configuration error." >&2
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 requester identity. Do not override Git author manually; report this configuration error." >&2
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
- trailer="Co-Authored-By: $JUNIOR_GIT_COAUTHOR_NAME <$JUNIOR_GIT_COAUTHOR_EMAIL>"
934
- if grep -Fqx "$trailer" "$message_file"; then
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
- printf '\\n%s\\n' "$trailer" >> "$message_file"
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 = requesterName(ctx.requester);
1832
- const authorEmail = requesterEmail(ctx.requester);
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.91.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.91.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 requester write access.
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.