@sentry/junior-github 0.93.0 → 0.94.0
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 +26 -26
- package/dist/index.js +315 -87
- package/dist/tool-support/attribution.d.ts +5 -0
- package/package.json +2 -2
- package/skills/github-code/SKILL.md +6 -4
- package/skills/github-code/references/api-surface.md +18 -20
- package/skills/github-code/references/troubleshooting-workarounds.md +2 -2
- package/skills/github-issues/SKILL.md +1 -1
- package/skills/github-issues/references/api-surface.md +11 -11
- package/skills/github-issues/references/troubleshooting-workarounds.md +4 -4
package/SETUP.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GitHub plugin setup
|
|
2
2
|
|
|
3
|
-
This plugin exposes two skills — `github-code` (clone, source-code investigation, pull requests) and `github-issues` (issue workflows).
|
|
3
|
+
This plugin exposes two skills — `github-code` (clone, source-code investigation, pull requests) and `github-issues` (issue workflows). Junior uses GitHub App installation tokens for reads, allowlisted issue and pull request writes, and Git branch pushes. Human OAuth is reserved for explicitly personal operations such as pull request reviews.
|
|
4
4
|
|
|
5
5
|
## 1) Create/install GitHub App
|
|
6
6
|
|
|
@@ -83,7 +83,7 @@ githubPlugin({
|
|
|
83
83
|
|
|
84
84
|
Junior records these permissions as plugin capabilities. The configured values are the maximum GitHub App envelope Junior may need for writes. Installation-read token requests remain read-only by requesting read-capable configured permissions at `read` level and omitting GitHub permission fields that have no `read` value. GitHub remains the source of truth for whether a permission name or level exists.
|
|
85
85
|
|
|
86
|
-
GitHub App user-to-server tokens do not use OAuth scopes as their permission model. Their effective access is limited by the GitHub App's installed permissions, the app installation's repository access, and the requesting user's own GitHub access. GitHub returns an empty `scope` value for
|
|
86
|
+
GitHub App user-to-server tokens do not use OAuth scopes as their permission model. Their effective access is limited by the GitHub App's installed permissions, the app installation's repository access, and the requesting user's own GitHub access. Repository-scoped installation tokens instead use the App permission envelope and installation repository access without borrowing the requesting user's authority. GitHub returns an empty `scope` value for user-to-server tokens, so Junior cannot verify granted scopes from the token response.
|
|
87
87
|
|
|
88
88
|
If you pass `additionalUserScopes`, Junior includes those values in the authorization URL and records the requested scope string as a local reauthorization contract. This does not expand or prove GitHub API permissions — configure GitHub App permissions with `appPermissions` and in the GitHub App settings for provider-enforced access:
|
|
89
89
|
|
|
@@ -93,15 +93,16 @@ githubPlugin({
|
|
|
93
93
|
});
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
Use `additionalUserScopes` only when
|
|
96
|
+
Use `additionalUserScopes` only when a human-identity integration flow requires specific GitHub OAuth scope parameters in the authorization URL. Do not rely on it to authorize Junior-owned repository or workflow writes — those use repository-scoped installation tokens and the configured GitHub App permissions.
|
|
97
97
|
|
|
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.
|
|
102
|
-
- `user-read` and `user-write` require the actor, or an explicitly delegated user subject
|
|
103
|
-
-
|
|
104
|
-
-
|
|
101
|
+
- The plugin classifies GitHub traffic from the forwarded HTTP request. Reads use `installation-read`, while `GET /user` uses `user-read`. Allowlisted issue and pull request mutations use repository-scoped installation grants. Git smart-HTTP pushes use `installation-pr-branch-write`. Unknown REST writes and GraphQL mutations are denied.
|
|
102
|
+
- `user-read` and the remaining explicitly human `user-write` operations require the actor, or an explicitly delegated user subject, to authorize the GitHub App through the private OAuth flow. Junior-owned issue, pull request, and branch operations do not fall back to user OAuth.
|
|
103
|
+
- Headless resource-event turns use the `resource-event` system actor and may receive the same repository-scoped installation grants. This lets Junior respond to subscribed pull request events by committing and pushing fixes without inheriting a subscriber's OAuth credential.
|
|
104
|
+
- Git commits use Junior as author and committer. Resolvable human run actors are credited once with `Co-Authored-By` trailers.
|
|
105
|
+
- Issued credentials are reused only within the current turn, credential leases are cached by plugin grant and repository lease scope, and upstream 403 permission denials clear the cached lease before the next retry.
|
|
105
106
|
- Sandbox does not receive raw tokens via env; host applies Authorization header transforms for GitHub API calls.
|
|
106
107
|
|
|
107
108
|
## 4) CLI usage
|
|
@@ -121,33 +122,32 @@ git -C repo fetch --depth=50 origin
|
|
|
121
122
|
git -C repo fetch --unshallow
|
|
122
123
|
```
|
|
123
124
|
|
|
124
|
-
Load the relevant GitHub skill for command guidance and repo context
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
gh
|
|
128
|
-
```
|
|
125
|
+
Load the relevant GitHub skill for command guidance and repo context. Create
|
|
126
|
+
new resources through the typed tools, for example
|
|
127
|
+
`github_createIssue({ repo: "owner/repo", title: "Example issue", body: "..." })`.
|
|
128
|
+
Use `gh` for the allowlisted lifecycle operations described by the skill.
|
|
129
129
|
|
|
130
130
|
`gh` supports either direct `GITHUB_TOKEN` (for local debugging) or sandbox-level header injection.
|
|
131
|
-
The plugin uses
|
|
131
|
+
The plugin uses installation credentials for read-only GitHub traffic, allowlisted issue and pull request mutations, and Git smart-HTTP pushes. GitHub App permissions still need to cover the operation: issues for issue edits/comments/labels, contents for pushes, pull requests for PR mutations, and workflows for workflow-file pushes.
|
|
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
|
|
136
|
-
- Pushing a branch with Git smart HTTP (`git push`) uses the `
|
|
137
|
-
-
|
|
135
|
+
- Creating the local Git commit does not call GitHub. Junior sets the GitHub App bot as author and committer and credits resolvable human actors with `Co-Authored-By` trailers.
|
|
136
|
+
- Pushing a branch with Git smart HTTP (`git push`) uses the repository-scoped `installation-pr-branch-write` grant and requires `Contents: write`. If `Workflows: write` is configured, it is included so workflow-file changes can be pushed.
|
|
137
|
+
- The smart-HTTP classifier does not distinguish Junior-managed branches or independently detect force updates or ref deletion. Use GitHub branch protection and limit the App installation to repositories where Junior may push.
|
|
138
|
+
- REST Git database and ref writes are denied by the current write allowlist. Use Git smart HTTP (`git push`) for branch updates instead.
|
|
138
139
|
- 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.
|
|
139
140
|
|
|
140
|
-
Fork creation is not part of the default PR path
|
|
141
|
+
Fork creation is not part of the default PR path and is denied by the current write allowlist. Do not grant `Administration: write` for routine PR creation; push a branch explicitly and create the PR with `github_createPullRequest` instead.
|
|
141
142
|
|
|
142
143
|
GitHub App permission scoping is a safety rail, not a hard sandbox boundary. It helps prevent accidental write scope and wrong-repo mutations, and the host runtime still decides when to mint credentials. Credential injection is provider-domain scoped for sandbox traffic to `api.github.com` and `github.com` during turns with a signed credential context. Keep repo context explicit, and let the plugin choose the required grant for the outbound request.
|
|
143
144
|
|
|
144
|
-
Be careful with mixed-surface PR commands
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
- `gh pr close --delete-branch` needs `github.contents.write`.
|
|
145
|
+
Be careful with mixed-surface PR commands. Use the allowlisted REST endpoints
|
|
146
|
+
rather than GraphQL-backed `gh pr` mutation commands. PR-native title, body,
|
|
147
|
+
base, state, ready-for-review, and requested-reviewer changes use the pull
|
|
148
|
+
request grant. Comments, labels, and assignees use GitHub's issue endpoints and
|
|
149
|
+
the issue grant. Merge, branch deletion, REST ref mutation, and administration
|
|
150
|
+
remain unsupported API operations.
|
|
151
151
|
|
|
152
152
|
For PR creation in automation, push explicitly and pass that branch as `head`:
|
|
153
153
|
|
|
@@ -192,10 +192,10 @@ jr-rpc config set github.repo getsentry/junior
|
|
|
192
192
|
2. Confirm the GitHub App is installed on your test repo with the permissions above.
|
|
193
193
|
3. Deploy `main` to prod.
|
|
194
194
|
4. Exercise `github-issues` to create an issue in a safe test repo.
|
|
195
|
-
5. Verify the issue is authored by the
|
|
195
|
+
5. Verify the issue is authored by the GitHub App bot and includes `Requested by` attribution.
|
|
196
196
|
6. Exercise `github-issues` to update title/body, add/remove labels, and add a comment.
|
|
197
197
|
7. Push a test branch and exercise `github-code` to create a draft PR using explicit repo targeting and `--head`.
|
|
198
|
-
8. Verify
|
|
198
|
+
8. Verify allowlisted mutations are attributed to the GitHub App bot, while a pull request review still requires delegated human authorization.
|
|
199
199
|
9. Verify GitHub API calls succeed while this skill is active without writing tokens into sandbox env/files.
|
|
200
200
|
10. Verify raw token values are never printed in output or logs.
|
|
201
201
|
11. Check logs for:
|
package/dist/index.js
CHANGED
|
@@ -150,6 +150,43 @@ function appendGitHubFooter(body, conversationId) {
|
|
|
150
150
|
${footer}` : footer;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// src/tool-support/attribution.ts
|
|
154
|
+
var GITHUB_REQUEST_ATTRIBUTION_START = "<!-- junior-request-attribution:start -->";
|
|
155
|
+
var GITHUB_REQUEST_ATTRIBUTION_END = "<!-- junior-request-attribution:end -->";
|
|
156
|
+
function cleanDisplayValue(value) {
|
|
157
|
+
const cleaned = value?.replace(/[\r\n<>]/g, " ").trim();
|
|
158
|
+
return cleaned || void 0;
|
|
159
|
+
}
|
|
160
|
+
function actorLabel(actor) {
|
|
161
|
+
if (!actor) {
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
if (actor.platform === "system") {
|
|
165
|
+
return `Junior system actor \`${actor.name}\``;
|
|
166
|
+
}
|
|
167
|
+
const display = cleanDisplayValue(actor.fullName) ?? cleanDisplayValue(actor.userName) ?? cleanDisplayValue(actor.userId);
|
|
168
|
+
return display ? `**${display.replaceAll("*", "\\*")}**` : void 0;
|
|
169
|
+
}
|
|
170
|
+
function appendGitHubRequesterAttribution(body, actor) {
|
|
171
|
+
const label = actorLabel(actor);
|
|
172
|
+
const attribution = label ? `${GITHUB_REQUEST_ATTRIBUTION_START}
|
|
173
|
+
Requested by ${label} via Junior.
|
|
174
|
+
${GITHUB_REQUEST_ATTRIBUTION_END}` : void 0;
|
|
175
|
+
const normalizedBody = body.trimEnd();
|
|
176
|
+
const existing = new RegExp(
|
|
177
|
+
`${GITHUB_REQUEST_ATTRIBUTION_START}[\\s\\S]*?${GITHUB_REQUEST_ATTRIBUTION_END}`
|
|
178
|
+
);
|
|
179
|
+
if (existing.test(normalizedBody)) {
|
|
180
|
+
return attribution ? normalizedBody.replace(existing, attribution) : normalizedBody.replace(existing, "").trimEnd();
|
|
181
|
+
}
|
|
182
|
+
if (!attribution) {
|
|
183
|
+
return normalizedBody;
|
|
184
|
+
}
|
|
185
|
+
return normalizedBody ? `${normalizedBody}
|
|
186
|
+
|
|
187
|
+
${attribution}` : attribution;
|
|
188
|
+
}
|
|
189
|
+
|
|
153
190
|
// src/tools/create-issue.ts
|
|
154
191
|
var GITHUB_ISSUE_CREATE_IDEMPOTENCY_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
155
192
|
var GITHUB_ISSUE_CREATE_LOCK_TTL_MS = 6e4;
|
|
@@ -297,14 +334,17 @@ function isDefinitiveGitHubIssueCreateRejection(error) {
|
|
|
297
334
|
}
|
|
298
335
|
return [400, 401, 404, 410, 422].includes(error.status);
|
|
299
336
|
}
|
|
300
|
-
function createGitHubIssueRequest(conversationId, input) {
|
|
337
|
+
function createGitHubIssueRequest(conversationId, input, actor) {
|
|
301
338
|
const repo = parseRepo(input.repo);
|
|
302
339
|
const labels = input.labels?.map(
|
|
303
340
|
(label) => nonEmptyString2(label, "labels entry")
|
|
304
341
|
);
|
|
305
342
|
const payload = {
|
|
306
343
|
title: nonEmptyString2(input.title, "title"),
|
|
307
|
-
body: appendGitHubFooter(
|
|
344
|
+
body: appendGitHubFooter(
|
|
345
|
+
appendGitHubRequesterAttribution(input.body ?? "", actor),
|
|
346
|
+
conversationId
|
|
347
|
+
),
|
|
308
348
|
...labels?.length ? { labels } : {}
|
|
309
349
|
};
|
|
310
350
|
return new Request(
|
|
@@ -378,7 +418,11 @@ function createGitHubIssueTool(ctx) {
|
|
|
378
418
|
"GitHub issue creation for this tool call has an uncertain pending result; refusing to create a duplicate issue."
|
|
379
419
|
);
|
|
380
420
|
}
|
|
381
|
-
const request = createGitHubIssueRequest(
|
|
421
|
+
const request = createGitHubIssueRequest(
|
|
422
|
+
conversationId,
|
|
423
|
+
parsedInput,
|
|
424
|
+
ctx.actor
|
|
425
|
+
);
|
|
382
426
|
const pendingState = {
|
|
383
427
|
status: "pending",
|
|
384
428
|
createdAtMs: Date.now()
|
|
@@ -601,13 +645,16 @@ function isDefinitiveGitHubPullRequestCreateRejection(error) {
|
|
|
601
645
|
}
|
|
602
646
|
return [400, 401, 404, 410, 422].includes(error.status);
|
|
603
647
|
}
|
|
604
|
-
function createGitHubPullRequestRequest(conversationId, input) {
|
|
648
|
+
function createGitHubPullRequestRequest(conversationId, input, actor) {
|
|
605
649
|
const repo = parseRepo2(input.repo);
|
|
606
650
|
const payload = {
|
|
607
651
|
title: nonEmptyString3(input.title, "title"),
|
|
608
652
|
head: nonEmptyString3(input.head, "head"),
|
|
609
653
|
base: nonEmptyString3(input.base, "base"),
|
|
610
|
-
body: appendGitHubFooter(
|
|
654
|
+
body: appendGitHubFooter(
|
|
655
|
+
appendGitHubRequesterAttribution(input.body ?? "", actor),
|
|
656
|
+
conversationId
|
|
657
|
+
),
|
|
611
658
|
...input.draft !== void 0 ? { draft: input.draft } : {}
|
|
612
659
|
};
|
|
613
660
|
return new Request(
|
|
@@ -740,7 +787,8 @@ function createGitHubPullRequestTool(ctx) {
|
|
|
740
787
|
}
|
|
741
788
|
const request = createGitHubPullRequestRequest(
|
|
742
789
|
conversationId,
|
|
743
|
-
parsedInput
|
|
790
|
+
parsedInput,
|
|
791
|
+
ctx.actor
|
|
744
792
|
);
|
|
745
793
|
const pendingState = {
|
|
746
794
|
status: "pending",
|
|
@@ -801,25 +849,17 @@ var GITHUB_GRAPHQL_RESPONSE_BODY_LIMIT_BYTES = 64 * 1024;
|
|
|
801
849
|
var HTTP_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
802
850
|
var USER_TOKEN_GRANTS = /* @__PURE__ */ new Set(["user-read", "user-write"]);
|
|
803
851
|
var CONTENTS_WRITE_REQUIREMENTS = [
|
|
804
|
-
"GitHub App Contents: write on the target repository"
|
|
805
|
-
"requesting GitHub user write access to the repository"
|
|
806
|
-
];
|
|
807
|
-
var WORKFLOWS_WRITE_REQUIREMENTS = [
|
|
808
|
-
"GitHub App Contents: write and Workflows: write on the target repository",
|
|
809
|
-
"requesting GitHub user write access to the repository"
|
|
852
|
+
"GitHub App Contents: write on the target repository"
|
|
810
853
|
];
|
|
811
854
|
var ISSUES_WRITE_REQUIREMENTS = [
|
|
812
|
-
"GitHub App Issues: write on the target repository"
|
|
813
|
-
"requesting GitHub user issue access to the repository"
|
|
855
|
+
"GitHub App Issues: write on the target repository"
|
|
814
856
|
];
|
|
815
857
|
var PULL_REQUESTS_WRITE_REQUIREMENTS = [
|
|
816
|
-
"GitHub App Pull requests: write on the target repository"
|
|
817
|
-
"requesting GitHub user write access to the repository"
|
|
858
|
+
"GitHub App Pull requests: write on the target repository"
|
|
818
859
|
];
|
|
819
|
-
var
|
|
820
|
-
|
|
821
|
-
"
|
|
822
|
-
"requesting GitHub user permission to fork the repository"
|
|
860
|
+
var PULL_REVIEW_WRITE_REQUIREMENTS = [
|
|
861
|
+
...PULL_REQUESTS_WRITE_REQUIREMENTS,
|
|
862
|
+
"requesting GitHub user permission to review the pull request"
|
|
823
863
|
];
|
|
824
864
|
var GitHubUserRefreshRejectedError = class extends Error {
|
|
825
865
|
constructor(message) {
|
|
@@ -920,14 +960,12 @@ function additionalActorCoauthorTrailers(args) {
|
|
|
920
960
|
if (!args.actors || args.actors.length === 0) {
|
|
921
961
|
return [];
|
|
922
962
|
}
|
|
923
|
-
const runActorKey = args.runActor ? actorIdentityKey(args.runActor) : void 0;
|
|
924
963
|
const seenEmails = /* @__PURE__ */ new Set([args.botEmail.toLowerCase()]);
|
|
925
|
-
|
|
926
|
-
seenEmails.add(args.authorEmail.toLowerCase());
|
|
927
|
-
}
|
|
964
|
+
const seenActors = /* @__PURE__ */ new Set();
|
|
928
965
|
const trailers = [];
|
|
929
966
|
for (const candidate of args.actors) {
|
|
930
|
-
|
|
967
|
+
const actorKey = actorIdentityKey(candidate);
|
|
968
|
+
if (seenActors.has(actorKey)) {
|
|
931
969
|
continue;
|
|
932
970
|
}
|
|
933
971
|
const name = actorName(candidate);
|
|
@@ -939,6 +977,7 @@ function additionalActorCoauthorTrailers(args) {
|
|
|
939
977
|
if (seenEmails.has(emailKey)) {
|
|
940
978
|
continue;
|
|
941
979
|
}
|
|
980
|
+
seenActors.add(actorKey);
|
|
942
981
|
seenEmails.add(emailKey);
|
|
943
982
|
trailers.push(`Co-Authored-By: ${name} <${email}>`);
|
|
944
983
|
}
|
|
@@ -954,23 +993,18 @@ if [ -z "$message_file" ]; then
|
|
|
954
993
|
fi
|
|
955
994
|
|
|
956
995
|
if [ -z "\${JUNIOR_GIT_AUTHOR_NAME:-}" ] || [ -z "\${JUNIOR_GIT_AUTHOR_EMAIL:-}" ]; then
|
|
957
|
-
echo "Junior GitHub plugin internal error:
|
|
996
|
+
echo "Junior GitHub plugin internal error: Junior author identity was not injected by the host runtime. Do not set Git author env vars manually; report this configuration error." >&2
|
|
958
997
|
exit 1
|
|
959
998
|
fi
|
|
960
999
|
|
|
961
1000
|
if [ "\${GIT_AUTHOR_NAME:-}" != "$JUNIOR_GIT_AUTHOR_NAME" ] || [ "\${GIT_AUTHOR_EMAIL:-}" != "$JUNIOR_GIT_AUTHOR_EMAIL" ]; then
|
|
962
|
-
echo "Junior GitHub plugin internal error: Git author was not set to the
|
|
963
|
-
exit 1
|
|
964
|
-
fi
|
|
965
|
-
|
|
966
|
-
if [ -z "\${JUNIOR_GIT_COAUTHOR_NAME:-}" ] || [ -z "\${JUNIOR_GIT_COAUTHOR_EMAIL:-}" ]; then
|
|
967
|
-
echo "Junior GitHub plugin internal error: Junior coauthor identity was not injected by the host runtime. Do not set coauthor env vars manually; report this configuration error." >&2
|
|
1001
|
+
echo "Junior GitHub plugin internal error: Git author was not set to the Junior identity. Do not override Git author manually; report this configuration error." >&2
|
|
968
1002
|
exit 1
|
|
969
1003
|
fi
|
|
970
1004
|
|
|
971
1005
|
# Git and GitHub only interpret the final contiguous paragraph as the trailer
|
|
972
1006
|
# block, so all missing trailers are collected and appended as one block:
|
|
973
|
-
# actor trailers in order
|
|
1007
|
+
# human actor trailers in run order.
|
|
974
1008
|
desired_trailers=""
|
|
975
1009
|
add_trailer() {
|
|
976
1010
|
desired_trailers="$desired_trailers$1"$'\\n'
|
|
@@ -984,8 +1018,6 @@ if [ -n "\${JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS:-}" ]; then
|
|
|
984
1018
|
done <<< "$JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS"
|
|
985
1019
|
fi
|
|
986
1020
|
|
|
987
|
-
add_trailer "Co-Authored-By: $JUNIOR_GIT_COAUTHOR_NAME <$JUNIOR_GIT_COAUTHOR_EMAIL>"
|
|
988
|
-
|
|
989
1021
|
final_trailer_block=$(awk '
|
|
990
1022
|
{ lines[NR] = $0 }
|
|
991
1023
|
END {
|
|
@@ -1011,6 +1043,57 @@ final_trailer_block=$(awk '
|
|
|
1011
1043
|
}
|
|
1012
1044
|
' "$message_file")
|
|
1013
1045
|
|
|
1046
|
+
duplicate_desired_trailer=false
|
|
1047
|
+
while IFS= read -r desired_trailer; do
|
|
1048
|
+
if [ -n "$desired_trailer" ] && [ "$(printf '%s\\n' "$final_trailer_block" | grep -Fxc -- "$desired_trailer")" -gt 1 ]; then
|
|
1049
|
+
duplicate_desired_trailer=true
|
|
1050
|
+
break
|
|
1051
|
+
fi
|
|
1052
|
+
done <<< "$desired_trailers"
|
|
1053
|
+
|
|
1054
|
+
if [ "$duplicate_desired_trailer" = true ]; then
|
|
1055
|
+
tmp_file=$(mktemp)
|
|
1056
|
+
desired_file=$(mktemp)
|
|
1057
|
+
trap 'rm -f "$tmp_file" "$desired_file"' EXIT
|
|
1058
|
+
printf '%s' "$desired_trailers" > "$desired_file"
|
|
1059
|
+
awk -v desired_file="$desired_file" '
|
|
1060
|
+
BEGIN {
|
|
1061
|
+
while ((getline value < desired_file) > 0) {
|
|
1062
|
+
if (value != "") {
|
|
1063
|
+
wanted[value] = 1
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
close(desired_file)
|
|
1067
|
+
}
|
|
1068
|
+
{ lines[NR] = $0 }
|
|
1069
|
+
END {
|
|
1070
|
+
line = NR
|
|
1071
|
+
while (line > 0 && lines[line] == "") {
|
|
1072
|
+
line--
|
|
1073
|
+
}
|
|
1074
|
+
start = line
|
|
1075
|
+
while (start > 0 && lines[start] != "") {
|
|
1076
|
+
start--
|
|
1077
|
+
}
|
|
1078
|
+
valid = line > 0
|
|
1079
|
+
for (i = start + 1; valid && i <= line; i++) {
|
|
1080
|
+
if (lines[i] !~ /^[[:alnum:]-]+: .+/) {
|
|
1081
|
+
valid = 0
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
for (i = 1; i <= NR; i++) {
|
|
1085
|
+
if (valid && i > start && i <= line && wanted[lines[i]]) {
|
|
1086
|
+
if (seen[lines[i]]++) {
|
|
1087
|
+
continue
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
print lines[i]
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
' "$message_file" > "$tmp_file"
|
|
1094
|
+
cat "$tmp_file" > "$message_file"
|
|
1095
|
+
fi
|
|
1096
|
+
|
|
1014
1097
|
missing_trailers=""
|
|
1015
1098
|
collect_missing_trailer() {
|
|
1016
1099
|
if ! printf '%s\\n' "$final_trailer_block" | grep -Fqx -- "$1"; then
|
|
@@ -1032,18 +1115,7 @@ if [ -n "$(tail -c 1 "$message_file")" ]; then
|
|
|
1032
1115
|
printf '\\n' >> "$message_file"
|
|
1033
1116
|
fi
|
|
1034
1117
|
|
|
1035
|
-
|
|
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
|
|
1118
|
+
if [ -n "$final_trailer_block" ]; then
|
|
1047
1119
|
printf '%s' "$missing_trailers" >> "$message_file"
|
|
1048
1120
|
else
|
|
1049
1121
|
printf '\\n%s' "$missing_trailers" >> "$message_file"
|
|
@@ -1298,6 +1370,41 @@ function readInstallationPermissions(installation) {
|
|
|
1298
1370
|
}
|
|
1299
1371
|
return readGrantPermissions(installation.permissions);
|
|
1300
1372
|
}
|
|
1373
|
+
function decodeGitHubPathSegment(value) {
|
|
1374
|
+
try {
|
|
1375
|
+
const decoded = decodeURIComponent(value).trim();
|
|
1376
|
+
return decoded && !decoded.includes("/") ? decoded : void 0;
|
|
1377
|
+
} catch {
|
|
1378
|
+
return void 0;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
function githubRepositoryFromUrl(upstreamUrl) {
|
|
1382
|
+
const segments = upstreamUrl.pathname.split("/").filter(Boolean);
|
|
1383
|
+
if (isGitHubApiUrl(upstreamUrl) && segments[0]?.toLowerCase() === "repos") {
|
|
1384
|
+
const owner2 = segments[1] ? decodeGitHubPathSegment(segments[1]) : void 0;
|
|
1385
|
+
const name2 = segments[2] ? decodeGitHubPathSegment(segments[2]) : void 0;
|
|
1386
|
+
return owner2 && name2 ? { owner: owner2, name: name2 } : void 0;
|
|
1387
|
+
}
|
|
1388
|
+
if (upstreamUrl.hostname.toLowerCase() !== "github.com") {
|
|
1389
|
+
return void 0;
|
|
1390
|
+
}
|
|
1391
|
+
const owner = segments[0] ? decodeGitHubPathSegment(segments[0]) : void 0;
|
|
1392
|
+
const rawName = segments[1]?.replace(/\.git$/i, "");
|
|
1393
|
+
const name = rawName ? decodeGitHubPathSegment(rawName) : void 0;
|
|
1394
|
+
return owner && name ? { owner, name } : void 0;
|
|
1395
|
+
}
|
|
1396
|
+
function githubRepositoryLeaseScope(repository) {
|
|
1397
|
+
return `repository:${repository.owner.toLowerCase()}/${repository.name.toLowerCase()}`;
|
|
1398
|
+
}
|
|
1399
|
+
function githubRepositoryFromLeaseScope(leaseScope) {
|
|
1400
|
+
const match = /^repository:([^/]+)\/([^/]+)$/.exec(leaseScope ?? "");
|
|
1401
|
+
if (!match?.[1] || !match[2]) {
|
|
1402
|
+
throw new GitHubPluginSetupError(
|
|
1403
|
+
"GitHub installation write grant is missing a repository lease scope."
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
return { owner: match[1], name: match[2] };
|
|
1407
|
+
}
|
|
1301
1408
|
async function resolveUserAccount(tokens) {
|
|
1302
1409
|
const account = await githubRequest("https://api.github.com", "/user", {
|
|
1303
1410
|
token: tokens.accessToken
|
|
@@ -1475,12 +1582,11 @@ async function issueInstallationCredential(options) {
|
|
|
1475
1582
|
throw new GitHubPluginSetupError(`Invalid ${options.installationIdEnv}`);
|
|
1476
1583
|
}
|
|
1477
1584
|
const appJwt = createAppJwt(appId, options.privateKeyEnv);
|
|
1478
|
-
|
|
1479
|
-
if (!
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
});
|
|
1585
|
+
const permissions = options.permissions ?? await options.loadPermissions?.({ appJwt, installationId });
|
|
1586
|
+
if (!permissions) {
|
|
1587
|
+
throw new GitHubPluginSetupError(
|
|
1588
|
+
"GitHub installation credential permissions are not configured."
|
|
1589
|
+
);
|
|
1484
1590
|
}
|
|
1485
1591
|
const accessTokenResponse = await githubRequest(
|
|
1486
1592
|
"https://api.github.com",
|
|
@@ -1488,7 +1594,10 @@ async function issueInstallationCredential(options) {
|
|
|
1488
1594
|
{
|
|
1489
1595
|
method: "POST",
|
|
1490
1596
|
token: appJwt,
|
|
1491
|
-
body: {
|
|
1597
|
+
body: {
|
|
1598
|
+
permissions,
|
|
1599
|
+
...options.repositories ? { repositories: options.repositories } : {}
|
|
1600
|
+
}
|
|
1492
1601
|
}
|
|
1493
1602
|
);
|
|
1494
1603
|
const parsedToken = parseInstallationTokenResponse(accessTokenResponse);
|
|
@@ -1684,12 +1793,31 @@ function githubApiWriteReason(method, upstreamUrl) {
|
|
|
1684
1793
|
if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/issues\/[^/]+\/comments$/.test(pathname)) {
|
|
1685
1794
|
return "github.issues-write";
|
|
1686
1795
|
}
|
|
1796
|
+
if (method === "PATCH" && /^\/repos\/[^/]+\/[^/]+\/issues\/[^/]+$/.test(pathname)) {
|
|
1797
|
+
return "github.issues-write";
|
|
1798
|
+
}
|
|
1799
|
+
if ((method === "POST" || method === "DELETE") && /^\/repos\/[^/]+\/[^/]+\/issues\/[^/]+\/(labels|assignees)(?:\/[^/]+)?$/.test(
|
|
1800
|
+
pathname
|
|
1801
|
+
)) {
|
|
1802
|
+
return "github.issues-write";
|
|
1803
|
+
}
|
|
1687
1804
|
if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/pulls$/.test(pathname)) {
|
|
1688
1805
|
return "github.pull-create";
|
|
1689
1806
|
}
|
|
1690
1807
|
if (method === "PATCH" && /^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+$/.test(pathname)) {
|
|
1691
1808
|
return "github.pull-requests-write";
|
|
1692
1809
|
}
|
|
1810
|
+
if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/ready_for_review$/.test(pathname)) {
|
|
1811
|
+
return "github.pull-requests-write";
|
|
1812
|
+
}
|
|
1813
|
+
if ((method === "POST" || method === "DELETE") && /^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/requested_reviewers$/.test(pathname)) {
|
|
1814
|
+
return "github.pull-requests-write";
|
|
1815
|
+
}
|
|
1816
|
+
if (/^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/reviews(?:\/[^/]+(?:\/(events|dismissals))?)?$/.test(
|
|
1817
|
+
pathname
|
|
1818
|
+
) && !HTTP_READ_METHODS.has(method)) {
|
|
1819
|
+
return "github.pull-review-write";
|
|
1820
|
+
}
|
|
1693
1821
|
if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/forks$/.test(pathname)) {
|
|
1694
1822
|
return "github.fork-create";
|
|
1695
1823
|
}
|
|
@@ -1782,29 +1910,59 @@ function grantRequirements(reason) {
|
|
|
1782
1910
|
if (reason === "github.git-write" || reason === "github.contents-write") {
|
|
1783
1911
|
return CONTENTS_WRITE_REQUIREMENTS;
|
|
1784
1912
|
}
|
|
1785
|
-
if (reason === "github.workflows-write") {
|
|
1786
|
-
return WORKFLOWS_WRITE_REQUIREMENTS;
|
|
1787
|
-
}
|
|
1788
1913
|
if (reason === "github.issue-create" || reason === "github.issues-write") {
|
|
1789
1914
|
return ISSUES_WRITE_REQUIREMENTS;
|
|
1790
1915
|
}
|
|
1916
|
+
if (reason === "github.pull-review-write") {
|
|
1917
|
+
return PULL_REVIEW_WRITE_REQUIREMENTS;
|
|
1918
|
+
}
|
|
1791
1919
|
if (reason === "github.pull-create" || reason === "github.pull-requests-write") {
|
|
1792
1920
|
return PULL_REQUESTS_WRITE_REQUIREMENTS;
|
|
1793
1921
|
}
|
|
1794
|
-
if (reason === "github.fork-create") {
|
|
1795
|
-
return FORK_CREATE_REQUIREMENTS;
|
|
1796
|
-
}
|
|
1797
1922
|
return void 0;
|
|
1798
1923
|
}
|
|
1799
|
-
function grantForAccess(access, reason, name) {
|
|
1924
|
+
function grantForAccess(access, reason, name, leaseScope) {
|
|
1800
1925
|
const requirements = grantRequirements(reason);
|
|
1801
1926
|
return {
|
|
1802
1927
|
name,
|
|
1803
1928
|
access,
|
|
1929
|
+
...leaseScope ? { leaseScope } : {},
|
|
1804
1930
|
reason,
|
|
1805
1931
|
...requirements ? { requirements } : {}
|
|
1806
1932
|
};
|
|
1807
1933
|
}
|
|
1934
|
+
function repositoryLeaseScope(upstreamUrl) {
|
|
1935
|
+
const repository = githubRepositoryFromUrl(upstreamUrl);
|
|
1936
|
+
if (!repository) {
|
|
1937
|
+
throw new EgressPolicyDenied(
|
|
1938
|
+
"GitHub write request does not identify a target repository."
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
return githubRepositoryLeaseScope(repository);
|
|
1942
|
+
}
|
|
1943
|
+
function installationGrantForWrite(reason, upstreamUrl) {
|
|
1944
|
+
const leaseScope = repositoryLeaseScope(upstreamUrl);
|
|
1945
|
+
if (reason === "github.issue-create" || reason === "github.issues-write") {
|
|
1946
|
+
return grantForAccess(
|
|
1947
|
+
"write",
|
|
1948
|
+
reason,
|
|
1949
|
+
"installation-issues-write",
|
|
1950
|
+
leaseScope
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
if (reason === "github.pull-create" || reason === "github.pull-requests-write") {
|
|
1954
|
+
return grantForAccess(
|
|
1955
|
+
"write",
|
|
1956
|
+
reason,
|
|
1957
|
+
"installation-pull-requests-write",
|
|
1958
|
+
leaseScope
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
if (reason === "github.pull-review-write") {
|
|
1962
|
+
return grantForAccess("write", reason, "user-write", leaseScope);
|
|
1963
|
+
}
|
|
1964
|
+
return void 0;
|
|
1965
|
+
}
|
|
1808
1966
|
async function githubGrantForEgress(ctx) {
|
|
1809
1967
|
const method = ctx.request.method.toUpperCase();
|
|
1810
1968
|
const upstreamUrl = new URL(ctx.request.url);
|
|
@@ -1816,10 +1974,18 @@ async function githubGrantForEgress(ctx) {
|
|
|
1816
1974
|
});
|
|
1817
1975
|
const smartHttpAccess = githubSmartHttpAccess(upstreamUrl);
|
|
1818
1976
|
if (smartHttpAccess) {
|
|
1977
|
+
if (smartHttpAccess === "write") {
|
|
1978
|
+
return grantForAccess(
|
|
1979
|
+
"write",
|
|
1980
|
+
"github.git-write",
|
|
1981
|
+
"installation-pr-branch-write",
|
|
1982
|
+
repositoryLeaseScope(upstreamUrl)
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1819
1985
|
return grantForAccess(
|
|
1820
1986
|
smartHttpAccess,
|
|
1821
|
-
|
|
1822
|
-
|
|
1987
|
+
"github.git-read",
|
|
1988
|
+
"installation-read"
|
|
1823
1989
|
);
|
|
1824
1990
|
}
|
|
1825
1991
|
const userReadReason = githubUserReadReason(method, upstreamUrl);
|
|
@@ -1828,7 +1994,13 @@ async function githubGrantForEgress(ctx) {
|
|
|
1828
1994
|
}
|
|
1829
1995
|
const writeReason = githubApiWriteReason(method, upstreamUrl);
|
|
1830
1996
|
if (writeReason) {
|
|
1831
|
-
|
|
1997
|
+
const grant = installationGrantForWrite(writeReason, upstreamUrl);
|
|
1998
|
+
if (grant) {
|
|
1999
|
+
return grant;
|
|
2000
|
+
}
|
|
2001
|
+
throw new EgressPolicyDenied(
|
|
2002
|
+
`GitHub write operation ${writeReason} is not enabled for Junior credentials.`
|
|
2003
|
+
);
|
|
1832
2004
|
}
|
|
1833
2005
|
const graphqlAccess = githubGraphqlAccess(
|
|
1834
2006
|
method,
|
|
@@ -1836,18 +2008,55 @@ async function githubGrantForEgress(ctx) {
|
|
|
1836
2008
|
ctx.request.bodyText
|
|
1837
2009
|
);
|
|
1838
2010
|
if (graphqlAccess) {
|
|
2011
|
+
if (graphqlAccess === "write") {
|
|
2012
|
+
throw new EgressPolicyDenied(
|
|
2013
|
+
"GitHub GraphQL mutations are not enabled for Junior credentials."
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
1839
2016
|
return grantForAccess(
|
|
1840
2017
|
graphqlAccess,
|
|
1841
|
-
|
|
1842
|
-
|
|
2018
|
+
"github.graphql-read",
|
|
2019
|
+
"installation-read"
|
|
1843
2020
|
);
|
|
1844
2021
|
}
|
|
1845
2022
|
const access = HTTP_READ_METHODS.has(method) ? "read" : "write";
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2023
|
+
if (access === "write") {
|
|
2024
|
+
throw new EgressPolicyDenied(
|
|
2025
|
+
"GitHub write request is not an explicitly allowed Junior operation."
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
return grantForAccess(access, "github.api-read", "installation-read");
|
|
2029
|
+
}
|
|
2030
|
+
function configuredWritePermission(appPermissions, permission) {
|
|
2031
|
+
const level = appPermissions?.[permission];
|
|
2032
|
+
if (level !== void 0 && level !== "write" && level !== "admin") {
|
|
2033
|
+
throw new GitHubPluginSetupError(
|
|
2034
|
+
`githubPlugin appPermissions.${permission} must allow write access for Junior-owned GitHub resources.`
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
return {
|
|
2038
|
+
metadata: "read",
|
|
2039
|
+
[permission]: "write"
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
function configuredBranchWritePermissions(appPermissions) {
|
|
2043
|
+
const contents = appPermissions?.contents;
|
|
2044
|
+
if (contents !== void 0 && contents !== "write" && contents !== "admin") {
|
|
2045
|
+
throw new GitHubPluginSetupError(
|
|
2046
|
+
"githubPlugin appPermissions.contents must allow write access for Junior-managed pull request branches."
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
const workflows = appPermissions?.workflows;
|
|
2050
|
+
if (workflows !== void 0 && workflows !== "write" && workflows !== "admin") {
|
|
2051
|
+
throw new GitHubPluginSetupError(
|
|
2052
|
+
"githubPlugin appPermissions.workflows must allow write access when configured."
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
return {
|
|
2056
|
+
contents: "write",
|
|
2057
|
+
metadata: "read",
|
|
2058
|
+
...workflows === "write" || workflows === "admin" ? { workflows: "write" } : {}
|
|
2059
|
+
};
|
|
1851
2060
|
}
|
|
1852
2061
|
function githubPlugin(options = {}) {
|
|
1853
2062
|
const botNameEnv = options.botNameEnv ?? "GITHUB_APP_BOT_NAME";
|
|
@@ -1933,23 +2142,15 @@ function githubPlugin(options = {}) {
|
|
|
1933
2142
|
if (!botName || !botEmail) {
|
|
1934
2143
|
return;
|
|
1935
2144
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
ctx.env.set("GIT_AUTHOR_EMAIL", authorEmail);
|
|
1941
|
-
ctx.env.set("JUNIOR_GIT_AUTHOR_NAME", authorName);
|
|
1942
|
-
ctx.env.set("JUNIOR_GIT_AUTHOR_EMAIL", authorEmail);
|
|
1943
|
-
}
|
|
2145
|
+
ctx.env.set("GIT_AUTHOR_NAME", botName);
|
|
2146
|
+
ctx.env.set("GIT_AUTHOR_EMAIL", botEmail);
|
|
2147
|
+
ctx.env.set("JUNIOR_GIT_AUTHOR_NAME", botName);
|
|
2148
|
+
ctx.env.set("JUNIOR_GIT_AUTHOR_EMAIL", botEmail);
|
|
1944
2149
|
ctx.env.set("GIT_COMMITTER_NAME", botName);
|
|
1945
2150
|
ctx.env.set("GIT_COMMITTER_EMAIL", botEmail);
|
|
1946
|
-
ctx.env.set("JUNIOR_GIT_COAUTHOR_NAME", botName);
|
|
1947
|
-
ctx.env.set("JUNIOR_GIT_COAUTHOR_EMAIL", botEmail);
|
|
1948
2151
|
const actorTrailers = additionalActorCoauthorTrailers({
|
|
1949
|
-
actors: ctx.actors,
|
|
1950
|
-
|
|
1951
|
-
botEmail,
|
|
1952
|
-
runActor: ctx.actor
|
|
2152
|
+
actors: [...ctx.actor ? [ctx.actor] : [], ...ctx.actors ?? []],
|
|
2153
|
+
botEmail
|
|
1953
2154
|
});
|
|
1954
2155
|
ctx.env.set(
|
|
1955
2156
|
"JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS",
|
|
@@ -1984,8 +2185,35 @@ function githubPlugin(options = {}) {
|
|
|
1984
2185
|
appIdEnv,
|
|
1985
2186
|
privateKeyEnv,
|
|
1986
2187
|
installationIdEnv,
|
|
1987
|
-
|
|
1988
|
-
|
|
2188
|
+
...appReadPermissions ? { permissions: appReadPermissions } : { loadPermissions: loadReadPermissions }
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
if (ctx.grant.name === "installation-issues-write" || ctx.grant.name === "installation-pull-requests-write") {
|
|
2192
|
+
const repository = githubRepositoryFromLeaseScope(
|
|
2193
|
+
ctx.grant.leaseScope
|
|
2194
|
+
);
|
|
2195
|
+
const permission = ctx.grant.name === "installation-issues-write" ? "issues" : "pull_requests";
|
|
2196
|
+
return await issueInstallationCredential({
|
|
2197
|
+
appIdEnv,
|
|
2198
|
+
privateKeyEnv,
|
|
2199
|
+
installationIdEnv,
|
|
2200
|
+
permissions: configuredWritePermission(
|
|
2201
|
+
appPermissions,
|
|
2202
|
+
permission
|
|
2203
|
+
),
|
|
2204
|
+
repositories: [repository.name]
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
if (ctx.grant.name === "installation-pr-branch-write") {
|
|
2208
|
+
const repository = githubRepositoryFromLeaseScope(
|
|
2209
|
+
ctx.grant.leaseScope
|
|
2210
|
+
);
|
|
2211
|
+
return await issueInstallationCredential({
|
|
2212
|
+
appIdEnv,
|
|
2213
|
+
privateKeyEnv,
|
|
2214
|
+
installationIdEnv,
|
|
2215
|
+
permissions: configuredBranchWritePermissions(appPermissions),
|
|
2216
|
+
repositories: [repository.name]
|
|
1989
2217
|
});
|
|
1990
2218
|
}
|
|
1991
2219
|
if (USER_TOKEN_GRANTS.has(ctx.grant.name)) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Actor } from "@sentry/junior-plugin-api";
|
|
2
|
+
export declare const GITHUB_REQUEST_ATTRIBUTION_START = "<!-- junior-request-attribution:start -->";
|
|
3
|
+
export declare const GITHUB_REQUEST_ATTRIBUTION_END = "<!-- junior-request-attribution:end -->";
|
|
4
|
+
/** Append or replace runtime-owned requester attribution in a GitHub body. */
|
|
5
|
+
export declare function appendGitHubRequesterAttribution(body: string, actor: Actor | undefined): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.94.0",
|
|
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.94.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^25.9.1",
|
|
@@ -66,6 +66,8 @@ Rules:
|
|
|
66
66
|
- After a failed fix attempt or strong correction, stop patching symptoms — re-read the evidence and restate the root cause before editing again.
|
|
67
67
|
- If the task is investigation-only, answer from evidence without editing.
|
|
68
68
|
|
|
69
|
+
**Advisor planning checkpoint:** For a non-trivial implementation — especially architecture or API changes, security-sensitive behavior, concurrency, migrations, broad cross-file edits, or work following failed attempts — call the advisor after gathering enough evidence and before editing. Provide the requirements, relevant code and output, constraints, proposed plan, alternatives considered, risks, and intended validation path. Resolve or explicitly account for material concerns before implementing.
|
|
70
|
+
|
|
69
71
|
### 3. Edit safely
|
|
70
72
|
|
|
71
73
|
- Smallest coherent change that satisfies the request.
|
|
@@ -79,8 +81,6 @@ Rules:
|
|
|
79
81
|
|
|
80
82
|
For multi-step or risky work, keep a compact checklist of intent, touched files, verification state, next step, and blockers.
|
|
81
83
|
|
|
82
|
-
For non-obvious architecture, security-sensitive, concurrency, or repeated-failure work, pause after investigation with evidence, risks, and a proposed plan before pushing ahead.
|
|
83
|
-
|
|
84
84
|
### 4. Verify before packaging
|
|
85
85
|
|
|
86
86
|
Before committing and creating a PR for code, config, or docs-as-code changes:
|
|
@@ -89,6 +89,8 @@ Before committing and creating a PR for code, config, or docs-as-code changes:
|
|
|
89
89
|
- For docs or instruction-only changes, do a content consistency review instead of claiming automated validation.
|
|
90
90
|
- If no credible check exists, say so explicitly.
|
|
91
91
|
|
|
92
|
+
**Advisor review checkpoint:** For a non-trivial implementation, call the advisor after inspecting the final diff and running the initial validation path, but before committing, opening or updating a PR, or reporting completion. Provide the diff, relevant surrounding code, requirements, validation results, known limitations, and any remaining risks. Ask specifically for correctness issues, regressions, missed cases, and testing gaps. Investigate material findings, revise as needed, and rerun affected checks before packaging.
|
|
93
|
+
|
|
92
94
|
### 5. Commit
|
|
93
95
|
|
|
94
96
|
Default format when the repo does not specify otherwise:
|
|
@@ -159,11 +161,11 @@ Before finishing, reconcile any plan or checklist stated earlier — mark items
|
|
|
159
161
|
|
|
160
162
|
**PR inspection** — read-only `gh pr` and `gh api` commands. Query both conversation comments (`--json comments`) and review comments (`gh api .../pulls/{n}/comments` and `.../reviews`).
|
|
161
163
|
|
|
162
|
-
**PR mutation** — push before create.
|
|
164
|
+
**PR mutation** — push before create. Use only the allowlisted REST endpoints in the API reference. Merge, close-with-delete, workflow dispatch, REST ref mutation, and admin operations are unsupported. Git smart HTTP does not independently prevent force updates or ref deletion; rely on GitHub rulesets and do not request destructive pushes.
|
|
163
165
|
|
|
164
166
|
## Guardrails
|
|
165
167
|
|
|
166
168
|
- Default shallow clones; deepen only when needed.
|
|
167
|
-
-
|
|
169
|
+
- Do not attempt destructive merges, force pushes, or ref deletion.
|
|
168
170
|
- Answer source questions from repo evidence, not product framing or memory.
|
|
169
171
|
- Stop and return concrete remediation on missing access or permissions.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GitHub API Surface — code & pull requests
|
|
2
2
|
|
|
3
|
-
PR creation uses Junior's `github_createPullRequest` tool.
|
|
3
|
+
PR creation uses Junior's `github_createPullRequest` tool. Supported mutations use allowlisted REST endpoints through `gh api`; generic GraphQL-backed `gh pr` mutations are not supported.
|
|
4
4
|
|
|
5
5
|
## Repo scoping
|
|
6
6
|
|
|
@@ -10,16 +10,14 @@ Treat explicit repo flags as command-targeting safety rails, not as a credential
|
|
|
10
10
|
|
|
11
11
|
## GitHub App permission guidance
|
|
12
12
|
|
|
13
|
-
| Permission capability
|
|
14
|
-
|
|
|
15
|
-
| `github.actions.read`
|
|
16
|
-
| `github.
|
|
17
|
-
| `github.contents.
|
|
18
|
-
| `github.
|
|
19
|
-
| `github.
|
|
20
|
-
| `github.pull-requests.
|
|
21
|
-
| `github.pull-requests.write` | `github_createPullRequest` after explicit push, `gh pr edit`, `gh pr close` |
|
|
22
|
-
| `github.administration.write` + `github.contents.read` | `gh repo fork`; avoid for routine PR creation |
|
|
13
|
+
| Permission capability | Commands |
|
|
14
|
+
| ---------------------------- | ------------------------------------------------------------------------------------ |
|
|
15
|
+
| `github.actions.read` | `gh run list`, `gh run view`, `gh run watch`, `gh workflow list`, `gh workflow view` |
|
|
16
|
+
| `github.contents.read` | `gh repo clone`, `git fetch` |
|
|
17
|
+
| `github.contents.write` | Git smart-HTTP `git push` only |
|
|
18
|
+
| `github.workflows.write` | Workflow-file changes carried by Git smart-HTTP push |
|
|
19
|
+
| `github.pull-requests.read` | `gh pr view`, `gh pr list`, `gh pr diff`, `gh pr checks` |
|
|
20
|
+
| `github.pull-requests.write` | Typed PR creation and allowlisted REST PR lifecycle endpoints |
|
|
23
21
|
|
|
24
22
|
## Command matrix
|
|
25
23
|
|
|
@@ -37,16 +35,18 @@ Treat explicit repo flags as command-targeting safety rails, not as a credential
|
|
|
37
35
|
| Stage and commit | `git -C DIRECTORY add -A && git -C DIRECTORY commit -m "message"` |
|
|
38
36
|
| Push branch before PR creation | `git -C DIRECTORY push -u origin BRANCH` |
|
|
39
37
|
| Create pull request (draft) | `github_createPullRequest({ repo: "owner/repo", head: "BRANCH", base: "BASE", title: "...", body: "...", draft: true })` |
|
|
40
|
-
| Update pull request | `gh
|
|
41
|
-
|
|
|
42
|
-
|
|
|
38
|
+
| Update pull request | `gh api repos/owner/repo/pulls/NUMBER --method PATCH --input payload.json` |
|
|
39
|
+
| Mark ready for review | `gh api repos/owner/repo/pulls/NUMBER/ready_for_review --method POST` |
|
|
40
|
+
| Request reviewers | `gh api repos/owner/repo/pulls/NUMBER/requested_reviewers --method POST --input reviewers.json` |
|
|
41
|
+
| Remove requested reviewers | `gh api repos/owner/repo/pulls/NUMBER/requested_reviewers --method DELETE --input reviewers.json` |
|
|
42
|
+
| Close pull request | `gh api repos/owner/repo/pulls/NUMBER --method PATCH -f state=closed` |
|
|
43
|
+
| Submit human review | `gh api repos/owner/repo/pulls/NUMBER/reviews --method POST --input review.json` |
|
|
43
44
|
| View pull request | `gh pr view NUMBER --repo owner/repo [--json ...]` |
|
|
44
45
|
| List pull requests | `gh pr list --repo owner/repo [--state open \| closed \| merged]` |
|
|
45
46
|
| Diff pull request | `gh pr diff NUMBER --repo owner/repo` |
|
|
46
47
|
| Check pull request status | `gh pr checks NUMBER --repo owner/repo` |
|
|
47
48
|
| View PR review comments | `gh api repos/{owner}/{repo}/pulls/{number}/comments` |
|
|
48
49
|
| View PR reviews | `gh api repos/{owner}/{repo}/pulls/{number}/reviews` |
|
|
49
|
-
| Dispatch workflow | `gh workflow run WORKFLOW -R owner/repo --ref REF [-f key=value ...]` |
|
|
50
50
|
| List workflow runs | `gh run list -R owner/repo --workflow WORKFLOW [--limit N] [--json ...]` |
|
|
51
51
|
| View workflow run | `gh run view RUN_ID -R owner/repo [--json ...] [--log-failed]` |
|
|
52
52
|
| Watch workflow run | `gh run watch RUN_ID -R owner/repo --exit-status` |
|
|
@@ -62,12 +62,10 @@ 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
|
|
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}`.
|
|
65
|
+
- A local `git commit` does not call GitHub. Pushing that commit uses Junior's repository-scoped installation credential and requires `github.contents.write` on the target repo.
|
|
67
66
|
- If the commit changes workflow files under `.github/workflows`, expect `github.workflows.write` in addition to contents write.
|
|
68
67
|
- 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.
|
|
69
|
-
-
|
|
68
|
+
- Merge, fork creation, workflow dispatch, REST contents/Git database writes, and repository administration are outside the current write allowlist.
|
|
70
69
|
- If the explicit `git push` fails with 401/403 or another access/permission error, verify the repo context and retry once. If it still fails, load troubleshooting guidance and report the exact command failure.
|
|
71
|
-
-
|
|
72
|
-
- `gh pr close --comment` may need `github.issues.write` (use `github-issues`); `gh pr close --delete-branch` needs `github.contents.write`.
|
|
70
|
+
- PR comments, labels, and assignees use GitHub's issue endpoints and therefore the issue grant; use the `github-issues` REST guidance for those operations.
|
|
73
71
|
- Return actionable errors for access, permission, not-found, and validation failures.
|
|
@@ -10,7 +10,7 @@ Use this table to recover quickly while keeping operations deterministic.
|
|
|
10
10
|
| Command affects or authenticates against the wrong repo | Stale `github.repo` default or authenticated command missing explicit repo. | Pass `--repo owner/repo` for the target repository, or update `github.repo` before retrying. |
|
|
11
11
|
| `GraphQL: Could not resolve to a Repository` | Repo slug is wrong or inaccessible. | Validate `owner/repo` and confirm app installation on target repository. |
|
|
12
12
|
| 401 Unauthorized | Issued GitHub credentials were rejected upstream. | Verify the target repo, then use the grant/auth signal to distinguish stale user OAuth from app installation or host env setup. |
|
|
13
|
-
| `junior-auth-required provider=github grant=user-write` | User-to-server OAuth is missing or stale for a
|
|
13
|
+
| `junior-auth-required provider=github grant=user-write` | User-to-server OAuth is missing or stale for a human-identity operation. | Follow the private OAuth prompt; do not ask the user to paste or manage tokens manually. |
|
|
14
14
|
| `git push` fails with 401/403 or auth/permission output | Write permission is missing, app installation is too narrow, or remote is wrong. | Verify the remote and repo context, retry once, then confirm app permissions and installation scope if it still fails. |
|
|
15
15
|
| Bash result includes `permission_denied` with `source: "upstream"` | GitHub returned 403 after Junior injected the named grant. | Do not call this a Junior runtime block. Use the message, connected account, upstream target, grant requirements, accepted-permissions, and SSO fields to explain the GitHub denial. |
|
|
16
16
|
| 403 Forbidden | App lacks required permission on repo or install scope is too narrow. | Verify the repo context, then confirm GitHub App permissions and installation scope. |
|
|
@@ -24,7 +24,7 @@ Use this table to recover quickly while keeping operations deterministic.
|
|
|
24
24
|
|
|
25
25
|
- Retry once for transient transport failures after verifying repo context.
|
|
26
26
|
- Do not loop retries on repeated 401/403/404 validation errors.
|
|
27
|
-
- Treat missing or stale `user-read`/`user-write` grants as private GitHub App OAuth work
|
|
27
|
+
- Treat missing or stale `user-read`/`user-write` grants as private GitHub App OAuth work. Treat all `installation-*` failures as App permission, installation scope, or host environment setup; they do not fall back to user OAuth.
|
|
28
28
|
- Do not describe `permission_denied` with `source: "upstream"` as Junior blocking the request. It means the egress proxy injected a credential, forwarded the request, and recorded GitHub's upstream 403. Prefer its `account` and `grant.requirements` fields over inference when explaining what to fix.
|
|
29
29
|
- Do not infer permission level from OAuth scopes. GitHub App user tokens report no OAuth scopes; GitHub App permissions and accepted-permissions headers are the useful evidence.
|
|
30
30
|
- For persistent permission problems, return explicit remediation and stop.
|
|
@@ -62,7 +62,7 @@ Follow [references/research-rules.md](references/research-rules.md) for cross-ty
|
|
|
62
62
|
|
|
63
63
|
**Source attribution:**
|
|
64
64
|
|
|
65
|
-
-
|
|
65
|
+
- The runtime adds the verified `Requested by` block. Do not add or rewrite requester attribution in model-authored body text.
|
|
66
66
|
- If the person who originally reported or observed the problem differs from the issue creator, capture that with durable body text such as `Reported by Alice.` or `Raised by Alice during incident triage.`
|
|
67
67
|
- Attach screenshots from the thread as image links when present.
|
|
68
68
|
- Include code snippets, related issues, and related PRs only when they materially improve the issue.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GitHub Issue API Surface
|
|
2
2
|
|
|
3
|
-
Issue creation uses Junior's `github_createIssue` tool. Other issue operations use `gh
|
|
3
|
+
Issue creation uses Junior's `github_createIssue` tool. Other issue operations use allowlisted REST endpoints through `gh api`; generic GraphQL-backed `gh issue` mutations are not supported.
|
|
4
4
|
|
|
5
5
|
## Repo scoping
|
|
6
6
|
|
|
@@ -10,22 +10,22 @@ Treat explicit repo flags as command-targeting safety rails, not as a credential
|
|
|
10
10
|
|
|
11
11
|
## GitHub App permission guidance
|
|
12
12
|
|
|
13
|
-
| Permission capability | Operations
|
|
14
|
-
| --------------------- |
|
|
15
|
-
| `github.issues.read` | `gh issue view`, `gh api /repos/.../comments`
|
|
16
|
-
| `github.issues.write` | `github_createIssue
|
|
13
|
+
| Permission capability | Operations |
|
|
14
|
+
| --------------------- | ------------------------------------------------------------------------------------ |
|
|
15
|
+
| `github.issues.read` | `gh issue view`, `gh api /repos/.../comments` |
|
|
16
|
+
| `github.issues.write` | `github_createIssue` and allowlisted REST issue lifecycle endpoints through `gh api` |
|
|
17
17
|
|
|
18
18
|
## Command matrix
|
|
19
19
|
|
|
20
20
|
| Operation | Command |
|
|
21
21
|
| ------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
22
22
|
| Create issue | `github_createIssue({ repo: "owner/repo", title: "...", body: "...", labels: ["..."] })` |
|
|
23
|
-
| Update issue fields | `gh
|
|
24
|
-
| Close issue | `gh
|
|
25
|
-
| Reopen issue | `gh
|
|
26
|
-
| Add labels | `gh
|
|
27
|
-
| Remove
|
|
28
|
-
| Add comment | `gh
|
|
23
|
+
| Update issue fields | `gh api repos/owner/repo/issues/NUMBER --method PATCH --input payload.json` |
|
|
24
|
+
| Close issue | `gh api repos/owner/repo/issues/NUMBER --method PATCH -f state=closed` |
|
|
25
|
+
| Reopen issue | `gh api repos/owner/repo/issues/NUMBER --method PATCH -f state=open` |
|
|
26
|
+
| Add labels | `gh api repos/owner/repo/issues/NUMBER/labels --method POST --input labels.json` |
|
|
27
|
+
| Remove label | `gh api repos/owner/repo/issues/NUMBER/labels/LABEL --method DELETE` |
|
|
28
|
+
| Add comment | `gh api repos/owner/repo/issues/NUMBER/comments --method POST --input comment.json` |
|
|
29
29
|
| List issues | `gh issue list --repo owner/repo --json number,title,state,url --limit 20` |
|
|
30
30
|
| Read issue | `gh issue view NUMBER --repo owner/repo --json number,title,state,labels,assignees,author,url,body` |
|
|
31
31
|
| Read comments | `gh api /repos/owner/repo/issues/NUMBER/comments --method GET --header "Accept: application/vnd.github+json"` |
|
|
@@ -9,15 +9,15 @@ Use this table to recover quickly while keeping operations deterministic.
|
|
|
9
9
|
| Command affects or authenticates against the wrong repo | Stale `github.repo` default or authenticated command missing explicit repo. | Pass `--repo owner/repo` for the target repository, or update `github.repo` before retrying. |
|
|
10
10
|
| `GraphQL: Could not resolve to a Repository` | Repo slug is wrong or inaccessible. | Validate `owner/repo` and confirm app installation on target repository. |
|
|
11
11
|
| 401 Unauthorized | Issued GitHub credentials were rejected upstream. | Verify the target repo, then use the grant/auth signal to distinguish stale user OAuth from app installation or host env setup. |
|
|
12
|
-
| `junior-auth-required provider=github grant=user-write` | User-to-server OAuth is missing or stale for a
|
|
12
|
+
| `junior-auth-required provider=github grant=user-write` | User-to-server OAuth is missing or stale for a human-identity operation. | Follow the private OAuth prompt; do not ask the user to paste or manage tokens manually. |
|
|
13
13
|
| 403 Forbidden | App lacks required permission on repo or install scope is too narrow. | Verify the repo context, then confirm GitHub App permissions and installation scope. |
|
|
14
14
|
| 404 Not Found | Issue number or repo is wrong. | Validate repo + issue ID with `gh issue view NUMBER --repo owner/repo`. |
|
|
15
|
-
|
|
|
16
|
-
| Comment command fails with empty body |
|
|
15
|
+
| Issue label mutation fails | Wrong REST payload or wrong repo context. | Use the allowlisted issue labels endpoint with an explicit `owner/repo` path and valid JSON input. |
|
|
16
|
+
| Comment command fails with empty body | JSON input is missing a non-empty `body`. | Ensure the REST comment payload contains a non-empty `body` before retrying. |
|
|
17
17
|
|
|
18
18
|
## Retry guidance
|
|
19
19
|
|
|
20
20
|
- Retry once for transient transport failures after verifying repo context.
|
|
21
21
|
- Do not loop retries on repeated 401/403/404 validation errors.
|
|
22
|
-
- Treat missing or stale `user-read`/`user-write` grants as private GitHub App OAuth work
|
|
22
|
+
- Treat missing or stale `user-read`/`user-write` grants as private GitHub App OAuth work. Treat all `installation-*` failures as App permission, installation scope, or host environment setup; they do not fall back to user OAuth.
|
|
23
23
|
- For persistent permission problems, return explicit remediation and stop.
|