projectops 4.28.1 → 4.28.3
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/cli/args.js +3 -2
- package/src/commands/doctor.js +22 -7
- package/src/core/assets.js +4 -0
- package/src/core/detect-fs.js +6 -1
package/README.md
CHANGED
package/package.json
CHANGED
package/src/cli/args.js
CHANGED
|
@@ -7,7 +7,8 @@ export const DEPLOY_TARGETS = ["docker-ssh", "vercel", "none"];
|
|
|
7
7
|
export const PUBLISH_TARGETS = ["nexus", "npm", "github-packages"];
|
|
8
8
|
export const INTENT_VALUES = ["app", "library", "both", "none", "manual"];
|
|
9
9
|
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
10
|
-
|
|
10
|
+
// 유니코드 글자·숫자 허용 (#719) — 공백·셸 메타문자는 문자 클래스에 없어 그대로 거부된다.
|
|
11
|
+
const BRANCH_RE = /^[\p{L}\p{N}][\p{L}\p{N}._/-]*$/u;
|
|
11
12
|
// 허용 실행 모드 (#665) — 오타가 조용히 "복사 0건 성공"으로 끝나지 않게 한다.
|
|
12
13
|
export const MODE_VALUES = ["full", "version", "workflows", "issues", "skills", "doctor", "interactive"];
|
|
13
14
|
|
|
@@ -103,7 +104,7 @@ export function parseArgs(argv) {
|
|
|
103
104
|
const v = (args.shift() ?? "").trim();
|
|
104
105
|
// 워크플로우 yaml·셸에 그대로 삽입되므로 안전한 브랜치명만 허용 (#676)
|
|
105
106
|
if (v && (!BRANCH_RE.test(v) || v.includes("..") || v.endsWith("/") || v.endsWith(".lock"))) {
|
|
106
|
-
throw new CliError(`--deploy-branch 값은
|
|
107
|
+
throw new CliError(`--deploy-branch 값은 글자·숫자·. _ - / 로 이루어진 브랜치명이어야 합니다: '${v}'`);
|
|
107
108
|
}
|
|
108
109
|
if (v) result.deployBranch = v;
|
|
109
110
|
break;
|
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: "건너뜀 (토큰 없음)",
|
package/src/core/assets.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 템플릿 자산 획득 + 제외 적용 (.sh download_template 등가) — template_integrator.sh 2064~2141.
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { exists, readText, copyDirSync, remove } from "./fsutil.js";
|
|
5
6
|
import { DOCS_TO_REMOVE, PLUGIN_ITEMS_TO_REMOVE } from "./exclusions.js";
|
|
@@ -21,6 +22,9 @@ export function acquireTemplate({ tempDir, source = { type: "git" } }) {
|
|
|
21
22
|
copyDirSync(source.path, tempDir);
|
|
22
23
|
} else {
|
|
23
24
|
const repo = source.repo || TEMPLATE_REPO;
|
|
25
|
+
// clone 은 stdio 를 버려 실패 원인이 사라진다 — 폴더가 읽기 전용이어도 "내려받지 못했다"로 오도되므로
|
|
26
|
+
// 먼저 만들어 권한 오류(EACCES/EPERM/EROFS)를 그대로 올린다 (#720). 빈 폴더에는 clone 이 가능하다.
|
|
27
|
+
mkdirSync(tempDir, { recursive: true });
|
|
24
28
|
execFileSync("git", ["clone", "--depth", "1", "--quiet", repo, tempDir], { stdio: "ignore" });
|
|
25
29
|
}
|
|
26
30
|
applyExclusions(tempDir);
|
package/src/core/detect-fs.js
CHANGED
|
@@ -4,7 +4,8 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
4
4
|
import { join, basename } from "node:path";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import { detectTypesFromMarkers, detectVersionFromFiles, suggestTypesByExtScan } from "./detect.js";
|
|
7
|
-
import { parseExisting } from "./version-yml.js";
|
|
7
|
+
import { parseExisting, convertLegacySingularType } from "./version-yml.js";
|
|
8
|
+
import { VALID_TYPES } from "../context.js";
|
|
8
9
|
|
|
9
10
|
const hasFile = (root) => (rel) => existsSync(join(root, rel));
|
|
10
11
|
const readFile = (root) => (rel) => {
|
|
@@ -50,6 +51,10 @@ export function detectTypes(root) {
|
|
|
50
51
|
if (existsSync(vy)) {
|
|
51
52
|
const { types } = parseExisting(readFileSync(vy, "utf8"));
|
|
52
53
|
if (types.length) return types; // basic 포함, 명시돼 있으면 그대로
|
|
54
|
+
// v4.1.0 이전 단수 project_type 키 (#718) — 마커가 없어도 명시된 값을 복원한다.
|
|
55
|
+
// 안 그러면 마커 없는 저장소가 basic 으로 떨어져 기존 타입의 워크플로우가 고아가 된다.
|
|
56
|
+
const legacy = (convertLegacySingularType(readFileSync(vy, "utf8")) || "").match(/^project_types: \["([a-z-]+)"\]/m);
|
|
57
|
+
if (legacy && VALID_TYPES.includes(legacy[1])) return [legacy[1]];
|
|
53
58
|
}
|
|
54
59
|
const byMarkers = detectTypesFromMarkers({ has: hasFile(root), read: readFile(root) });
|
|
55
60
|
if (byMarkers.length === 1 && byMarkers[0] === "basic") {
|