@perrylink/dsh-github 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.es.md +2 -0
- package/README.hi.md +2 -0
- package/README.md +2 -0
- package/README.pt.md +2 -0
- package/README.zh-CN.md +2 -0
- package/package.json +1 -1
- package/scripts/action-patch.mjs +152 -138
- package/scripts/action-post.mjs +61 -48
- package/scripts/local-test.mjs +259 -0
package/README.es.md
CHANGED
|
@@ -240,6 +240,8 @@ pnpm run check:readmes # cross-checks TOC anchors, tools, and config keys in a
|
|
|
240
240
|
|
|
241
241
|
Las pruebas simulan la API de GitHub, la CLI `gh` y git mediante runners inyectados — sin red, sin credenciales reales. `test/security.test.ts` verifica que la cadena del token nunca aparece en ninguna salida visible para el modelo o para el humano. `test/e2e.test.ts` contiene pruebas de humo optativas de la API real que se omiten automáticamente salvo que `DSH_GITHUB_E2E_TOKEN` esté definido (solo endpoints de solo lectura).
|
|
242
242
|
|
|
243
|
+
Para ejercitar la acción compuesta localmente, ejecuta `node scripts/local-test.mjs --owner-repo you/repo --pr 42` (consulta `--help` para todas las opciones). El simulador fija explícitamente `DSH_HOME`, `DSH_PROFILE_DIR`, `RUNNER_TEMP` y el directorio de salida dentro de un sandbox nuevo del directorio temporal del sistema para cada paso — tu dsh home real nunca se lee ni se escribe, incluso si existe un `DSH_HOME` de ámbito máquina — y reproduce los pasos install → prepare → run headless → post de `action.yml`. `action-patch.mjs` y `action-post.mjs` se niegan a ejecutarse fuera de un runner de GitHub Actions, de modo que la acción no puede escribir overlays de perfil ni informes en ubicaciones locales desconocidas.
|
|
244
|
+
|
|
243
245
|
## 🗂 Estructura del repositorio
|
|
244
246
|
|
|
245
247
|
```
|
package/README.hi.md
CHANGED
|
@@ -240,6 +240,8 @@ pnpm run check:readmes # cross-checks TOC anchors, tools, and config keys in a
|
|
|
240
240
|
|
|
241
241
|
Tests injected runners के ज़रिए GitHub API, `gh` CLI, और git को mock करते हैं — कोई network नहीं, कोई real credentials नहीं। `test/security.test.ts` पुष्टि करता है कि token string किसी भी model- या human-visible output में कभी नहीं आता। `test/e2e.test.ts` में opt-in real-API smoke tests हैं जो `DSH_GITHUB_E2E_TOKEN` सेट न होने पर खुद को skip कर लेते हैं (केवल read-only endpoints)।
|
|
242
242
|
|
|
243
|
+
Composite action को locally आज़माने के लिए `node scripts/local-test.mjs --owner-repo you/repo --pr 42` चलाएँ (सभी options के लिए `--help` देखें)। Simulator हर spawned step के लिए `DSH_HOME`, `DSH_PROFILE_DIR`, `RUNNER_TEMP`, और output directory को system temp directory के एक नए sandbox में स्पष्ट रूप से fix करता है — आपका real dsh home कभी पढ़ा या लिखा नहीं जाता, चाहे machine-scope `DSH_HOME` मौजूद हो — और `action.yml` के install → prepare → headless run → post चरणों को replay करता है। `action-patch.mjs` और `action-post.mjs` GitHub Actions runner के बाहर चलने से मना कर देते हैं, इसलिए action किसी अज्ञात local location में profile overlay या report नहीं लिख सकता।
|
|
244
|
+
|
|
243
245
|
## 🗂 रिपॉज़िटरी संरचना
|
|
244
246
|
|
|
245
247
|
```
|
package/README.md
CHANGED
|
@@ -242,6 +242,8 @@ pnpm run check:readmes # cross-checks TOC anchors, tools, and config keys in a
|
|
|
242
242
|
|
|
243
243
|
Tests mock the GitHub API, the `gh` CLI, and git through injected runners — no network, no real credentials. `test/security.test.ts` asserts the token string never appears in any model- or human-visible output. `test/e2e.test.ts` contains opt-in real-API smoke tests that self-skip unless `DSH_GITHUB_E2E_TOKEN` is set (read-only endpoints only; the dedicated variable keeps the unit suite hermetic).
|
|
244
244
|
|
|
245
|
+
To exercise the composite action locally, run `node scripts/local-test.mjs --owner-repo you/repo --pr 42` (see `--help` for all options). The simulator hardcodes `DSH_HOME`, `DSH_PROFILE_DIR`, `RUNNER_TEMP`, and the output directory under a fresh system-temp sandbox for every spawned step — your real dsh home is never read or written, even when a machine-scope `DSH_HOME` exists — and replays the install → prepare → headless run → post steps of `action.yml`. `action-patch.mjs` and `action-post.mjs` refuse to run outside a GitHub Actions runner, so the action cannot write profile overlays or reports into unknown local locations.
|
|
246
|
+
|
|
245
247
|
## 🗂 Repository layout
|
|
246
248
|
|
|
247
249
|
```
|
package/README.pt.md
CHANGED
|
@@ -240,6 +240,8 @@ pnpm run check:readmes # cross-checks TOC anchors, tools, and config keys in a
|
|
|
240
240
|
|
|
241
241
|
Os testes simulam a API do GitHub, a CLI `gh` e o git por meio de runners injetados — sem rede, sem credenciais reais. `test/security.test.ts` garante que a string do token nunca aparece em nenhuma saída visível ao modelo ou ao humano. `test/e2e.test.ts` contém testes de fumaça optativos da API real que se pulam automaticamente a menos que `DSH_GITHUB_E2E_TOKEN` esteja definido (apenas endpoints somente leitura).
|
|
242
242
|
|
|
243
|
+
Para exercitar a ação composta localmente, execute `node scripts/local-test.mjs --owner-repo you/repo --pr 42` (veja `--help` para todas as opções). O simulador fixa explicitamente `DSH_HOME`, `DSH_PROFILE_DIR`, `RUNNER_TEMP` e o diretório de saída em um sandbox novo do diretório temporário do sistema para cada etapa — seu dsh home real nunca é lido nem gravado, mesmo que exista um `DSH_HOME` de escopo de máquina — e reproduz os passos install → prepare → execução headless → post do `action.yml`. `action-patch.mjs` e `action-post.mjs` se recusam a executar fora de um runner do GitHub Actions, de modo que a ação não pode gravar overlays de perfil nem relatórios em locais desconhecidos.
|
|
244
|
+
|
|
243
245
|
## 🗂 Estrutura do repositório
|
|
244
246
|
|
|
245
247
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -239,6 +239,8 @@ pnpm run check:readmes # 交叉检查 5 个 README 的目录锚点、工具与
|
|
|
239
239
|
|
|
240
240
|
测试通过注入的 runner mock 掉 GitHub API、`gh` CLI 与 git——不联网、不用真实凭证。`test/security.test.ts` 断言 token 字符串不出现在任何模型或人类可见输出中。`test/e2e.test.ts` 是可选真实 API 冒烟测试:未设置 `DSH_GITHUB_E2E_TOKEN` 时自动跳过(只打只读端点;独立变量保证单测套件与环境隔离)。
|
|
241
241
|
|
|
242
|
+
要在本地演练 composite action,运行 `node scripts/local-test.mjs --owner-repo you/repo --pr 42`(全部选项见 `--help`)。模拟器为每个子进程显式写死 `DSH_HOME`、`DSH_PROFILE_DIR`、`RUNNER_TEMP` 与输出目录——全部位于全新的系统临时目录沙箱内,即使存在 Machine 级 `DSH_HOME` 也绝不读写你真实的 dsh home——并按 `action.yml` 的顺序回放 install → prepare → headless run → post 四步。`action-patch.mjs` 与 `action-post.mjs` 在 GitHub Actions runner 之外一律拒绝运行,因此 action 不会把 profile overlay 或报告写到未知的本地位置。
|
|
243
|
+
|
|
242
244
|
## 🗂 目录结构
|
|
243
245
|
|
|
244
246
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@perrylink/dsh-github",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Official-grade GitHub CI for DeepSeek Harness: composite action (review PRs / fix CI / report), polling PR review bot with idempotent inline comments, a status-check gate, and approval-gated PR/issue/repo/file tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/scripts/action-patch.mjs
CHANGED
|
@@ -1,138 +1,152 @@
|
|
|
1
|
-
// Generates the dsh headless profile overlay and the task text for the
|
|
2
|
-
// composite action. Zero-dependency Node: every input arrives through the
|
|
3
|
-
// runner's INPUT_* environment variables (composite-action inputs), and every
|
|
4
|
-
// value is emitted as a JSON string (a valid YAML scalar) so arbitrary user
|
|
5
|
-
// input can never break the YAML structure.
|
|
6
|
-
//
|
|
7
|
-
// Writes to $INPUT_OUTPUT_DIR:
|
|
8
|
-
// dsh-github-ci.cordis.yml — the --patch overlay (Minimal persona + role
|
|
9
|
-
// line, model selection, sandbox, the minimal bash/editor toolset, and
|
|
10
|
-
// the dsh-github row with the full ci.* configuration).
|
|
11
|
-
// task.txt — the headless task for the selected mode.
|
|
12
|
-
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
13
|
-
import { join } from 'node:path'
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
1
|
+
// Generates the dsh headless profile overlay and the task text for the
|
|
2
|
+
// composite action. Zero-dependency Node: every input arrives through the
|
|
3
|
+
// runner's INPUT_* environment variables (composite-action inputs), and every
|
|
4
|
+
// value is emitted as a JSON string (a valid YAML scalar) so arbitrary user
|
|
5
|
+
// input can never break the YAML structure.
|
|
6
|
+
//
|
|
7
|
+
// Writes to $INPUT_OUTPUT_DIR:
|
|
8
|
+
// dsh-github-ci.cordis.yml — the --patch overlay (Minimal persona + role
|
|
9
|
+
// line, model selection, sandbox, the minimal bash/editor toolset, and
|
|
10
|
+
// the dsh-github row with the full ci.* configuration).
|
|
11
|
+
// task.txt — the headless task for the selected mode.
|
|
12
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
// Local-machine protection: the composite action resolves every path from the
|
|
16
|
+
// runner environment (RUNNER_TEMP for the output directory, GITHUB_WORKSPACE
|
|
17
|
+
// for the sandbox root). Outside a GitHub Actions runner those variables do
|
|
18
|
+
// not exist, and every fallback would silently write the profile overlay and
|
|
19
|
+
// task into the current working directory while a locally spawned `dsh`
|
|
20
|
+
// inherits the developer's real DSH_HOME. Refuse to run instead of guessing.
|
|
21
|
+
const runnerTemp = (process.env.RUNNER_TEMP ?? '').trim()
|
|
22
|
+
const workspace = (process.env.GITHUB_WORKSPACE ?? '').trim()
|
|
23
|
+
if (runnerTemp === '' || workspace === '') {
|
|
24
|
+
console.error('dsh-github: refusing to run outside a GitHub Actions runner — both RUNNER_TEMP and GITHUB_WORKSPACE must be set.')
|
|
25
|
+
console.error('dsh-github: to exercise the action locally, use `node scripts/local-test.mjs`; it pins DSH_HOME, DSH_PROFILE_DIR, and the output directory under the system temp directory.')
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const env = (name) => process.env[name] ?? ''
|
|
30
|
+
const boolOf = (value, fallback) => value.trim() === '' ? fallback : value.trim() === 'true' || value.trim() === '1'
|
|
31
|
+
const listOf = (value) => value.split(',').map(item => item.trim()).filter(item => item.length > 0)
|
|
32
|
+
const y = (value) => JSON.stringify(value)
|
|
33
|
+
|
|
34
|
+
const task = env('INPUT_TASK').trim() || 'review'
|
|
35
|
+
const pr = env('INPUT_PR').trim()
|
|
36
|
+
const ownerRepo = env('INPUT_OWNER_REPO').trim()
|
|
37
|
+
const model = env('INPUT_MODEL').trim() || 'deepseek-v4-flash'
|
|
38
|
+
const engine = env('INPUT_ENGINE').trim() === 'model' ? 'model' : 'static'
|
|
39
|
+
const checkName = env('INPUT_CHECK_NAME').trim() || 'dsh-github-review'
|
|
40
|
+
const blocking = boolOf(env('INPUT_BLOCKING'), true)
|
|
41
|
+
const failOn = env('INPUT_FAIL_ON').trim() === 'warning' ? 'warning' : 'error'
|
|
42
|
+
const labelFilters = listOf(env('INPUT_LABEL_FILTERS'))
|
|
43
|
+
const pathFilters = listOf(env('INPUT_PATH_FILTERS'))
|
|
44
|
+
const maxDiffChars = Number(env('INPUT_MAX_DIFF_CHARS')) > 0 ? Math.floor(Number(env('INPUT_MAX_DIFF_CHARS'))) : 8000
|
|
45
|
+
const postComments = boolOf(env('INPUT_POST_COMMENTS'), true)
|
|
46
|
+
const postCheck = boolOf(env('INPUT_POST_CHECK'), true)
|
|
47
|
+
const requestTimeoutMs = Number(env('INPUT_REQUEST_TIMEOUT_MS')) > 0 ? Math.floor(Number(env('INPUT_REQUEST_TIMEOUT_MS'))) : 30000
|
|
48
|
+
const outputDir = env('INPUT_OUTPUT_DIR').trim() || join(process.env.RUNNER_TEMP ?? '.', 'dsh-github')
|
|
49
|
+
const customTask = env('INPUT_TASK_PROMPT').trim()
|
|
50
|
+
|
|
51
|
+
if ((task === 'review' || task === 'fix-ci') && pr === '') {
|
|
52
|
+
console.error('dsh-github: the `pr` input is required for the review and fix-ci tasks (defaults to github.event.pull_request.number on pull_request events)')
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
if (ownerRepo === '') {
|
|
56
|
+
console.error('dsh-github: the `owner-repo` input is required (defaults to github.repository)')
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
mkdirSync(outputDir, { recursive: true })
|
|
61
|
+
|
|
62
|
+
const callJson = (extra) => JSON.stringify({ task: extra.task, pr, ownerRepo, ...extra })
|
|
63
|
+
|
|
64
|
+
let taskText
|
|
65
|
+
if (customTask !== '') {
|
|
66
|
+
taskText = customTask
|
|
67
|
+
} else if (task === 'review' && engine === 'static') {
|
|
68
|
+
taskText = `You are the dsh-github CI reviewer for this repository. Call the ci_run tool once with ${callJson({ task: 'review' })} and then reply with only the verdict and the summary line from its result.`
|
|
69
|
+
} else if (task === 'review') {
|
|
70
|
+
taskText = `You are a careful software engineer reviewing a GitHub pull request. First call ci_run with ${callJson({ task: 'analyze' })}. Review the diff yourself: correctness, security, style, and maintainability. Then call ci_run with ${callJson({ task: 'publish' })} plus a "body" holding your review (Markdown, verdict first, findings grouped by file) and a "findings" array for line-anchored problems you found. Finish by replying with only the verdict and the summary line from the publish result.`
|
|
71
|
+
} else if (task === 'fix-ci') {
|
|
72
|
+
taskText = `You are the dsh-github CI engineer for this repository. Investigate the CI failures of PR #${pr} in ${ownerRepo}: read the failing checks with gh_review (fields "ci"), inspect the code with gh_file and gh_repo, and use the bash tool only to read logs or run read-only diagnostics. Do NOT modify files. Write a fix plan as your final answer and call ci_run with ${callJson({ task: 'publish' })} plus a "body" holding that plan so it is posted as a review comment.`
|
|
73
|
+
} else {
|
|
74
|
+
taskText = `You are the dsh-github CI reporter for this repository. Generate a concise, factual report about ${ownerRepo}${pr !== '' ? ` PR #${pr}` : ''} using the read tools (gh_repo, gh_file, gh_review, gh_search). Write the report as your final answer and, if a pull request is being analyzed, call ci_run with ${callJson({ task: 'publish' })} plus a "body" holding the report so it is posted as a review comment.`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const personaText = 'You are a helpful software engineer assistant.\nYou are the dsh-github CI assistant for this repository.'
|
|
78
|
+
|
|
79
|
+
const overlay = `# dsh-github CI overlay — generated by the composite action; every value is
|
|
80
|
+
# emitted by scripts/action-patch.mjs from the action inputs. Do not edit.
|
|
81
|
+
# Rows that already exist in dsh-base are replaced by id; new rows ride an
|
|
82
|
+
# insert block (non-insert patches only replace existing rows).
|
|
83
|
+
- id: persona
|
|
84
|
+
name: '@deepseek-ai/dsh-persona'
|
|
85
|
+
config:
|
|
86
|
+
text: ${y(personaText)}
|
|
87
|
+
complete: true
|
|
88
|
+
includeRuntimeContext: false
|
|
89
|
+
|
|
90
|
+
- id: agent-default-model
|
|
91
|
+
name: '@deepseek-ai/dsh-agent-default-model'
|
|
92
|
+
config:
|
|
93
|
+
provider: deepseek-official
|
|
94
|
+
model: ${y(model)}
|
|
95
|
+
|
|
96
|
+
- id: llm-deepseek
|
|
97
|
+
name: '@deepseek-ai/dsh-llm-deepseek'
|
|
98
|
+
config:
|
|
99
|
+
apiKeyEnv: DEEPSEEK_API_KEY
|
|
100
|
+
|
|
101
|
+
- id: sandbox-policy
|
|
102
|
+
name: '@deepseek-ai/dsh-sandbox-policy'
|
|
103
|
+
config:
|
|
104
|
+
mode: workspace-write
|
|
105
|
+
workspaceRoot: !!js process.env.GITHUB_WORKSPACE ?? process.cwd()
|
|
106
|
+
|
|
107
|
+
# The minimal toolset (persistent bash + editor), matching the official
|
|
108
|
+
# minimal preset: enough to investigate CI failures and generate reports,
|
|
109
|
+
# bounded by the workspace-write sandbox above.
|
|
110
|
+
- insert:
|
|
111
|
+
- id: terminal
|
|
112
|
+
name: '@deepseek-ai/dsh-terminal'
|
|
113
|
+
|
|
114
|
+
- id: terminal-bash
|
|
115
|
+
name: '@deepseek-ai/dsh-terminal-bash'
|
|
116
|
+
config:
|
|
117
|
+
timeoutMs: 300000
|
|
118
|
+
|
|
119
|
+
- id: persistent-bash
|
|
120
|
+
name: '@deepseek-ai/dsh-tool-bash-persistent'
|
|
121
|
+
config:
|
|
122
|
+
timeoutMs: 300000
|
|
123
|
+
|
|
124
|
+
- id: str-replace-editor
|
|
125
|
+
name: '@deepseek-ai/dsh-tool-str-replace-editor'
|
|
126
|
+
config:
|
|
127
|
+
maxOutputChars: 16000
|
|
128
|
+
|
|
129
|
+
- id: dsh-github
|
|
130
|
+
name: '@perrylink/dsh-github'
|
|
131
|
+
config:
|
|
132
|
+
tokenSource: env
|
|
133
|
+
tokenRef: DSH_GITHUB_TOKEN
|
|
134
|
+
maxDiffChars: ${maxDiffChars}
|
|
135
|
+
requestTimeoutMs: ${requestTimeoutMs}
|
|
136
|
+
ci:
|
|
137
|
+
enabled: true
|
|
138
|
+
engine: ${y(engine)}
|
|
139
|
+
autoApprove: ['ci.run']
|
|
140
|
+
checkName: ${y(checkName)}
|
|
141
|
+
blocking: ${blocking}
|
|
142
|
+
failOn: ${y(failOn)}
|
|
143
|
+
pollIntervalMs: 0
|
|
144
|
+
labelFilters: ${y(labelFilters)}
|
|
145
|
+
pathFilters: ${y(pathFilters)}
|
|
146
|
+
postComments: ${postComments}
|
|
147
|
+
reportDir: ${y(outputDir)}
|
|
148
|
+
`
|
|
149
|
+
|
|
150
|
+
writeFileSync(join(outputDir, 'dsh-github-ci.cordis.yml'), overlay)
|
|
151
|
+
writeFileSync(join(outputDir, 'task.txt'), taskText + '\n')
|
|
152
|
+
console.log(`dsh-github: overlay and task written to ${outputDir} (task: ${task}, engine: ${engine})`)
|
package/scripts/action-post.mjs
CHANGED
|
@@ -1,48 +1,61 @@
|
|
|
1
|
-
// Post-process the CI run for the composite action: publish the step outputs
|
|
2
|
-
// from dsh-github-ci-result.json and enforce the blocking gate.
|
|
3
|
-
//
|
|
4
|
-
// Exit codes:
|
|
5
|
-
// 0 — report exists and the gate passed (or the verdict is non-blocking).
|
|
6
|
-
// 1 — report missing (the headless run never produced one), or
|
|
7
|
-
// blocking=true and the verdict is "needs-changes".
|
|
8
|
-
// The status check itself (published by the pipeline) is what gates branch
|
|
9
|
-
// protection; this exit code fails the CI job for blocking=false-style
|
|
10
|
-
// workflow-level enforcement and for downstream status reporting.
|
|
11
|
-
import { readFileSync, appendFileSync } from 'node:fs'
|
|
12
|
-
import { join } from 'node:path'
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
console.error(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
1
|
+
// Post-process the CI run for the composite action: publish the step outputs
|
|
2
|
+
// from dsh-github-ci-result.json and enforce the blocking gate.
|
|
3
|
+
//
|
|
4
|
+
// Exit codes:
|
|
5
|
+
// 0 — report exists and the gate passed (or the verdict is non-blocking).
|
|
6
|
+
// 1 — report missing (the headless run never produced one), or
|
|
7
|
+
// blocking=true and the verdict is "needs-changes".
|
|
8
|
+
// The status check itself (published by the pipeline) is what gates branch
|
|
9
|
+
// protection; this exit code fails the CI job for blocking=false-style
|
|
10
|
+
// workflow-level enforcement and for downstream status reporting.
|
|
11
|
+
import { readFileSync, appendFileSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
// Local-machine protection: the post step reads the CI result from the output
|
|
15
|
+
// directory and publishes step outputs through GITHUB_OUTPUT, both of which
|
|
16
|
+
// only exist on a GitHub Actions runner. Outside a runner the fallback would
|
|
17
|
+
// read an arbitrary `./dsh-github` directory next to the caller's cwd and
|
|
18
|
+
// report phantom verdicts. Refuse to run instead of guessing.
|
|
19
|
+
const runnerTemp = (process.env.RUNNER_TEMP ?? '').trim()
|
|
20
|
+
const workspace = (process.env.GITHUB_WORKSPACE ?? '').trim()
|
|
21
|
+
if (runnerTemp === '' || workspace === '') {
|
|
22
|
+
console.error('dsh-github: refusing to run outside a GitHub Actions runner — both RUNNER_TEMP and GITHUB_WORKSPACE must be set.')
|
|
23
|
+
console.error('dsh-github: to exercise the action locally, use `node scripts/local-test.mjs`; it pins DSH_HOME, DSH_PROFILE_DIR, and the output directory under the system temp directory.')
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const outputDir = process.env.INPUT_OUTPUT_DIR?.trim() || join(process.env.RUNNER_TEMP ?? '.', 'dsh-github')
|
|
28
|
+
const resultPath = join(outputDir, 'dsh-github-ci-result.json')
|
|
29
|
+
const blocking = (process.env.INPUT_BLOCKING ?? 'true').trim() === 'true'
|
|
30
|
+
const outputFile = process.env.GITHUB_OUTPUT ?? ''
|
|
31
|
+
|
|
32
|
+
let result
|
|
33
|
+
try {
|
|
34
|
+
result = JSON.parse(readFileSync(resultPath, 'utf8'))
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`dsh-github: no CI result at ${resultPath} — the headless run did not produce a review (${error instanceof Error ? error.message : String(error)}). Check the dsh stderr log next to it.`)
|
|
37
|
+
if (outputFile !== '') appendFileSync(outputFile, 'verdict=error\n')
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const markdownPath = join(outputDir, 'dsh-github-ci-summary.md')
|
|
42
|
+
const verdict = typeof result.verdict === 'string' ? result.verdict : 'error'
|
|
43
|
+
if (outputFile !== '') {
|
|
44
|
+
appendFileSync(outputFile,
|
|
45
|
+
`verdict=${verdict}\n`
|
|
46
|
+
+ `report-json=${resultPath}\n`
|
|
47
|
+
+ `report-markdown=${markdownPath}\n`
|
|
48
|
+
+ `${result.checkRun?.url !== undefined ? `check-url=${result.checkRun.url}\n` : ''}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(`dsh-github: verdict ${verdict} — PR #${result.pr} in ${result.repo}${result.alreadyReviewed ? ' (already reviewed at this head commit)' : ''}`)
|
|
52
|
+
console.log(`dsh-github: ${result.summary}`)
|
|
53
|
+
|
|
54
|
+
if (blocking && verdict === 'needs-changes') {
|
|
55
|
+
console.error(`dsh-github: blocking gate failed — the review verdict is "needs-changes". See ${markdownPath}`)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
if (verdict === 'error') {
|
|
59
|
+
console.error('dsh-github: the pipeline reported an error verdict')
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Local simulator for the composite action — explicitly isolated.
|
|
2
|
+
//
|
|
3
|
+
// The real action resolves every path from the runner environment:
|
|
4
|
+
// DSH_HOME = ${{ runner.temp }}/dsh-home
|
|
5
|
+
// DSH_PROFILE_DIR = ${{ runner.temp }}/dsh-home/profiles/headless
|
|
6
|
+
// output dir = ${{ inputs.output-dir }} (default ${{ runner.temp }}/dsh-github)
|
|
7
|
+
// A naive local copy inherits the developer's real environment instead: a
|
|
8
|
+
// process-level (or machine-scope) DSH_HOME wins over everything, so the
|
|
9
|
+
// headless profile, sessions, storages, and reports would be written straight
|
|
10
|
+
// into the real dsh home — which is exactly what shuts local content down.
|
|
11
|
+
//
|
|
12
|
+
// This script therefore never reads DSH_HOME / DSH_PROFILE_DIR / RUNNER_TEMP
|
|
13
|
+
// from the inherited environment. Every spawned step receives hardcoded
|
|
14
|
+
// process-level values rooted in a fresh system-temp sandbox:
|
|
15
|
+
// DSH_HOME = <tmp>/dsh-github-local-<n>/dsh-home
|
|
16
|
+
// DSH_PROFILE_DIR = <tmp>/dsh-github-local-<n>/dsh-home/profiles/headless
|
|
17
|
+
// RUNNER_TEMP = <tmp>/dsh-github-local-<n>
|
|
18
|
+
// GITHUB_WORKSPACE = --workspace (default: this repository root)
|
|
19
|
+
// INPUT_OUTPUT_DIR = <tmp>/dsh-github-local-<n>/output
|
|
20
|
+
//
|
|
21
|
+
// Steps replayed, mirroring action.yml: install → prepare (action-patch.mjs)
|
|
22
|
+
// → headless run → post (action-post.mjs). Nothing outside the sandbox is
|
|
23
|
+
// written. Run `node scripts/local-test.mjs --help` for the option list.
|
|
24
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { spawnSync } from 'node:child_process'
|
|
26
|
+
import { tmpdir } from 'node:os'
|
|
27
|
+
import { dirname, delimiter, isAbsolute, join, resolve } from 'node:path'
|
|
28
|
+
import { fileURLToPath } from 'node:url'
|
|
29
|
+
|
|
30
|
+
const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
31
|
+
|
|
32
|
+
const USAGE = `dsh-github local action simulator
|
|
33
|
+
|
|
34
|
+
Usage: node scripts/local-test.mjs [options]
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--task <review|fix-ci|report> CI task to simulate (default: review)
|
|
38
|
+
--pr <number> Pull request number (required for review/fix-ci)
|
|
39
|
+
--owner-repo <owner/repo> Repository to review (required for review/fix-ci)
|
|
40
|
+
--task-prompt <text> Complete replacement for the default task text
|
|
41
|
+
--model <id> Model for the headless session (default: deepseek-v4-flash)
|
|
42
|
+
--engine <static|model> Review engine (default: static)
|
|
43
|
+
--check-name <name> Status-check name (default: dsh-github-review)
|
|
44
|
+
--blocking <true|false> Fail on needs-changes verdict (default: true)
|
|
45
|
+
--fail-on <error|warning> Lowest severity that flips the verdict (default: error)
|
|
46
|
+
--label-filters <a,b> Comma-separated label filters
|
|
47
|
+
--path-filters <glob,glob> Comma-separated path filters
|
|
48
|
+
--max-diff-chars <n> Diff character cap (default: 8000)
|
|
49
|
+
--post-comments <true|false> Post inline review comments (default: true)
|
|
50
|
+
--post-check <true|false> Publish the status check (default: true)
|
|
51
|
+
--request-timeout-ms <n> Per GitHub request timeout (default: 30000)
|
|
52
|
+
--plugin-version <version|path> @perrylink/dsh-github version or local folder
|
|
53
|
+
(default: latest)
|
|
54
|
+
--workspace <dir> GITHUB_WORKSPACE for the sandbox (default: repo root)
|
|
55
|
+
--skip-install Skip the npm install of dsh-github into the profile
|
|
56
|
+
--skip-run Only install + generate the overlay/task (no headless run)
|
|
57
|
+
--clean Delete the temp sandbox when finished
|
|
58
|
+
|
|
59
|
+
Environment (passed through from your shell, never logged):
|
|
60
|
+
DEEPSEEK_API_KEY Required (the action's deepseek-api-key input)
|
|
61
|
+
GITHUB_TOKEN | DSH_GITHUB_TOKEN Optional (the action's github-token input)
|
|
62
|
+
|
|
63
|
+
Isolation: DSH_HOME, DSH_PROFILE_DIR, RUNNER_TEMP, and the output directory are
|
|
64
|
+
hardcoded under the system temp directory for every spawned step, overriding any
|
|
65
|
+
inherited DSH_HOME (including machine-scope values). Your real dsh home is never
|
|
66
|
+
read or written.`
|
|
67
|
+
|
|
68
|
+
const args = process.argv.slice(2)
|
|
69
|
+
const options = {
|
|
70
|
+
task: 'review',
|
|
71
|
+
pr: '',
|
|
72
|
+
ownerRepo: '',
|
|
73
|
+
taskPrompt: '',
|
|
74
|
+
model: 'deepseek-v4-flash',
|
|
75
|
+
engine: 'static',
|
|
76
|
+
checkName: 'dsh-github-review',
|
|
77
|
+
blocking: 'true',
|
|
78
|
+
failOn: 'error',
|
|
79
|
+
labelFilters: '',
|
|
80
|
+
pathFilters: '',
|
|
81
|
+
maxDiffChars: '8000',
|
|
82
|
+
postComments: 'true',
|
|
83
|
+
postCheck: 'true',
|
|
84
|
+
requestTimeoutMs: '30000',
|
|
85
|
+
pluginVersion: 'latest',
|
|
86
|
+
workspace: repoRoot,
|
|
87
|
+
skipInstall: false,
|
|
88
|
+
skipRun: false,
|
|
89
|
+
clean: false,
|
|
90
|
+
}
|
|
91
|
+
const valueOf = { task: 1, pr: 1, 'owner-repo': 1, 'task-prompt': 1, model: 1, engine: 1, 'check-name': 1, blocking: 1, 'fail-on': 1, 'label-filters': 1, 'path-filters': 1, 'max-diff-chars': 1, 'post-comments': 1, 'post-check': 1, 'request-timeout-ms': 1, 'plugin-version': 1, workspace: 1 }
|
|
92
|
+
const keyOf = { task: 'task', pr: 'pr', 'owner-repo': 'ownerRepo', 'task-prompt': 'taskPrompt', model: 'model', engine: 'engine', 'check-name': 'checkName', blocking: 'blocking', 'fail-on': 'failOn', 'label-filters': 'labelFilters', 'path-filters': 'pathFilters', 'max-diff-chars': 'maxDiffChars', 'post-comments': 'postComments', 'post-check': 'postCheck', 'request-timeout-ms': 'requestTimeoutMs', 'plugin-version': 'pluginVersion', workspace: 'workspace' }
|
|
93
|
+
for (let i = 0; i < args.length; i++) {
|
|
94
|
+
const arg = args[i]
|
|
95
|
+
if (arg === '--help' || arg === '-h') { console.log(USAGE); process.exit(0) }
|
|
96
|
+
if (arg === '--skip-install') { options.skipInstall = true; continue }
|
|
97
|
+
if (arg === '--skip-run') { options.skipRun = true; continue }
|
|
98
|
+
if (arg === '--clean') { options.clean = true; continue }
|
|
99
|
+
const eq = arg.indexOf('=')
|
|
100
|
+
const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq)
|
|
101
|
+
if (name in valueOf) {
|
|
102
|
+
const inline = eq === -1 ? '' : arg.slice(eq + 1)
|
|
103
|
+
const value = inline !== '' ? inline : args[++i] ?? ''
|
|
104
|
+
options[keyOf[name]] = value
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
if (arg.startsWith('-')) {
|
|
108
|
+
console.error(`dsh-github local-test: unknown option ${arg}`)
|
|
109
|
+
console.error('Run `node scripts/local-test.mjs --help` for the option list.')
|
|
110
|
+
process.exit(2)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Sandbox — hardcoded under the system temp directory, derived from nothing
|
|
115
|
+
// that the developer's environment could point at their real home.
|
|
116
|
+
const sandboxRoot = mkdtempSync(join(tmpdir(), 'dsh-github-local-'))
|
|
117
|
+
const dshHome = join(sandboxRoot, 'dsh-home')
|
|
118
|
+
const profileDir = join(dshHome, 'profiles', 'headless')
|
|
119
|
+
const outputDir = join(sandboxRoot, 'output')
|
|
120
|
+
const workspace = isAbsolute(options.workspace) ? options.workspace : resolve(options.workspace)
|
|
121
|
+
|
|
122
|
+
console.log('dsh-github local action simulator — isolated sandbox')
|
|
123
|
+
console.log(` sandbox: ${sandboxRoot}`)
|
|
124
|
+
console.log(` DSH_HOME: ${dshHome}`)
|
|
125
|
+
console.log(` DSH_PROFILE_DIR:${profileDir}`)
|
|
126
|
+
console.log(` output dir: ${outputDir}`)
|
|
127
|
+
console.log(` workspace: ${workspace}`)
|
|
128
|
+
console.log('The real dsh home and any inherited DSH_HOME are never read or written.')
|
|
129
|
+
|
|
130
|
+
/** Run one shell command line, inheriting the caller's environment plus explicit overrides. */
|
|
131
|
+
function run(commandLine, { env = {}, allowFail = false, capture = false } = {}) {
|
|
132
|
+
const result = spawnSync(commandLine, {
|
|
133
|
+
shell: true,
|
|
134
|
+
cwd: repoRoot,
|
|
135
|
+
env: { ...process.env, ...env },
|
|
136
|
+
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
137
|
+
encoding: 'utf8',
|
|
138
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
139
|
+
})
|
|
140
|
+
const status = typeof result.status === 'number' ? result.status : 1
|
|
141
|
+
if (!allowFail && status !== 0) {
|
|
142
|
+
console.error(`dsh-github local-test: command failed (exit ${status}): ${commandLine}`)
|
|
143
|
+
process.exit(status)
|
|
144
|
+
}
|
|
145
|
+
return { status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Locate an executable (with a .cmd/.exe/.bat shim on Windows) on PATH. */
|
|
149
|
+
function findOnPath(name) {
|
|
150
|
+
const entries = (process.env.PATH ?? '').split(delimiter)
|
|
151
|
+
const extensions = process.platform === 'win32' ? ['cmd', 'exe', 'bat', ''] : ['']
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
for (const extension of extensions) {
|
|
154
|
+
const candidate = join(entry, extension === '' ? name : `${name}.${extension}`)
|
|
155
|
+
if (candidate !== name && existsSync(candidate)) return candidate
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return ''
|
|
159
|
+
}
|
|
160
|
+
const q = (value) => JSON.stringify(value)
|
|
161
|
+
|
|
162
|
+
// ---- Step 1: install the profile (action.yml "Install dsh and dsh-github") ----
|
|
163
|
+
mkdirSync(profileDir, { recursive: true })
|
|
164
|
+
writeFileSync(join(profileDir, 'package.json'), '{"private": true}\n')
|
|
165
|
+
const installEnv = { DSH_HOME: dshHome, DSH_PROFILE_DIR: profileDir }
|
|
166
|
+
if (options.skipInstall) {
|
|
167
|
+
console.log('skipping npm install (--skip-install): the overlay row @perrylink/dsh-github must resolve from the profile node_modules')
|
|
168
|
+
} else {
|
|
169
|
+
console.log(`installing @perrylink/dsh-github@${options.pluginVersion} into the sandboxed profile …`)
|
|
170
|
+
const target = existsSync(options.pluginVersion) || options.pluginVersion.includes('/') || options.pluginVersion.includes('\\')
|
|
171
|
+
? resolve(options.pluginVersion)
|
|
172
|
+
: `@perrylink/dsh-github@${options.pluginVersion}`
|
|
173
|
+
run(`npm install --prefix ${q(profileDir)} --legacy-peer-deps --package-lock=false --no-save --no-audit --no-fund ${q(target)}`, { env: installEnv })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---- Step 2: prepare (action.yml "Generate the dsh profile overlay and task") ----
|
|
177
|
+
mkdirSync(outputDir, { recursive: true })
|
|
178
|
+
const inputs = {
|
|
179
|
+
INPUT_TASK: options.task,
|
|
180
|
+
INPUT_PR: options.pr,
|
|
181
|
+
INPUT_OWNER_REPO: options.ownerRepo,
|
|
182
|
+
INPUT_MODEL: options.model,
|
|
183
|
+
INPUT_ENGINE: options.engine,
|
|
184
|
+
INPUT_CHECK_NAME: options.checkName,
|
|
185
|
+
INPUT_BLOCKING: options.blocking,
|
|
186
|
+
INPUT_FAIL_ON: options.failOn,
|
|
187
|
+
INPUT_LABEL_FILTERS: options.labelFilters,
|
|
188
|
+
INPUT_PATH_FILTERS: options.pathFilters,
|
|
189
|
+
INPUT_MAX_DIFF_CHARS: options.maxDiffChars,
|
|
190
|
+
INPUT_POST_COMMENTS: options.postComments,
|
|
191
|
+
INPUT_POST_CHECK: options.postCheck,
|
|
192
|
+
INPUT_REQUEST_TIMEOUT_MS: options.requestTimeoutMs,
|
|
193
|
+
INPUT_TASK_PROMPT: options.taskPrompt,
|
|
194
|
+
INPUT_OUTPUT_DIR: outputDir,
|
|
195
|
+
}
|
|
196
|
+
run(`node ${q(join(repoRoot, 'scripts', 'action-patch.mjs'))}`, {
|
|
197
|
+
env: { RUNNER_TEMP: sandboxRoot, GITHUB_WORKSPACE: workspace, ...inputs },
|
|
198
|
+
})
|
|
199
|
+
console.log(`overlay and task written: ${join(outputDir, 'dsh-github-ci.cordis.yml')}`)
|
|
200
|
+
|
|
201
|
+
if (options.skipRun) {
|
|
202
|
+
console.log('stopping after prepare (--skip-run); the sandbox is kept for inspection.')
|
|
203
|
+
console.log(`sandbox: ${sandboxRoot}`)
|
|
204
|
+
process.exit(0)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---- Step 3: headless run (action.yml "Run dsh headless") ----
|
|
208
|
+
if ((process.env.DEEPSEEK_API_KEY ?? '').trim() === '') {
|
|
209
|
+
console.error('dsh-github local-test: DEEPSEEK_API_KEY is required (the action\'s deepseek-api-key input). Set it in your shell, not in the profile.')
|
|
210
|
+
console.error(`sandbox kept for inspection: ${sandboxRoot}`)
|
|
211
|
+
process.exit(2)
|
|
212
|
+
}
|
|
213
|
+
if (findOnPath('dsh') === '') {
|
|
214
|
+
console.error('dsh-github local-test: the dsh CLI was not found on PATH. Install it first: npm install --global @deepseek-ai/dsh')
|
|
215
|
+
console.error(`sandbox kept for inspection: ${sandboxRoot}`)
|
|
216
|
+
process.exit(2)
|
|
217
|
+
}
|
|
218
|
+
const githubToken = process.env.DSH_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN ?? ''
|
|
219
|
+
if (githubToken === '' && options.postComments === 'true') {
|
|
220
|
+
console.log('note: no DSH_GITHUB_TOKEN / GITHUB_TOKEN in the environment — posting comments and the status check will fail per operation (static dry inspections still work).')
|
|
221
|
+
}
|
|
222
|
+
const taskText = readFileSync(join(outputDir, 'task.txt'), 'utf8')
|
|
223
|
+
const overlayPath = join(outputDir, 'dsh-github-ci.cordis.yml')
|
|
224
|
+
const runEnv = {
|
|
225
|
+
DSH_HOME: dshHome,
|
|
226
|
+
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY,
|
|
227
|
+
DSH_GITHUB_CI_DRIVER: '1',
|
|
228
|
+
DSH_GITHUB_CI_OUTPUT_DIR: outputDir,
|
|
229
|
+
DSH_TELEMETRY_DISABLED: '1',
|
|
230
|
+
}
|
|
231
|
+
if (githubToken !== '') runEnv.DSH_GITHUB_TOKEN = githubToken
|
|
232
|
+
console.log('running dsh headless in the sandboxed home …')
|
|
233
|
+
const headless = run(`dsh --profile headless --patch ${q(overlayPath)} ${q(taskText)}`, { env: runEnv, allowFail: true, capture: true })
|
|
234
|
+
writeFileSync(join(outputDir, 'dsh-github-stdout.log'), headless.stdout)
|
|
235
|
+
writeFileSync(join(outputDir, 'dsh-github-stderr.log'), headless.stderr)
|
|
236
|
+
writeFileSync(join(outputDir, 'dsh-github-exit.txt'), `${headless.status}\n`)
|
|
237
|
+
console.log(headless.stdout)
|
|
238
|
+
if (headless.stderr.trim() !== '') console.log(headless.stderr)
|
|
239
|
+
|
|
240
|
+
// ---- Step 4: post (action.yml "Publish outputs and enforce the gate") ----
|
|
241
|
+
const post = run(`node ${q(join(repoRoot, 'scripts', 'action-post.mjs'))}`, {
|
|
242
|
+
env: {
|
|
243
|
+
RUNNER_TEMP: sandboxRoot,
|
|
244
|
+
GITHUB_WORKSPACE: workspace,
|
|
245
|
+
INPUT_OUTPUT_DIR: outputDir,
|
|
246
|
+
INPUT_BLOCKING: options.blocking,
|
|
247
|
+
GITHUB_OUTPUT: join(outputDir, 'github-output.env'),
|
|
248
|
+
},
|
|
249
|
+
allowFail: true,
|
|
250
|
+
})
|
|
251
|
+
console.log(`\nsandbox kept at: ${sandboxRoot}`)
|
|
252
|
+
console.log(` result: ${join(outputDir, 'dsh-github-ci-result.json')}`)
|
|
253
|
+
console.log(` summary: ${join(outputDir, 'dsh-github-ci-summary.md')}`)
|
|
254
|
+
console.log(` logs: ${join(outputDir, 'dsh-github-stdout.log')} / dsh-github-stderr.log`)
|
|
255
|
+
if (options.clean) {
|
|
256
|
+
rmSync(sandboxRoot, { recursive: true, force: true })
|
|
257
|
+
console.log('sandbox removed (--clean).')
|
|
258
|
+
}
|
|
259
|
+
process.exit(post.status)
|