@goodandready/dsh-github-ops 0.6.9 → 0.6.11
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/CHANGELOG.md +6 -0
- package/lib/events.js +13 -0
- package/lib/tools/deployments.js +154 -0
- package/lib/tools/register-ops.js +131 -0
- package/lib/tools/webhook-sim.js +289 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.11
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- **GitHub Environments & Deployments Management (#122)**: Added `gh_environment_list` tool to inspect deployment environments, wait timers, and protection rules. Added `gh_deployment_list`, `gh_deployment_create`, and `gh_deployment_status_create` tools for full deployment lifecycle auditing and status updates.
|
|
7
|
+
- **Webhook Simulator & Event Emitter (#125)**: Added `gh_webhook_simulate` tool supporting canonical mock payloads for `issues`, `pull_request`, `workflow_run`, `release`, `deployment`, `deployment_status`, and `push` events, with live broadcast into local `SseEventManager` and optional HTTP delivery to webhook receivers.
|
|
8
|
+
|
|
3
9
|
## 0.6.9
|
|
4
10
|
|
|
5
11
|
### Features
|
package/lib/events.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
let activeEventManager = null
|
|
3
|
+
|
|
4
|
+
export function setGlobalEventManager(mgr) {
|
|
5
|
+
activeEventManager = mgr
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getGlobalEventManager() {
|
|
9
|
+
return activeEventManager
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
// Server-Sent Events (SSE) notification manager for CI runs and PR events.
|
|
2
13
|
//
|
|
3
14
|
// Maintains client subscriptions, broadcasts real-time events, and runs an adaptive
|
|
@@ -21,6 +32,7 @@ export class SseEventManager {
|
|
|
21
32
|
this.lastRuns = new Map() // runId -> { status, conclusion }
|
|
22
33
|
this.lastPulls = new Map() // pullNumber -> { updated, state }
|
|
23
34
|
this.hasActiveWork = false
|
|
35
|
+
setGlobalEventManager(this)
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
get clientCount() {
|
|
@@ -197,5 +209,6 @@ export class SseEventManager {
|
|
|
197
209
|
this.clients.clear()
|
|
198
210
|
this.lastRuns.clear()
|
|
199
211
|
this.lastPulls.clear()
|
|
212
|
+
if (getGlobalEventManager() === this) setGlobalEventManager(null)
|
|
200
213
|
}
|
|
201
214
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// GitHub Environments and Deployments tools.
|
|
2
|
+
// Pure functions over a GitHub client.
|
|
3
|
+
|
|
4
|
+
function ensureRepo({ owner, repo }) {
|
|
5
|
+
if (!owner || !repo) {
|
|
6
|
+
throw new Error('owner and repo are required (pass "repository": "owner/repo" or set defaults in settings)')
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** List environments for a repository. */
|
|
11
|
+
export async function listEnvironments(client, { owner, repo, limit = 30 } = {}) {
|
|
12
|
+
ensureRepo({ owner, repo })
|
|
13
|
+
const perPage = Math.max(1, Math.min(Number(limit) || 30, 100))
|
|
14
|
+
const { data } = await client.get(`/repos/${owner}/${repo}/environments`, {
|
|
15
|
+
query: { per_page: perPage },
|
|
16
|
+
})
|
|
17
|
+
const envs = data && Array.isArray(data.environments) ? data.environments : []
|
|
18
|
+
return {
|
|
19
|
+
totalCount: data && typeof data.total_count === 'number' ? data.total_count : envs.length,
|
|
20
|
+
environments: envs.map((env) => ({
|
|
21
|
+
id: env.id,
|
|
22
|
+
name: env.name,
|
|
23
|
+
url: env.html_url || '',
|
|
24
|
+
createdAt: env.created_at,
|
|
25
|
+
updatedAt: env.updated_at,
|
|
26
|
+
protectionRulesCount: Array.isArray(env.protection_rules) ? env.protection_rules.length : 0,
|
|
27
|
+
protectionRules: (env.protection_rules || []).map((r) => ({
|
|
28
|
+
id: r.id,
|
|
29
|
+
type: r.type,
|
|
30
|
+
waitTimer: r.wait_timer,
|
|
31
|
+
})),
|
|
32
|
+
})),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** List deployments for a repository. */
|
|
37
|
+
export async function listDeployments(client, { owner, repo, environment, ref, sha, task, limit = 30 } = {}) {
|
|
38
|
+
ensureRepo({ owner, repo })
|
|
39
|
+
const perPage = Math.max(1, Math.min(Number(limit) || 30, 100))
|
|
40
|
+
const query = { per_page: perPage }
|
|
41
|
+
if (environment) query.environment = environment
|
|
42
|
+
if (ref) query.ref = ref
|
|
43
|
+
if (sha) query.sha = sha
|
|
44
|
+
if (task) query.task = task
|
|
45
|
+
|
|
46
|
+
const { data } = await client.get(`/repos/${owner}/${repo}/deployments`, { query })
|
|
47
|
+
const deployments = Array.isArray(data) ? data : []
|
|
48
|
+
return {
|
|
49
|
+
deployments: deployments.map((d) => ({
|
|
50
|
+
id: d.id,
|
|
51
|
+
sha: d.sha,
|
|
52
|
+
ref: d.ref,
|
|
53
|
+
task: d.task,
|
|
54
|
+
environment: d.environment,
|
|
55
|
+
description: d.description || '',
|
|
56
|
+
creator: d.creator && d.creator.login,
|
|
57
|
+
createdAt: d.created_at,
|
|
58
|
+
updatedAt: d.updated_at,
|
|
59
|
+
url: d.url,
|
|
60
|
+
statusesUrl: d.statuses_url,
|
|
61
|
+
})),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Create a new deployment for a ref. */
|
|
66
|
+
export async function createDeployment(client, {
|
|
67
|
+
owner, repo, ref, environment = 'production', description, task = 'deploy',
|
|
68
|
+
autoMerge = false, requiredContexts = [], transientEnvironment = false,
|
|
69
|
+
productionEnvironment,
|
|
70
|
+
} = {}) {
|
|
71
|
+
ensureRepo({ owner, repo })
|
|
72
|
+
if (!ref) throw new Error('ref is required (branch name, tag or commit SHA)')
|
|
73
|
+
|
|
74
|
+
const payload = {
|
|
75
|
+
ref,
|
|
76
|
+
environment,
|
|
77
|
+
task,
|
|
78
|
+
auto_merge: Boolean(autoMerge),
|
|
79
|
+
required_contexts: Array.isArray(requiredContexts) ? requiredContexts : [],
|
|
80
|
+
transient_environment: Boolean(transientEnvironment),
|
|
81
|
+
}
|
|
82
|
+
if (typeof productionEnvironment === 'boolean') {
|
|
83
|
+
payload.production_environment = productionEnvironment
|
|
84
|
+
}
|
|
85
|
+
if (description) payload.description = String(description)
|
|
86
|
+
|
|
87
|
+
const { data } = await client.post(`/repos/${owner}/${repo}/deployments`, payload)
|
|
88
|
+
return {
|
|
89
|
+
id: data && data.id,
|
|
90
|
+
sha: data && data.sha,
|
|
91
|
+
ref: data && data.ref,
|
|
92
|
+
task: data && data.task,
|
|
93
|
+
environment: data && data.environment,
|
|
94
|
+
description: (data && data.description) || '',
|
|
95
|
+
creator: data && data.creator && data.creator.login,
|
|
96
|
+
createdAt: data && data.created_at,
|
|
97
|
+
url: data && data.url,
|
|
98
|
+
statusesUrl: data && data.statuses_url,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** List deployment statuses. */
|
|
103
|
+
export async function listDeploymentStatuses(client, { owner, repo, deploymentId, limit = 30 } = {}) {
|
|
104
|
+
ensureRepo({ owner, repo })
|
|
105
|
+
if (!deploymentId) throw new Error('deploymentId is required')
|
|
106
|
+
const perPage = Math.max(1, Math.min(Number(limit) || 30, 100))
|
|
107
|
+
const { data } = await client.get(`/repos/${owner}/${repo}/deployments/${deploymentId}/statuses`, {
|
|
108
|
+
query: { per_page: perPage },
|
|
109
|
+
})
|
|
110
|
+
const statuses = Array.isArray(data) ? data : []
|
|
111
|
+
return {
|
|
112
|
+
deploymentId,
|
|
113
|
+
statuses: statuses.map((s) => ({
|
|
114
|
+
id: s.id,
|
|
115
|
+
state: s.state,
|
|
116
|
+
description: s.description || '',
|
|
117
|
+
environmentUrl: s.environment_url || '',
|
|
118
|
+
logUrl: s.log_url || '',
|
|
119
|
+
creator: s.creator && s.creator.login,
|
|
120
|
+
createdAt: s.created_at,
|
|
121
|
+
})),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Create a status for an existing deployment. */
|
|
126
|
+
export async function createDeploymentStatus(client, {
|
|
127
|
+
owner, repo, deploymentId, state, logUrl, description, environmentUrl, autoInactive = true,
|
|
128
|
+
} = {}) {
|
|
129
|
+
ensureRepo({ owner, repo })
|
|
130
|
+
if (!deploymentId) throw new Error('deploymentId is required')
|
|
131
|
+
const validStates = ['error', 'failure', 'in_progress', 'queued', 'pending', 'success']
|
|
132
|
+
if (!state || !validStates.includes(state)) {
|
|
133
|
+
throw new Error(`state must be one of: ${validStates.join(', ')}`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const payload = {
|
|
137
|
+
state,
|
|
138
|
+
auto_inactive: Boolean(autoInactive),
|
|
139
|
+
}
|
|
140
|
+
if (logUrl) payload.log_url = String(logUrl)
|
|
141
|
+
if (description) payload.description = String(description)
|
|
142
|
+
if (environmentUrl) payload.environment_url = String(environmentUrl)
|
|
143
|
+
|
|
144
|
+
const { data } = await client.post(`/repos/${owner}/${repo}/deployments/${deploymentId}/statuses`, payload)
|
|
145
|
+
return {
|
|
146
|
+
id: data && data.id,
|
|
147
|
+
deploymentId,
|
|
148
|
+
state: data && data.state,
|
|
149
|
+
description: (data && data.description) || '',
|
|
150
|
+
environmentUrl: (data && data.environment_url) || '',
|
|
151
|
+
logUrl: (data && data.log_url) || '',
|
|
152
|
+
createdAt: data && data.created_at,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { matrixStatus } from './matrix.js'
|
|
2
|
+
import { listEnvironments, listDeployments, createDeployment, createDeploymentStatus } from './deployments.js'
|
|
3
|
+
import { simulateWebhook } from './webhook-sim.js'
|
|
2
4
|
// Workflow-run and repository-settings tool registrations.
|
|
3
5
|
//
|
|
4
6
|
// Reads are free; writes follow the plugin's model — an explicit confirm flag for the
|
|
@@ -367,4 +369,133 @@ export function registerOpsTools({ tool, asRepo, client, liveConfig }) {
|
|
|
367
369
|
})
|
|
368
370
|
},
|
|
369
371
|
)
|
|
372
|
+
|
|
373
|
+
// ------------------------------------------------------------------------ environments & deployments (#122)
|
|
374
|
+
|
|
375
|
+
tool(
|
|
376
|
+
'gh_environment_list',
|
|
377
|
+
'List deployment environments configured for a repository, including protection rules and wait timers. Read-only.',
|
|
378
|
+
repoParams({
|
|
379
|
+
limit: { type: 'number', description: 'Maximum environments to return (default 30, max 100).' },
|
|
380
|
+
}),
|
|
381
|
+
async (args) => {
|
|
382
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
383
|
+
return listEnvironments(await client(), { owner, repo, limit: args.limit })
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
tool(
|
|
388
|
+
'gh_deployment_list',
|
|
389
|
+
'List deployments for a repository with optional filtering by environment, ref, sha or task. Read-only.',
|
|
390
|
+
repoParams({
|
|
391
|
+
environment: { type: 'string', description: 'Filter by environment name (e.g. "production", "staging").' },
|
|
392
|
+
ref: { type: 'string', description: 'Filter by git ref or branch name.' },
|
|
393
|
+
sha: { type: 'string', description: 'Filter by commit SHA.' },
|
|
394
|
+
task: { type: 'string', description: 'Filter by task name (e.g. "deploy").' },
|
|
395
|
+
limit: { type: 'number', description: 'Maximum deployments to return (default 30, max 100).' },
|
|
396
|
+
}),
|
|
397
|
+
async (args) => {
|
|
398
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
399
|
+
return listDeployments(await client(), {
|
|
400
|
+
owner,
|
|
401
|
+
repo,
|
|
402
|
+
environment: args.environment,
|
|
403
|
+
ref: args.ref,
|
|
404
|
+
sha: args.sha,
|
|
405
|
+
task: args.task,
|
|
406
|
+
limit: args.limit,
|
|
407
|
+
})
|
|
408
|
+
},
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
tool(
|
|
412
|
+
'gh_deployment_create',
|
|
413
|
+
'Create a new deployment for a ref (branch, tag or commit SHA).',
|
|
414
|
+
repoParams({
|
|
415
|
+
ref: { type: 'string', description: 'Git ref to deploy (branch name, tag, or commit SHA). Required.' },
|
|
416
|
+
environment: { type: 'string', description: 'Target environment name (default "production").' },
|
|
417
|
+
task: { type: 'string', description: 'Deployment task (default "deploy").' },
|
|
418
|
+
description: { type: 'string', description: 'Optional human-readable description.' },
|
|
419
|
+
autoMerge: { type: 'boolean', description: 'Whether to auto-merge default branch into ref (default false).' },
|
|
420
|
+
requiredContexts: {
|
|
421
|
+
type: 'array',
|
|
422
|
+
items: { type: 'string' },
|
|
423
|
+
description: 'Status check contexts required to pass before deploying (default empty []).'
|
|
424
|
+
},
|
|
425
|
+
productionEnvironment: { type: 'boolean', description: 'Whether this environment is a production environment.' },
|
|
426
|
+
}),
|
|
427
|
+
async (args) => {
|
|
428
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
429
|
+
return createDeployment(await client(), {
|
|
430
|
+
owner,
|
|
431
|
+
repo,
|
|
432
|
+
ref: args.ref,
|
|
433
|
+
environment: args.environment || 'production',
|
|
434
|
+
task: args.task || 'deploy',
|
|
435
|
+
description: args.description,
|
|
436
|
+
autoMerge: Boolean(args.autoMerge),
|
|
437
|
+
requiredContexts: args.requiredContexts || [],
|
|
438
|
+
productionEnvironment: args.productionEnvironment,
|
|
439
|
+
})
|
|
440
|
+
},
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
tool(
|
|
444
|
+
'gh_deployment_status_create',
|
|
445
|
+
'Create a status for an existing deployment (e.g. "in_progress", "success", "failure", "error").',
|
|
446
|
+
repoParams({
|
|
447
|
+
deploymentId: { type: 'number', description: 'The numeric ID of the deployment. Required.' },
|
|
448
|
+
state: {
|
|
449
|
+
type: 'string',
|
|
450
|
+
description: 'State of the deployment: "error", "failure", "in_progress", "queued", "pending", "success". Required.'
|
|
451
|
+
},
|
|
452
|
+
description: { type: 'string', description: 'Short description of the status.' },
|
|
453
|
+
logUrl: { type: 'string', description: 'URL of the deployment logs.' },
|
|
454
|
+
environmentUrl: { type: 'string', description: 'URL for accessing the deployed environment.' },
|
|
455
|
+
autoInactive: { type: 'boolean', description: 'Add inactive state to previous non-transient, non-production deployments (default true).' },
|
|
456
|
+
}),
|
|
457
|
+
async (args) => {
|
|
458
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
459
|
+
return createDeploymentStatus(await client(), {
|
|
460
|
+
owner,
|
|
461
|
+
repo,
|
|
462
|
+
deploymentId: args.deploymentId,
|
|
463
|
+
state: args.state,
|
|
464
|
+
description: args.description,
|
|
465
|
+
logUrl: args.logUrl,
|
|
466
|
+
environmentUrl: args.environmentUrl,
|
|
467
|
+
autoInactive: args.autoInactive !== false,
|
|
468
|
+
})
|
|
469
|
+
},
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
// ------------------------------------------------------------------------ webhook simulator (#125)
|
|
473
|
+
|
|
474
|
+
tool(
|
|
475
|
+
'gh_webhook_simulate',
|
|
476
|
+
'Simulate a canonical GitHub webhook event (issues, pull_request, workflow_run, release, deployment, push). Emits to local SSE bus and optional HTTP target.',
|
|
477
|
+
repoParams({
|
|
478
|
+
event: {
|
|
479
|
+
type: 'string',
|
|
480
|
+
description: 'Event name: "issues", "pull_request", "workflow_run", "release", "deployment", "deployment_status", "push". Default "issues".'
|
|
481
|
+
},
|
|
482
|
+
action: { type: 'string', description: 'Action for event (e.g. "opened", "closed", "completed", "published"). Default "opened".' },
|
|
483
|
+
data: { type: 'object', additionalProperties: true, description: 'Optional custom payload properties to merge into canonical payload.' },
|
|
484
|
+
broadcast: { type: 'boolean', description: 'Whether to broadcast to local SSE event manager (default true).' },
|
|
485
|
+
targetUrl: { type: 'string', description: 'Optional webhook receiver endpoint URL to deliver payload via HTTP POST.' },
|
|
486
|
+
}),
|
|
487
|
+
async (args) => {
|
|
488
|
+
const { owner, repo } = await asRepo(args, liveConfig())
|
|
489
|
+
return simulateWebhook({
|
|
490
|
+
owner,
|
|
491
|
+
repo,
|
|
492
|
+
event: args.event || 'issues',
|
|
493
|
+
action: args.action || 'opened',
|
|
494
|
+
data: args.data || {},
|
|
495
|
+
broadcast: args.broadcast !== false,
|
|
496
|
+
targetUrl: args.targetUrl,
|
|
497
|
+
})
|
|
498
|
+
},
|
|
499
|
+
)
|
|
500
|
+
|
|
370
501
|
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// Webhook Simulator and Local Event Emitter.
|
|
2
|
+
// Generates canonical GitHub webhook payloads and emits to local SSE event bus.
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from 'node:crypto'
|
|
5
|
+
import { getGlobalEventManager } from '../events.js'
|
|
6
|
+
|
|
7
|
+
/** Canonical mock payload generator for GitHub webhook events. */
|
|
8
|
+
export function buildCanonicalWebhookPayload({
|
|
9
|
+
event = 'issues',
|
|
10
|
+
action = 'opened',
|
|
11
|
+
owner = 'goodandready',
|
|
12
|
+
repo = 'dsh-github-ops',
|
|
13
|
+
data = {},
|
|
14
|
+
} = {}) {
|
|
15
|
+
const deliveryId = randomUUID()
|
|
16
|
+
const timestamp = new Date().toISOString()
|
|
17
|
+
const repoFullName = `${owner}/${repo}`
|
|
18
|
+
|
|
19
|
+
const baseRepo = {
|
|
20
|
+
id: 123456789,
|
|
21
|
+
node_id: 'MDEwOlJlcG9zaXRvcnkxMjM0NTY3ODk=',
|
|
22
|
+
name: repo,
|
|
23
|
+
full_name: repoFullName,
|
|
24
|
+
private: false,
|
|
25
|
+
owner: {
|
|
26
|
+
login: owner,
|
|
27
|
+
id: 9876543,
|
|
28
|
+
avatar_url: 'https://avatars.githubusercontent.com/u/9876543?v=4',
|
|
29
|
+
html_url: `https://github.com/${owner}`,
|
|
30
|
+
},
|
|
31
|
+
html_url: `https://github.com/${repoFullName}`,
|
|
32
|
+
default_branch: 'main',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sender = {
|
|
36
|
+
login: 'agent-antigravity',
|
|
37
|
+
id: 424242,
|
|
38
|
+
avatar_url: 'https://avatars.githubusercontent.com/u/424242?v=4',
|
|
39
|
+
html_url: 'https://github.com/agent-antigravity',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let eventPayload = {}
|
|
43
|
+
|
|
44
|
+
switch (event) {
|
|
45
|
+
case 'issues': {
|
|
46
|
+
eventPayload = {
|
|
47
|
+
action,
|
|
48
|
+
issue: {
|
|
49
|
+
id: 1001,
|
|
50
|
+
number: 42,
|
|
51
|
+
title: 'Example Issue from Webhook Simulator',
|
|
52
|
+
user: sender,
|
|
53
|
+
labels: [{ id: 1, name: 'bug', color: 'd73a4a' }],
|
|
54
|
+
state: action === 'closed' ? 'closed' : 'open',
|
|
55
|
+
comments: 0,
|
|
56
|
+
created_at: timestamp,
|
|
57
|
+
updated_at: timestamp,
|
|
58
|
+
body: 'This is a simulated issue payload for testing.',
|
|
59
|
+
html_url: `https://github.com/${repoFullName}/issues/42`,
|
|
60
|
+
},
|
|
61
|
+
repository: baseRepo,
|
|
62
|
+
sender,
|
|
63
|
+
}
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case 'pull_request': {
|
|
68
|
+
eventPayload = {
|
|
69
|
+
action,
|
|
70
|
+
number: 101,
|
|
71
|
+
pull_request: {
|
|
72
|
+
id: 2002,
|
|
73
|
+
number: 101,
|
|
74
|
+
state: action === 'closed' ? 'closed' : 'open',
|
|
75
|
+
title: 'feat: simulated pull request payload',
|
|
76
|
+
user: sender,
|
|
77
|
+
body: 'Simulated PR body for local testing.',
|
|
78
|
+
created_at: timestamp,
|
|
79
|
+
updated_at: timestamp,
|
|
80
|
+
head: {
|
|
81
|
+
label: `${owner}:feature-sim`,
|
|
82
|
+
ref: 'feature-sim',
|
|
83
|
+
sha: '0123456789abcdef0123456789abcdef01234567',
|
|
84
|
+
},
|
|
85
|
+
base: {
|
|
86
|
+
label: `${owner}:main`,
|
|
87
|
+
ref: 'main',
|
|
88
|
+
sha: 'abcdef0123456789abcdef0123456789abcdef01',
|
|
89
|
+
},
|
|
90
|
+
merged: action === 'closed',
|
|
91
|
+
html_url: `https://github.com/${repoFullName}/pull/101`,
|
|
92
|
+
},
|
|
93
|
+
repository: baseRepo,
|
|
94
|
+
sender,
|
|
95
|
+
}
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case 'workflow_run': {
|
|
100
|
+
eventPayload = {
|
|
101
|
+
action,
|
|
102
|
+
workflow_run: {
|
|
103
|
+
id: 3003,
|
|
104
|
+
name: 'CI Build & Test',
|
|
105
|
+
head_branch: 'main',
|
|
106
|
+
head_sha: '0123456789abcdef0123456789abcdef01234567',
|
|
107
|
+
status: action === 'completed' ? 'completed' : 'in_progress',
|
|
108
|
+
conclusion: action === 'completed' ? 'success' : null,
|
|
109
|
+
html_url: `https://github.com/${repoFullName}/actions/runs/3003`,
|
|
110
|
+
created_at: timestamp,
|
|
111
|
+
updated_at: timestamp,
|
|
112
|
+
},
|
|
113
|
+
repository: baseRepo,
|
|
114
|
+
sender,
|
|
115
|
+
}
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case 'release': {
|
|
120
|
+
eventPayload = {
|
|
121
|
+
action,
|
|
122
|
+
release: {
|
|
123
|
+
id: 4004,
|
|
124
|
+
tag_name: 'v1.0.0',
|
|
125
|
+
name: 'Release v1.0.0',
|
|
126
|
+
draft: false,
|
|
127
|
+
prerelease: false,
|
|
128
|
+
created_at: timestamp,
|
|
129
|
+
published_at: timestamp,
|
|
130
|
+
html_url: `https://github.com/${repoFullName}/releases/tag/v1.0.0`,
|
|
131
|
+
},
|
|
132
|
+
repository: baseRepo,
|
|
133
|
+
sender,
|
|
134
|
+
}
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case 'deployment': {
|
|
139
|
+
eventPayload = {
|
|
140
|
+
action,
|
|
141
|
+
deployment: {
|
|
142
|
+
id: 5005,
|
|
143
|
+
sha: '0123456789abcdef0123456789abcdef01234567',
|
|
144
|
+
ref: 'main',
|
|
145
|
+
task: 'deploy',
|
|
146
|
+
environment: 'production',
|
|
147
|
+
description: 'Deploying to production server',
|
|
148
|
+
created_at: timestamp,
|
|
149
|
+
updated_at: timestamp,
|
|
150
|
+
},
|
|
151
|
+
repository: baseRepo,
|
|
152
|
+
sender,
|
|
153
|
+
}
|
|
154
|
+
break
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case 'deployment_status': {
|
|
158
|
+
eventPayload = {
|
|
159
|
+
action,
|
|
160
|
+
deployment_status: {
|
|
161
|
+
id: 6006,
|
|
162
|
+
state: 'success',
|
|
163
|
+
description: 'Deployment finished successfully',
|
|
164
|
+
target_url: 'https://goodandready.app',
|
|
165
|
+
created_at: timestamp,
|
|
166
|
+
updated_at: timestamp,
|
|
167
|
+
},
|
|
168
|
+
deployment: {
|
|
169
|
+
id: 5005,
|
|
170
|
+
environment: 'production',
|
|
171
|
+
},
|
|
172
|
+
repository: baseRepo,
|
|
173
|
+
sender,
|
|
174
|
+
}
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case 'push': {
|
|
179
|
+
eventPayload = {
|
|
180
|
+
ref: 'refs/heads/main',
|
|
181
|
+
before: 'abcdef0123456789abcdef0123456789abcdef01',
|
|
182
|
+
after: '0123456789abcdef0123456789abcdef01234567',
|
|
183
|
+
commits: [
|
|
184
|
+
{
|
|
185
|
+
id: '0123456789abcdef0123456789abcdef01234567',
|
|
186
|
+
message: 'feat: add exciting new capability',
|
|
187
|
+
timestamp,
|
|
188
|
+
author: { name: 'Vadim', email: 'vadim@goodandready.app' },
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
repository: baseRepo,
|
|
192
|
+
sender,
|
|
193
|
+
}
|
|
194
|
+
break
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
default: {
|
|
198
|
+
eventPayload = {
|
|
199
|
+
action,
|
|
200
|
+
event,
|
|
201
|
+
repository: baseRepo,
|
|
202
|
+
sender,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const payload = {
|
|
208
|
+
...eventPayload,
|
|
209
|
+
...data,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { deliveryId, timestamp, payload }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Simulate a webhook event, emit to local SSE bus and optionally dispatch to HTTP receiver. */
|
|
216
|
+
export async function simulateWebhook({
|
|
217
|
+
event = 'issues',
|
|
218
|
+
action = 'opened',
|
|
219
|
+
owner = 'goodandready',
|
|
220
|
+
repo = 'dsh-github-ops',
|
|
221
|
+
data = {},
|
|
222
|
+
broadcast = true,
|
|
223
|
+
targetUrl,
|
|
224
|
+
eventManager,
|
|
225
|
+
fetchFn = (typeof fetch !== 'undefined' ? fetch : null),
|
|
226
|
+
} = {}) {
|
|
227
|
+
const { deliveryId, timestamp, payload } = buildCanonicalWebhookPayload({
|
|
228
|
+
event,
|
|
229
|
+
action,
|
|
230
|
+
owner,
|
|
231
|
+
repo,
|
|
232
|
+
data,
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
let broadcastCount = 0
|
|
236
|
+
if (broadcast) {
|
|
237
|
+
const mgr = eventManager || getGlobalEventManager()
|
|
238
|
+
if (mgr && typeof mgr.broadcast === 'function') {
|
|
239
|
+
mgr.broadcast('webhook', {
|
|
240
|
+
event,
|
|
241
|
+
action,
|
|
242
|
+
deliveryId,
|
|
243
|
+
timestamp,
|
|
244
|
+
payload,
|
|
245
|
+
})
|
|
246
|
+
mgr.broadcast(event, payload)
|
|
247
|
+
broadcastCount = mgr.clientCount || 1
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let delivered = false
|
|
252
|
+
let targetStatus = null
|
|
253
|
+
let deliveryError = null
|
|
254
|
+
|
|
255
|
+
if (targetUrl && typeof fetchFn === 'function') {
|
|
256
|
+
try {
|
|
257
|
+
const res = await fetchFn(targetUrl, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: {
|
|
260
|
+
'Content-Type': 'application/json',
|
|
261
|
+
'User-Agent': 'GitHub-Hookshot/dsh-github-ops',
|
|
262
|
+
'X-GitHub-Event': event,
|
|
263
|
+
'X-GitHub-Delivery': deliveryId,
|
|
264
|
+
'X-GitHub-Hook-ID': '9999999',
|
|
265
|
+
},
|
|
266
|
+
body: JSON.stringify(payload),
|
|
267
|
+
})
|
|
268
|
+
delivered = Boolean(res && res.ok)
|
|
269
|
+
targetStatus = res ? res.status : null
|
|
270
|
+
} catch (err) {
|
|
271
|
+
deliveryError = String((err && err.message) || err)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
simulated: true,
|
|
277
|
+
event,
|
|
278
|
+
action,
|
|
279
|
+
deliveryId,
|
|
280
|
+
timestamp,
|
|
281
|
+
broadcasted: broadcast && broadcastCount > 0,
|
|
282
|
+
broadcastClients: broadcastCount,
|
|
283
|
+
targetUrl: targetUrl || null,
|
|
284
|
+
delivered,
|
|
285
|
+
targetStatus,
|
|
286
|
+
deliveryError,
|
|
287
|
+
payload,
|
|
288
|
+
}
|
|
289
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-github-ops",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
4
4
|
"description": "GitHub operations for DeepSeek Harness: releases, tags, a guarded generic API pass-through, workflow runs, secrets and sanitized mirror publication.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|