@volter/twin-github 0.1.2 → 2.0.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/README.md +88 -36
- package/client/github-mirror.css +277 -319
- package/client/github-mirror.d.ts +418 -0
- package/client/github-mirror.js +485 -0
- package/client/github-mirror.tsx +153 -357
- package/client/pulls-rest.ts +159 -0
- package/client/pulls-workspace.tsx +841 -0
- package/dist/client/github-mirror.bundle.js +239 -0
- package/dist/client/github-mirror.css +916 -0
- package/dist/client/github-mirror.d.ts +418 -0
- package/dist/client/github-mirror.js +485 -0
- package/dist/client/github-mirror.tsx +1315 -0
- package/dist/client/pulls-rest.d.ts +42 -0
- package/dist/client/pulls-rest.js +140 -0
- package/dist/client/pulls-rest.ts +159 -0
- package/dist/client/pulls-workspace.bundle.js +22 -0
- package/dist/client/pulls-workspace.d.ts +114 -0
- package/dist/client/pulls-workspace.js +418 -0
- package/dist/client/pulls-workspace.tsx +841 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +40 -0
- package/dist/src/generated/graphql-sdl.gen.json +1 -0
- package/dist/src/generated/graphql.gen.json +1 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/github-budget.d.ts +69 -0
- package/dist/src/github-budget.js +172 -0
- package/dist/src/github-capabilities.d.ts +5 -0
- package/dist/src/github-capabilities.js +4468 -0
- package/dist/src/github-conformance.d.ts +43 -0
- package/dist/src/github-conformance.js +76 -0
- package/dist/src/github-connector.d.ts +307 -0
- package/dist/src/github-connector.js +1398 -0
- package/dist/src/github-events.d.ts +41 -0
- package/dist/src/github-events.js +230 -0
- package/dist/src/github-git-http.d.ts +49 -0
- package/dist/src/github-git-http.js +185 -0
- package/dist/src/github-git-plane.d.ts +114 -0
- package/dist/src/github-git-plane.js +407 -0
- package/dist/src/github-mirror-state.d.ts +2 -0
- package/dist/src/github-mirror-state.js +335 -0
- package/dist/src/github-mirror-ui.d.ts +20 -0
- package/dist/src/github-mirror-ui.js +101 -0
- package/dist/src/github-server.d.ts +14 -0
- package/dist/src/github-server.js +240 -0
- package/dist/src/github-shared.d.ts +12 -0
- package/dist/src/github-shared.js +21 -0
- package/dist/src/github-twin.d.ts +1411 -0
- package/dist/src/github-twin.js +4084 -0
- package/dist/src/github-ui-conformance.d.ts +4 -0
- package/dist/src/github-ui-conformance.js +105 -0
- package/dist/src/github-ui-structure.d.ts +18 -0
- package/dist/src/github-ui-structure.js +251 -0
- package/dist/src/graphql-wire.d.ts +22 -0
- package/dist/src/graphql-wire.js +89 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +101 -0
- package/dist/src/manifest.d.ts +8 -0
- package/dist/src/manifest.js +597 -0
- package/dist/src/npm-registry.d.ts +7 -0
- package/dist/src/npm-registry.js +47 -0
- package/dist/src/screens/app-installation.d.ts +3 -0
- package/dist/src/screens/app-installation.js +166 -0
- package/dist/src/screens/app-manifest.d.ts +3 -0
- package/dist/src/screens/app-manifest.js +81 -0
- package/dist/src/screens/oauth.d.ts +15 -0
- package/dist/src/screens/oauth.js +257 -0
- package/dist/src/screens/session.d.ts +8 -0
- package/dist/src/screens/session.js +159 -0
- package/dist/src/semantics/actions.d.ts +2 -0
- package/dist/src/semantics/actions.js +413 -0
- package/dist/src/semantics/activity.d.ts +4 -0
- package/dist/src/semantics/activity.js +161 -0
- package/dist/src/semantics/apps.d.ts +2 -0
- package/dist/src/semantics/apps.js +144 -0
- package/dist/src/semantics/branches.d.ts +2 -0
- package/dist/src/semantics/branches.js +136 -0
- package/dist/src/semantics/checks.d.ts +2 -0
- package/dist/src/semantics/checks.js +176 -0
- package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
- package/dist/src/semantics/code-scanning-upload.js +97 -0
- package/dist/src/semantics/codespaces.d.ts +2 -0
- package/dist/src/semantics/codespaces.js +58 -0
- package/dist/src/semantics/commits.d.ts +2 -0
- package/dist/src/semantics/commits.js +109 -0
- package/dist/src/semantics/contents.d.ts +2 -0
- package/dist/src/semantics/contents.js +131 -0
- package/dist/src/semantics/deployments.d.ts +2 -0
- package/dist/src/semantics/deployments.js +127 -0
- package/dist/src/semantics/gists.d.ts +2 -0
- package/dist/src/semantics/gists.js +86 -0
- package/dist/src/semantics/git.d.ts +2 -0
- package/dist/src/semantics/git.js +235 -0
- package/dist/src/semantics/graphql.d.ts +7 -0
- package/dist/src/semantics/graphql.js +512 -0
- package/dist/src/semantics/index.d.ts +5 -0
- package/dist/src/semantics/index.js +62 -0
- package/dist/src/semantics/issues.d.ts +2 -0
- package/dist/src/semantics/issues.js +456 -0
- package/dist/src/semantics/keys.d.ts +2 -0
- package/dist/src/semantics/keys.js +66 -0
- package/dist/src/semantics/labels.d.ts +2 -0
- package/dist/src/semantics/labels.js +100 -0
- package/dist/src/semantics/meta.d.ts +12 -0
- package/dist/src/semantics/meta.js +144 -0
- package/dist/src/semantics/notifications.d.ts +4 -0
- package/dist/src/semantics/notifications.js +62 -0
- package/dist/src/semantics/orgs.d.ts +2 -0
- package/dist/src/semantics/orgs.js +420 -0
- package/dist/src/semantics/packages.d.ts +2 -0
- package/dist/src/semantics/packages.js +55 -0
- package/dist/src/semantics/pages.d.ts +2 -0
- package/dist/src/semantics/pages.js +176 -0
- package/dist/src/semantics/projects.d.ts +2 -0
- package/dist/src/semantics/projects.js +239 -0
- package/dist/src/semantics/pulls.d.ts +2 -0
- package/dist/src/semantics/pulls.js +462 -0
- package/dist/src/semantics/push-reactions.d.ts +101 -0
- package/dist/src/semantics/push-reactions.js +513 -0
- package/dist/src/semantics/releases.d.ts +19 -0
- package/dist/src/semantics/releases.js +231 -0
- package/dist/src/semantics/repo-invitations.d.ts +2 -0
- package/dist/src/semantics/repo-invitations.js +98 -0
- package/dist/src/semantics/repos.d.ts +17 -0
- package/dist/src/semantics/repos.js +390 -0
- package/dist/src/semantics/rulesets.d.ts +2 -0
- package/dist/src/semantics/rulesets.js +111 -0
- package/dist/src/semantics/search.d.ts +2 -0
- package/dist/src/semantics/search.js +84 -0
- package/dist/src/semantics/security.d.ts +2 -0
- package/dist/src/semantics/security.js +177 -0
- package/dist/src/semantics/shared.d.ts +61 -0
- package/dist/src/semantics/shared.js +151 -0
- package/dist/src/semantics/users.d.ts +2 -0
- package/dist/src/semantics/users.js +255 -0
- package/dist/src/semantics/webhooks.d.ts +2 -0
- package/dist/src/semantics/webhooks.js +107 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
- package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
- package/dist/test-fixtures/github-comment-schema.json +702 -0
- package/dist/test-fixtures/github-known-deviations.json +94 -0
- package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
- package/dist/test-fixtures/github-openapi-operations.json +362 -0
- package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
- package/dist/test-fixtures/github-pull-schema.json +3601 -0
- package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
- package/dist/test-fixtures/github-review-schema.json +236 -0
- package/package.json +20 -11
- package/src/cli.ts +7 -6
- package/src/generated/graphql-sdl.gen.json +1 -0
- package/src/generated/graphql.gen.json +1 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/github-a11y-snapshot.uitest.ts +5 -5
- package/src/github-budget.ts +4 -4
- package/src/github-capabilities.ts +1993 -556
- package/src/github-conformance.ts +12 -7
- package/src/github-connector.ts +108 -96
- package/src/github-events.ts +223 -95
- package/src/github-git-http.ts +47 -83
- package/src/github-git-plane.ts +247 -385
- package/src/github-journey.uitest.ts +28 -44
- package/src/github-mirror-state.ts +23 -61
- package/src/github-mirror-ui.ts +16 -10
- package/src/github-server.ts +158 -60
- package/src/github-shared.ts +1 -1
- package/src/github-twin.ts +1525 -4494
- package/src/github-ui-conformance.ts +7 -8
- package/src/github-ui-structure.ts +19 -18
- package/src/graphql-wire.ts +115 -0
- package/src/index.ts +47 -7
- package/src/manifest.ts +605 -0
- package/src/npm-registry.ts +43 -0
- package/src/screens/app-installation.tsx +217 -0
- package/src/screens/app-manifest.tsx +97 -0
- package/src/screens/oauth.tsx +258 -0
- package/src/screens/session.tsx +186 -0
- package/src/semantics/actions.ts +395 -0
- package/src/semantics/activity.ts +171 -0
- package/src/semantics/apps.ts +133 -0
- package/src/semantics/branches.ts +133 -0
- package/src/semantics/checks.ts +163 -0
- package/src/semantics/code-scanning-upload.ts +92 -0
- package/src/semantics/codespaces.ts +58 -0
- package/src/semantics/commits.ts +109 -0
- package/src/semantics/contents.ts +112 -0
- package/src/semantics/deployments.ts +116 -0
- package/src/semantics/gists.ts +85 -0
- package/src/semantics/git.ts +226 -0
- package/src/semantics/graphql.ts +505 -0
- package/src/semantics/index.ts +68 -0
- package/src/semantics/issues.ts +434 -0
- package/src/semantics/keys.ts +66 -0
- package/src/semantics/labels.ts +91 -0
- package/src/semantics/meta.ts +139 -0
- package/src/semantics/notifications.ts +58 -0
- package/src/semantics/orgs.ts +383 -0
- package/src/semantics/packages.ts +59 -0
- package/src/semantics/pages.ts +154 -0
- package/src/semantics/projects.ts +246 -0
- package/src/semantics/pulls.ts +421 -0
- package/src/semantics/push-reactions.ts +471 -0
- package/src/semantics/releases.ts +213 -0
- package/src/semantics/repo-invitations.ts +112 -0
- package/src/semantics/repos.ts +373 -0
- package/src/semantics/rulesets.ts +107 -0
- package/src/semantics/search.ts +83 -0
- package/src/semantics/security.ts +153 -0
- package/src/semantics/shared.ts +181 -0
- package/src/semantics/users.ts +252 -0
- package/src/semantics/webhooks.ts +101 -0
- package/test-fixtures/github-known-deviations.json +7 -8
- package/test-fixtures/github-openapi-operations.json +46 -227
- package/src/github-graphql.ts +0 -398
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { GITHUB_WORLD_USER, githubNotFound, resolvePlaneRef, paginate, toGithubCodeScanningAlert, toGithubCodeScanningAnalysis, toGithubDependabotAlert, toGithubSecretScanningAlert } from "../github-twin.js";
|
|
2
|
+
import { asView, gh, put, send } from "./shared.js";
|
|
3
|
+
import { dependabotOn, dependabotReads, manifestsAt } from "./push-reactions.js";
|
|
4
|
+
const wantState = (query) => new URLSearchParams((query ?? '').replace(/^\?/, '')).get('state');
|
|
5
|
+
const alertNumber = (ctx) => Number(decodeURIComponent(gh(ctx).seg[5]));
|
|
6
|
+
const invalid = (ctx, resource, field) => ctx.reply({ message: 'Validation Failed', errors: [{ resource, field, code: 'invalid' }] }, 422);
|
|
7
|
+
/** PATCH an alert: to `dismissed` with one of the vendor's reasons, or back (clearing the reason). */
|
|
8
|
+
async function dismissable(ctx, kind) {
|
|
9
|
+
const { repo, body, occurredAt, state, caller } = gh(ctx);
|
|
10
|
+
const number = alertNumber(ctx);
|
|
11
|
+
const alerts = kind === 'code_scanning' ? state().codeScanningAlerts : state().dependabotAlerts;
|
|
12
|
+
const existing = alerts.find((a) => a.repository === repo && a.number === number);
|
|
13
|
+
if (!existing)
|
|
14
|
+
return send(ctx, githubNotFound());
|
|
15
|
+
const newState = body.state !== undefined ? String(body.state) : existing.state;
|
|
16
|
+
// Dismissing requires a reason (real GitHub 422s without it).
|
|
17
|
+
if (newState === 'dismissed' && !body.dismissed_reason)
|
|
18
|
+
return ctx.reply({ message: 'dismissed_reason is required when dismissing an alert' }, 422);
|
|
19
|
+
const validReasons = kind === 'code_scanning' ? ['false positive', "won't fix", 'used in tests'] : ['fix_started', 'inaccurate', 'no_bandwidth', 'not_used', 'tolerable_risk'];
|
|
20
|
+
if (newState === 'dismissed' && !validReasons.includes(String(body.dismissed_reason)))
|
|
21
|
+
return invalid(ctx, kind === 'code_scanning' ? 'CodeScanningAlert' : 'DependabotAlert', 'dismissed_reason');
|
|
22
|
+
const fields = { number, repository: repo, state: newState, updated_at: occurredAt };
|
|
23
|
+
if (newState === 'dismissed') {
|
|
24
|
+
fields.dismissed_reason = String(body.dismissed_reason);
|
|
25
|
+
fields.dismissed_comment = body.dismissed_comment !== undefined ? String(body.dismissed_comment) : null;
|
|
26
|
+
fields.dismissed_at = occurredAt;
|
|
27
|
+
fields.dismissed_by_login = caller ?? GITHUB_WORLD_USER;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
fields.dismissed_reason = null;
|
|
31
|
+
fields.dismissed_comment = null;
|
|
32
|
+
fields.dismissed_at = null;
|
|
33
|
+
fields.dismissed_by_login = null;
|
|
34
|
+
}
|
|
35
|
+
if (kind === 'code_scanning') {
|
|
36
|
+
await put(ctx, { operation: 'code_scanning_alert.update', subjectType: 'code_scanning_alert', subjectId: `csalert:${repo}#${number}`, fields });
|
|
37
|
+
return ctx.reply(toGithubCodeScanningAlert(state().codeScanningAlerts.find((x) => x.repository === repo && x.number === number)));
|
|
38
|
+
}
|
|
39
|
+
await put(ctx, { operation: 'dependabot_alert.update', subjectType: 'dependabot_alert', subjectId: `dependabot:${repo}#${number}`, fields });
|
|
40
|
+
return ctx.reply(toGithubDependabotAlert(state().dependabotAlerts.find((x) => x.repository === repo && x.number === number)));
|
|
41
|
+
}
|
|
42
|
+
async function vulnerabilityAlerts(ctx, on) {
|
|
43
|
+
const { repo, root, caller, state } = gh(ctx);
|
|
44
|
+
const st = state();
|
|
45
|
+
const r = st.repos.find((x) => x.full_name === repo);
|
|
46
|
+
if (!r)
|
|
47
|
+
return send(ctx, githubNotFound());
|
|
48
|
+
const me = caller ?? GITHUB_WORLD_USER;
|
|
49
|
+
const admin = r.owner === me || st.orgMembers.some((m) => m.org === r.owner && m.login === me && m.role === 'admin' && m.state === 'active') || st.collaborators.some((c) => c.repository === repo && c.login === me && c.permission === 'admin');
|
|
50
|
+
if (!admin)
|
|
51
|
+
return send(ctx, githubNotFound());
|
|
52
|
+
await put(ctx, { operation: on ? 'vulnerability_alerts.enable' : 'vulnerability_alerts.disable', subjectType: 'vulnerability_alerts', subjectId: `vulnalerts:${repo}`, fields: { repository: repo, enabled: on } });
|
|
53
|
+
const tip = on ? await resolvePlaneRef(repo, r.default_branch ?? 'main', r.default_branch ?? 'main', root) : null;
|
|
54
|
+
if (tip)
|
|
55
|
+
await dependabotReads(ctx, repo, tip, await manifestsAt(repo, tip, root));
|
|
56
|
+
return send(ctx, { status: 204, body: undefined });
|
|
57
|
+
}
|
|
58
|
+
export function securitySemantics() {
|
|
59
|
+
const codeScanningListRecentAnalyses = async (ctx) => {
|
|
60
|
+
const { repo, query, state } = gh(ctx);
|
|
61
|
+
const q = new URLSearchParams((query ?? '').replace(/^\?/, ''));
|
|
62
|
+
const items = state().codeScanningAnalyses.filter((a) => a.repository === repo && (!q.get('sarif_id') || a.sarif_id === q.get('sarif_id')) && (!q.get('ref') || a.ref === q.get('ref')));
|
|
63
|
+
// most recent first, as GitHub lists them
|
|
64
|
+
return ctx.reply(paginate([...items].sort((x, y) => String(y.created_at ?? '').localeCompare(String(x.created_at ?? '')) || y.id - x.id), query).map(toGithubCodeScanningAnalysis));
|
|
65
|
+
};
|
|
66
|
+
const codeScanningGetAnalysis = async (ctx) => {
|
|
67
|
+
const { repo, state } = gh(ctx);
|
|
68
|
+
const id = alertNumber(ctx);
|
|
69
|
+
const a = state().codeScanningAnalyses.find((x) => x.repository === repo && x.id === id);
|
|
70
|
+
return send(ctx, a ? { status: 200, body: toGithubCodeScanningAnalysis(a) } : githubNotFound());
|
|
71
|
+
};
|
|
72
|
+
const codeScanningDeleteAnalysis = async (ctx) => {
|
|
73
|
+
const { repo, state } = gh(ctx);
|
|
74
|
+
const id = alertNumber(ctx);
|
|
75
|
+
if (!state().codeScanningAnalyses.find((a) => a.repository === repo && a.id === id))
|
|
76
|
+
return send(ctx, githubNotFound());
|
|
77
|
+
await put(ctx, { operation: 'code_scanning_analysis.delete', subjectType: 'code_scanning_analysis', subjectId: `csanalysis:${id}`, fields: { deleted: true, repository: repo } });
|
|
78
|
+
return ctx.reply({ next_analysis_url: null, confirm_delete_url: null });
|
|
79
|
+
};
|
|
80
|
+
const secretScanningListAlertsForRepo = async (ctx) => {
|
|
81
|
+
const { repo, query, state } = gh(ctx);
|
|
82
|
+
const want = wantState(query);
|
|
83
|
+
let items = state().secretScanningAlerts.filter((a) => a.repository === repo);
|
|
84
|
+
if (want)
|
|
85
|
+
items = items.filter((a) => a.state === want);
|
|
86
|
+
return ctx.reply(paginate(items, query).map(toGithubSecretScanningAlert));
|
|
87
|
+
};
|
|
88
|
+
const secretScanningGetAlert = async (ctx) => {
|
|
89
|
+
const { repo, state } = gh(ctx);
|
|
90
|
+
const n = alertNumber(ctx);
|
|
91
|
+
const a = state().secretScanningAlerts.find((x) => x.repository === repo && x.number === n);
|
|
92
|
+
return send(ctx, a ? { status: 200, body: toGithubSecretScanningAlert(a) } : githubNotFound());
|
|
93
|
+
};
|
|
94
|
+
const secretScanningListLocationsForAlert = async (ctx) => {
|
|
95
|
+
const { repo, state } = gh(ctx);
|
|
96
|
+
const n = alertNumber(ctx);
|
|
97
|
+
const alert = state().secretScanningAlerts.find((x) => x.repository === repo && x.number === n);
|
|
98
|
+
if (!alert)
|
|
99
|
+
return send(ctx, githubNotFound());
|
|
100
|
+
return ctx.reply((alert.locations ?? []).map((l) => ({ type: 'commit', details: { ...l, blob_url: `https://api.github.com/repos/${repo}/git/blobs/${l.blob_sha}`, commit_url: `https://api.github.com/repos/${repo}/git/commits/${l.commit_sha}` } })));
|
|
101
|
+
};
|
|
102
|
+
const secretScanningUpdateAlert = async (ctx) => {
|
|
103
|
+
const { repo, body, occurredAt, state } = gh(ctx);
|
|
104
|
+
const number = alertNumber(ctx);
|
|
105
|
+
const existing = state().secretScanningAlerts.find((a) => a.repository === repo && a.number === number);
|
|
106
|
+
if (!existing)
|
|
107
|
+
return send(ctx, githubNotFound());
|
|
108
|
+
const newState = body.state !== undefined ? String(body.state) : existing.state;
|
|
109
|
+
if (newState === 'resolved' && !body.resolution)
|
|
110
|
+
return ctx.reply({ message: 'resolution is required when resolving an alert' }, 422);
|
|
111
|
+
if (newState === 'resolved' && !['false_positive', 'wont_fix', 'revoked', 'used_in_tests'].includes(String(body.resolution)))
|
|
112
|
+
return invalid(ctx, 'SecretScanningAlert', 'resolution');
|
|
113
|
+
const fields = { number, repository: repo, state: newState, updated_at: occurredAt };
|
|
114
|
+
if (newState === 'resolved') {
|
|
115
|
+
fields.resolution = String(body.resolution);
|
|
116
|
+
fields.resolution_comment = body.resolution_comment !== undefined ? String(body.resolution_comment) : null;
|
|
117
|
+
fields.resolved_at = occurredAt;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
fields.resolution = null;
|
|
121
|
+
fields.resolution_comment = null;
|
|
122
|
+
fields.resolved_at = null;
|
|
123
|
+
}
|
|
124
|
+
await put(ctx, { operation: 'secret_scanning_alert.update', subjectType: 'secret_scanning_alert', subjectId: `secretscan:${repo}#${number}`, fields });
|
|
125
|
+
return ctx.reply(toGithubSecretScanningAlert(state().secretScanningAlerts.find((x) => x.repository === repo && x.number === number)));
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
'code-scanning/list-alerts-for-repo': async (ctx) => {
|
|
129
|
+
const { repo, query, state } = gh(ctx);
|
|
130
|
+
const want = wantState(query);
|
|
131
|
+
let items = state().codeScanningAlerts.filter((a) => a.repository === repo);
|
|
132
|
+
if (want)
|
|
133
|
+
items = items.filter((a) => a.state === want);
|
|
134
|
+
return ctx.reply(paginate(items, query).map((a) => asView('code-scanning-alert-items', toGithubCodeScanningAlert(a))));
|
|
135
|
+
},
|
|
136
|
+
'code-scanning/get-alert': async (ctx) => {
|
|
137
|
+
const { repo, state } = gh(ctx);
|
|
138
|
+
const n = alertNumber(ctx);
|
|
139
|
+
const a = state().codeScanningAlerts.find((x) => x.repository === repo && x.number === n);
|
|
140
|
+
return send(ctx, a ? { status: 200, body: toGithubCodeScanningAlert(a) } : githubNotFound());
|
|
141
|
+
},
|
|
142
|
+
'code-scanning/update-alert': async (ctx) => dismissable(ctx, 'code_scanning'),
|
|
143
|
+
'code-scanning/list-recent-analyses': codeScanningListRecentAnalyses,
|
|
144
|
+
'code-scanning/get-analysis': codeScanningGetAnalysis,
|
|
145
|
+
'code-scanning/delete-analysis': codeScanningDeleteAnalysis,
|
|
146
|
+
// Dependabot alerts on or off for a repository: GET answers 204 when on and 404 when off; turning them on reads the
|
|
147
|
+
// default branch's manifests at once (docs.github.com/rest/repos/repos#check-if-vulnerability-alerts-are-enabled-for-a-repository,
|
|
148
|
+
// #enable-vulnerability-alerts, #disable-vulnerability-alerts). Only an admin of the repository turns them on or off.
|
|
149
|
+
'repos/check-vulnerability-alerts': async (ctx) => send(ctx, dependabotOn(ctx, gh(ctx).repo) ? { status: 204, body: undefined } : githubNotFound()),
|
|
150
|
+
'repos/enable-vulnerability-alerts': async (ctx) => vulnerabilityAlerts(ctx, true),
|
|
151
|
+
'repos/disable-vulnerability-alerts': async (ctx) => vulnerabilityAlerts(ctx, false),
|
|
152
|
+
// with alerts off there are none to list: GitHub answers 403
|
|
153
|
+
'dependabot/list-alerts-for-repo': async (ctx) => {
|
|
154
|
+
const { repo, query, state } = gh(ctx);
|
|
155
|
+
if (!dependabotOn(ctx, repo))
|
|
156
|
+
return ctx.reply({ message: 'Dependabot alerts are disabled for this repository.', documentation_url: 'https://docs.github.com/rest/dependabot/alerts#list-dependabot-alerts-for-a-repository' }, 403);
|
|
157
|
+
const want = wantState(query);
|
|
158
|
+
let items = state().dependabotAlerts.filter((a) => a.repository === repo);
|
|
159
|
+
if (want)
|
|
160
|
+
items = items.filter((a) => a.state === want);
|
|
161
|
+
return ctx.reply(paginate(items, query).map(toGithubDependabotAlert));
|
|
162
|
+
},
|
|
163
|
+
'dependabot/get-alert': async (ctx) => {
|
|
164
|
+
const { repo, state } = gh(ctx);
|
|
165
|
+
const n = alertNumber(ctx);
|
|
166
|
+
const a = state().dependabotAlerts.find((x) => x.repository === repo && x.number === n);
|
|
167
|
+
return send(ctx, a ? { status: 200, body: toGithubDependabotAlert(a) } : githubNotFound());
|
|
168
|
+
},
|
|
169
|
+
'dependabot/update-alert': async (ctx) => dismissable(ctx, 'dependabot'),
|
|
170
|
+
'secret-scanning/list-alerts-for-repo': secretScanningListAlertsForRepo,
|
|
171
|
+
'secret-scanning/get-alert': secretScanningGetAlert,
|
|
172
|
+
// where the secret was found: each commit and file secret scanning read it in
|
|
173
|
+
'secret-scanning/list-locations-for-alert': secretScanningListLocationsForAlert,
|
|
174
|
+
// resolving needs one of the vendor's resolutions; reopening clears it
|
|
175
|
+
'secret-scanning/update-alert': secretScanningUpdateAlert,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { SemanticsContext } from '@volter/world-core';
|
|
2
|
+
import { type GithubAtomic, type GithubResponse, type GithubState, type GithubWrite, type GithubWriteEvent } from '../github-twin.js';
|
|
3
|
+
/** An object as one of GitHub's views of it answers: a list's user is a simple-user, a list's pull request
|
|
4
|
+
* pull-request-simple, a search hit an issue-search-result-item. The view keeps the fields the spec gives
|
|
5
|
+
* it, from the full object it is a view of. */
|
|
6
|
+
export declare function asView(view: string, full: Record<string, unknown>): Record<string, unknown>;
|
|
7
|
+
export type Gh = {
|
|
8
|
+
ctx: SemanticsContext;
|
|
9
|
+
/** the path's segments as sent (percent-encoded), one per segment of the operation's path: a parameter
|
|
10
|
+
* that spans slashes is one segment holding its encoded value; an issue or pull number resolved
|
|
11
|
+
* through the alias map */
|
|
12
|
+
seg: string[];
|
|
13
|
+
/** the path, trailing slashes dropped */
|
|
14
|
+
path: string;
|
|
15
|
+
/** the query string with its `?`, when there is one */
|
|
16
|
+
query: string | undefined;
|
|
17
|
+
/** `owner/name` for a path under /repos */
|
|
18
|
+
repo: string;
|
|
19
|
+
root: string | undefined;
|
|
20
|
+
occurredAt: string;
|
|
21
|
+
/** the credential, as the twin's user door minted it */
|
|
22
|
+
token: string | undefined;
|
|
23
|
+
/** the caller's login, when the credential names one */
|
|
24
|
+
caller: string | undefined;
|
|
25
|
+
/** the JSON body as sent */
|
|
26
|
+
body: Record<string, any>;
|
|
27
|
+
state(): GithubState;
|
|
28
|
+
};
|
|
29
|
+
export declare function gh(ctx: SemanticsContext): Gh;
|
|
30
|
+
/** GitHub's projection over this request's tree. */
|
|
31
|
+
export declare const githubStateOf: (ctx: SemanticsContext) => GithubState;
|
|
32
|
+
/** GitHub's answer: JSON, or nothing for a 204 or an answer with no body. */
|
|
33
|
+
export declare function send(ctx: SemanticsContext, r: GithubResponse): Response;
|
|
34
|
+
/** A stored write, as the kernel records it: the subject's type, id, fields and operation name. */
|
|
35
|
+
export declare function put(ctx: SemanticsContext, w: {
|
|
36
|
+
operation: string;
|
|
37
|
+
subjectType: string;
|
|
38
|
+
subjectId: string;
|
|
39
|
+
fields: Record<string, unknown>;
|
|
40
|
+
}): Promise<Record<string, unknown>>;
|
|
41
|
+
export declare function collectAnnouncements<T>(run: () => Promise<T>): Promise<{
|
|
42
|
+
value: T;
|
|
43
|
+
events: GithubWriteEvent[];
|
|
44
|
+
}>;
|
|
45
|
+
export declare function noteAnnounced(events: GithubWriteEvent[]): void;
|
|
46
|
+
/** Deliver the webhooks a write announces. Fire-and-forget, like GitHub: a delivery failure never
|
|
47
|
+
* fails the write. */
|
|
48
|
+
export declare function announce(ctx: SemanticsContext, events: Array<GithubWriteEvent | undefined>): Promise<void>;
|
|
49
|
+
/** One past the highest `<prefix>:<n>` subject id stored, tombstones included. */
|
|
50
|
+
export declare function nextSeq(ctx: SemanticsContext, prefix: string): number;
|
|
51
|
+
/** The pack helpers' atomic decision (GithubAtomic), made through this request's context. */
|
|
52
|
+
export declare function atomic(ctx: SemanticsContext): GithubAtomic;
|
|
53
|
+
/** The pack helpers' writer (GithubWrite), through this request's context. */
|
|
54
|
+
export declare const writer: (ctx: SemanticsContext) => GithubWrite;
|
|
55
|
+
/** Who a commit or tag made through the API is by when the request names no one: the signed-in person, as
|
|
56
|
+
* GitHub defaults author and committer to the authenticated user (docs.github.com/rest/repos/contents), with
|
|
57
|
+
* the noreply address GitHub gives an account that keeps its email private. */
|
|
58
|
+
export declare function identityOf(g: Gh): {
|
|
59
|
+
name: string;
|
|
60
|
+
email: string;
|
|
61
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// What every GitHub handler (and GraphQL resolver) shares: the request as GitHub's helpers read it (the
|
|
2
|
+
// path's segments, the query, the JSON body), the tree read through the pack's projection over the
|
|
3
|
+
// context's whole-tree read, GitHub's answers, and the webhooks a write announces.
|
|
4
|
+
//
|
|
5
|
+
// Reads: GitHub's projection (`githubState`) folds every stored type in one pass, in tree order
|
|
6
|
+
// (`ctx.tree()`), and replays open/closed history from the log, which it still reads from the kernel:
|
|
7
|
+
// `ctx.history` is per subject and carries no actor, so it cannot tell a local create from an
|
|
8
|
+
// observation. Writes go through the context (`put`, `ctx.atomically`).
|
|
9
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
10
|
+
import { callerLogin, githubState, resolveGithubNumber, twinUserId } from "../github-twin.js";
|
|
11
|
+
import { emitGithubEvent, emitGithubInstallationEvents } from "../github-events.js";
|
|
12
|
+
import surface from '../generated/surface.gen.json' with { type: 'json' };
|
|
13
|
+
import { spanning } from "../manifest.js";
|
|
14
|
+
const VIEWS = new Map(surface.resources.map((r) => [r.name, new Set(r.fields.map((f) => f.name))]));
|
|
15
|
+
/** An object as one of GitHub's views of it answers: a list's user is a simple-user, a list's pull request
|
|
16
|
+
* pull-request-simple, a search hit an issue-search-result-item. The view keeps the fields the spec gives
|
|
17
|
+
* it, from the full object it is a view of. */
|
|
18
|
+
export function asView(view, full) {
|
|
19
|
+
const keep = VIEWS.get(view);
|
|
20
|
+
if (!keep)
|
|
21
|
+
throw new Error(`github: the spec has no view named ${view}`);
|
|
22
|
+
return Object.fromEntries(Object.entries(full).filter(([k]) => keep.has(k)));
|
|
23
|
+
}
|
|
24
|
+
export function gh(ctx) {
|
|
25
|
+
const url = new URL(ctx.call.request.url);
|
|
26
|
+
const path = url.pathname.replace(/\/+$/, '');
|
|
27
|
+
const sent = path.replace(/^\/+/, '').split('/');
|
|
28
|
+
const template = ctx.call.operation.path.replace(/^\/+/, '').split('/');
|
|
29
|
+
const extra = sent.length - template.length;
|
|
30
|
+
const at = extra > 0 ? template.findIndex((t) => t.startsWith('{') && spanning.includes(t.slice(1, -1))) : -1;
|
|
31
|
+
const seg = at === -1 ? sent : [...sent.slice(0, at), sent.slice(at, at + extra + 1).join('%2F'), ...sent.slice(at + extra + 1)];
|
|
32
|
+
if (seg[0] === 'repos' && (seg[3] === 'issues' || seg[3] === 'pulls') && seg[4] && /^\d+$/.test(seg[4]))
|
|
33
|
+
seg[4] = resolveGithubNumber(seg[1], seg[2], seg[4], ctx.root);
|
|
34
|
+
const token = (ctx.call.request.headers.get('authorization') ?? '').replace(/^(?:bearer|token)\s+/i, '').trim() || undefined;
|
|
35
|
+
return {
|
|
36
|
+
ctx,
|
|
37
|
+
seg,
|
|
38
|
+
path,
|
|
39
|
+
query: url.search || undefined,
|
|
40
|
+
repo: `${seg[1]}/${seg[2]}`,
|
|
41
|
+
root: ctx.root,
|
|
42
|
+
occurredAt: ctx.occurredAt,
|
|
43
|
+
token,
|
|
44
|
+
caller: callerLogin(token, ctx.root),
|
|
45
|
+
body: (ctx.body ?? {}),
|
|
46
|
+
state: () => githubStateOf(ctx),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** GitHub's projection over this request's tree. */
|
|
50
|
+
export const githubStateOf = (ctx) => githubState(ctx.root);
|
|
51
|
+
/** GitHub's answer: JSON, or nothing for a 204 or an answer with no body. */
|
|
52
|
+
export function send(ctx, r) {
|
|
53
|
+
if (r.status === 204 || r.body === undefined)
|
|
54
|
+
return ctx.raw(null, { status: r.status });
|
|
55
|
+
return ctx.reply(r.body, r.status);
|
|
56
|
+
}
|
|
57
|
+
/** A stored write, as the kernel records it: the subject's type, id, fields and operation name. */
|
|
58
|
+
export function put(ctx, w) {
|
|
59
|
+
return ctx.write(w.subjectType, w.subjectId, w.fields, w.operation);
|
|
60
|
+
}
|
|
61
|
+
/** The events announced while an in-process call runs, for the caller that asked (applyGithubWrite
|
|
62
|
+
* answers them beside the response, as the hand-written writes did). */
|
|
63
|
+
const announcements = new AsyncLocalStorage();
|
|
64
|
+
export async function collectAnnouncements(run) {
|
|
65
|
+
const events = [];
|
|
66
|
+
const value = await announcements.run(events, run);
|
|
67
|
+
// an in-process call made inside another (a GraphQL mutation writing through REST) announces to both
|
|
68
|
+
announcements.getStore()?.push(...events);
|
|
69
|
+
return { value, events };
|
|
70
|
+
}
|
|
71
|
+
export function noteAnnounced(events) {
|
|
72
|
+
announcements.getStore()?.push(...events);
|
|
73
|
+
}
|
|
74
|
+
/** Deliver the webhooks a write announces. Fire-and-forget, like GitHub: a delivery failure never
|
|
75
|
+
* fails the write. */
|
|
76
|
+
export async function announce(ctx, events) {
|
|
77
|
+
for (const event of events) {
|
|
78
|
+
if (!event)
|
|
79
|
+
continue;
|
|
80
|
+
// a branch that moved is read by GitHub's own processes (workflows, secret scanning, Dependabot)
|
|
81
|
+
if (event.event === 'push' && event.ref && event.after) {
|
|
82
|
+
const { reactToPush } = await import("./push-reactions.js");
|
|
83
|
+
await reactToPush(ctx, event.repository, String(event.ref), String(event.before ?? '0'.repeat(40)), String(event.after));
|
|
84
|
+
}
|
|
85
|
+
// and so is a pull request opened or pushed to: its workflows run (semantics/push-reactions.ts)
|
|
86
|
+
if (event.event === 'pull_request' && event.number) {
|
|
87
|
+
const { reactToPullRequest } = await import("./push-reactions.js");
|
|
88
|
+
await reactToPullRequest(ctx, event.repository, Number(event.number), String(event.action ?? ''));
|
|
89
|
+
}
|
|
90
|
+
// and someone else's act on an issue or pull request may notify the World's user
|
|
91
|
+
if (event.event === 'issues' || event.event === 'issue_comment' || event.event === 'pull_request' || event.event === 'pull_request_review') {
|
|
92
|
+
const { notify } = await import("./notifications.js");
|
|
93
|
+
await notify(ctx, event);
|
|
94
|
+
}
|
|
95
|
+
noteAnnounced([event]);
|
|
96
|
+
try {
|
|
97
|
+
await emitGithubEvent(event, { occurredAt: ctx.occurredAt, root: ctx.root });
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* fire-and-forget */
|
|
101
|
+
}
|
|
102
|
+
// and the GitHub Apps installed on the repository hear of it on their own webhooks
|
|
103
|
+
try {
|
|
104
|
+
await emitGithubInstallationEvents(event, { occurredAt: ctx.occurredAt, root: ctx.root });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* fire-and-forget */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** The stored types whose subject ids carry each sequence prefix (`sshkey:3`). */
|
|
112
|
+
const SEQ_TYPES = {
|
|
113
|
+
artifact: ['actions_artifact'], asset: ['release_asset'], autolink: ['autolink'], cache: ['actions_cache'], check: ['check_run'],
|
|
114
|
+
comment: ['issue_comment', 'pull_request_review_comment'], commitcomment: ['commit_comment'], csanalysis: ['code_scanning_analysis'],
|
|
115
|
+
deploykey: ['deploy_key'], deployment: ['deployment'], depstatus: ['deployment_status'], discomment: ['discussion_comment'],
|
|
116
|
+
gpgkey: ['user_gpg_key'], installation: ['app_installation'], issuetype: ['issue_type'], label: ['label'], orghook: ['org_webhook'],
|
|
117
|
+
package: ['package'], packageversion: ['package_version'], pagesbuild: ['pages_build'], project: ['project'], projectfield: ['project_field'],
|
|
118
|
+
projectitem: ['project_item'], projectview: ['project_view'], reaction: ['reaction'], release: ['release'], repo: ['repository'], repoinvite: ['repo_invitation'],
|
|
119
|
+
review: ['pull_request_review'], rulesuite: ['rule_suite'], ruleset: ['ruleset'], sshkey: ['user_ssh_key'], status: ['commit_status'],
|
|
120
|
+
team: ['team'], teamdisc: ['team_discussion'], teamdisccomment: ['team_discussion_comment'], thread: ['review_thread'], webhook: ['webhook'],
|
|
121
|
+
};
|
|
122
|
+
/** One past the highest `<prefix>:<n>` subject id stored, tombstones included. */
|
|
123
|
+
export function nextSeq(ctx, prefix) {
|
|
124
|
+
const pattern = new RegExp(`^${prefix}:(\\d+)$`);
|
|
125
|
+
let max = 0;
|
|
126
|
+
for (const type of SEQ_TYPES[prefix] ?? []) {
|
|
127
|
+
for (const r of ctx.rowsRaw(type, { withDeleted: true })) {
|
|
128
|
+
const m = pattern.exec(String(r.id));
|
|
129
|
+
if (m)
|
|
130
|
+
max = Math.max(max, Number(m[1]));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return max + 1;
|
|
134
|
+
}
|
|
135
|
+
/** The pack helpers' atomic decision (GithubAtomic), made through this request's context. */
|
|
136
|
+
export function atomic(ctx) {
|
|
137
|
+
return (decide) => ctx.atomically(() => {
|
|
138
|
+
const d = decide();
|
|
139
|
+
return d.write ? { value: d.value, write: { resource: d.write.subjectType, id: d.write.subjectId, fields: d.write.fields, operation: d.write.operation } } : { value: d.value };
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** The pack helpers' writer (GithubWrite), through this request's context. */
|
|
143
|
+
export const writer = (ctx) => (w) => put(ctx, w);
|
|
144
|
+
/** Who a commit or tag made through the API is by when the request names no one: the signed-in person, as
|
|
145
|
+
* GitHub defaults author and committer to the authenticated user (docs.github.com/rest/repos/contents), with
|
|
146
|
+
* the noreply address GitHub gives an account that keeps its email private. */
|
|
147
|
+
export function identityOf(g) {
|
|
148
|
+
const login = g.caller ?? 'octocat';
|
|
149
|
+
const profile = login === 'octocat' ? g.state().userProfile : undefined;
|
|
150
|
+
return { name: String(profile?.name ?? login), email: `${twinUserId(login)}+${login}@users.noreply.github.com` };
|
|
151
|
+
}
|