projectops 4.28.2 → 4.28.4
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 +1 -1
- package/package.json +1 -1
- package/src/commands/doctor.js +22 -7
- package/src/core/copy/workflows.js +6 -1
package/README.md
CHANGED
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -162,7 +162,16 @@ export function localChecks(cwd = ".") {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
// ── 원격 점검 ─────────────────────────────────────────────────────────
|
|
165
|
-
|
|
165
|
+
// permissions 블록을 한 번도 선언하지 않은 워크플로우 — 이때만 저장소 기본 권한(Settings)이 그대로 적용된다.
|
|
166
|
+
// 선언이 있으면 저장소 기본값이 read 여도 선언한 범위로 동작한다 (조직 read 기본값에서 릴리스 automerge 실측, #723).
|
|
167
|
+
export function workflowsWithoutPermissions(cwd = ".") {
|
|
168
|
+
const wfDir = join(cwd, PATHS.workflowsDir);
|
|
169
|
+
if (!existsSync(wfDir)) return [];
|
|
170
|
+
return readdirSync(wfDir).filter((f) => /\.ya?ml$/.test(f))
|
|
171
|
+
.filter((f) => !/^\s*permissions:/m.test(readFileSync(join(wfDir, f), "utf8")));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function remoteChecks(slug, token, requiredSecrets = [], undeclared = []) {
|
|
166
175
|
const rows = [];
|
|
167
176
|
const add = (r) => rows.push(r);
|
|
168
177
|
|
|
@@ -175,15 +184,20 @@ export async function remoteChecks(slug, token, requiredSecrets = []) {
|
|
|
175
184
|
} else if (perm.data?.default_workflow_permissions === "write") {
|
|
176
185
|
add({ name: "Workflow permissions", purpose: PERM_PURPOSE, status: "OK",
|
|
177
186
|
value: "Read and write permissions" });
|
|
187
|
+
} else if (undeclared.length === 0) {
|
|
188
|
+
// 모든 워크플로우가 permissions 를 스스로 선언 → 저장소 기본값(read)은 실제 동작에 영향이 없다
|
|
189
|
+
add({ name: "Workflow permissions", purpose: PERM_PURPOSE, status: "INFO",
|
|
190
|
+
value: "읽기 전용 (워크플로우가 권한을 직접 선언해 영향 없음)",
|
|
191
|
+
detail: ["설치된 워크플로우가 permissions 를 모두 선언하고 있어 기본값이 읽기 전용이어도 정상 동작합니다."] });
|
|
178
192
|
} else {
|
|
179
193
|
add({
|
|
180
194
|
name: "Workflow permissions", purpose: PERM_PURPOSE, status: "WARN",
|
|
181
195
|
value: "Read repository contents permission (읽기 전용)",
|
|
182
196
|
detail: [
|
|
183
|
-
"
|
|
184
|
-
|
|
185
|
-
"조치:
|
|
186
|
-
" → 'Read and write permissions' 선택",
|
|
197
|
+
"permissions 선언이 없는 워크플로우는 저장소에 쓰거나 다른 워크플로우를 실행할 수 없습니다.",
|
|
198
|
+
...undeclared.map((f) => ` ${f}`),
|
|
199
|
+
"조치: 해당 파일에 permissions 블록을 추가하거나",
|
|
200
|
+
" Settings > Actions > General > Workflow permissions → 'Read and write permissions' 선택",
|
|
187
201
|
],
|
|
188
202
|
});
|
|
189
203
|
}
|
|
@@ -225,7 +239,8 @@ export async function remoteChecks(slug, token, requiredSecrets = []) {
|
|
|
225
239
|
"AI를 쓰려면 GEMINI_API_KEY(무료)를 등록하세요 — https://aistudio.google.com/apikey",
|
|
226
240
|
],
|
|
227
241
|
});
|
|
228
|
-
|
|
242
|
+
// AI 키는 위에서 따로 안내하는 선택 항목이다 — '배포에 필요한 값' 미등록 목록에 섞으면 필수처럼 읽힌다 (#723)
|
|
243
|
+
const missing = requiredSecrets.filter((n) => !have.has(n) && !(n in AI_KEYS));
|
|
229
244
|
add({
|
|
230
245
|
name: "Secret 등록 여부", purpose: "배포에 필요한 값",
|
|
231
246
|
status: missing.length ? "WARN" : "OK",
|
|
@@ -280,7 +295,7 @@ export async function runDoctor({ cwd = ".", token = process.env.GITHUB_TOKEN ||
|
|
|
280
295
|
|
|
281
296
|
if (slug && token) {
|
|
282
297
|
const v = verifyInstall(cwd);
|
|
283
|
-
rows.push(...await remoteChecks(slug, token, [...v.secrets.keys()]));
|
|
298
|
+
rows.push(...await remoteChecks(slug, token, [...v.secrets.keys()], workflowsWithoutPermissions(cwd)));
|
|
284
299
|
} else if (slug) {
|
|
285
300
|
rows.push({ name: "저장소 설정 점검", purpose: "권한·머지 방식·Secret", status: "INFO",
|
|
286
301
|
value: "건너뜀 (토큰 없음)",
|
|
@@ -6,7 +6,7 @@ import { join, basename } from "node:path";
|
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
7
7
|
import { PATHS } from "../paths.js";
|
|
8
8
|
import { exists, copyFileSync, listYamlFiles } from "../fsutil.js";
|
|
9
|
-
import { isUnchanged, substituteEnv } from "../wizard-env.js";
|
|
9
|
+
import { isUnchanged, substituteEnv, resolveGlobalTokens } from "../wizard-env.js";
|
|
10
10
|
import { isUserModified, readBaseline, writeBaseline, sha256 } from "../baseline.js";
|
|
11
11
|
import { substituteBranches } from "../branch-sub.js";
|
|
12
12
|
import { saveIncoming, lineDiffCounts } from "../incoming.js";
|
|
@@ -221,6 +221,11 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
221
221
|
const dst = join(workflowsDir, filename);
|
|
222
222
|
if (existsSync(dst)) continue; // 이미 존재하면 스킵
|
|
223
223
|
copyFileSync(join(secretDir, filename), dst);
|
|
224
|
+
// PROJECT_NAME 이 고정값이면 모든 프로젝트가 서버의 같은 폴더에 Secret 을 덮어쓴다 (#729) —
|
|
225
|
+
// 다른 배포 워크플로처럼 레포 이름으로 채운다. 이름을 알 수 없으면 기존 기본값을 쓴다.
|
|
226
|
+
const raw = readFileSync(dst, "utf8");
|
|
227
|
+
const filled = resolveGlobalTokens(raw, repoName || "my-project");
|
|
228
|
+
if (filled !== raw) writeFileSync(dst, filled);
|
|
224
229
|
counters.optionalCopied++;
|
|
225
230
|
counters.copied++;
|
|
226
231
|
counters.copiedFiles.push(filename);
|