project-auto-wizard 0.3.3 → 0.4.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 CHANGED
@@ -17,7 +17,7 @@ npx project-auto-wizard
17
17
  [![node](https://img.shields.io/badge/node-%3E%3D20.12-brightgreen)](package.json)
18
18
 
19
19
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
20
- ## 최신 버전 : v0.3.1 (2026-08-18)
20
+ ## 최신 버전 : v0.3.3 (2026-08-18)
21
21
 
22
22
  [전체 버전 기록 보기](CHANGELOG.md)
23
23
 
@@ -130,7 +130,7 @@ flowchart LR
130
130
 
131
131
  - **pr-flow** (기본): `VERSION-CONTROL`(main 직접 push 안전망) + `AUTO-CHANGELOG-CONTROL`(릴리스 PR) + `RELEASE-PUBLISH`(tag+Release) 3종 설치
132
132
  - **trunk-based** (릴리스 브랜치 = 개발 브랜치): `RELEASE-PUBLISH` 하나가 main push마다 버전확정 → 체인지로그 → tag → Release를 순차 처리
133
- - 마법사가 브랜치를 묻고(`--main-branch`/`--develop-branch`) 없으면 **생성 + push**까지. 선택은 `version.yml`에 저장되어 업데이트 시 재질문 없음
133
+ - 마법사가 브랜치 전략(pr-flow/trunk-based)을 먼저 묻고, trunk-based를 고르면 개발 브랜치 질문 없이 릴리스 브랜치 하나만 사용합니다. 비대화형은 `--main-branch`/`--develop-branch` 플래그로 지정하며, 없으면 **생성 + push**까지. 선택은 `version.yml`에 저장되어 업데이트 시 재질문 없음
134
134
 
135
135
  ## 설치 옵션
136
136
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-auto-wizard",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "One command DevOps: npx wizard that installs GitHub-native AI Release Automation into any project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,6 +45,11 @@ SSH_AUTH_METHOD:
45
45
  help: "배포 서버 접속 방식입니다. 둘 중 하나를 그대로 입력하세요."
46
46
  example: "password 또는 key"
47
47
 
48
+ ISSUE_HELPER_CREATE_BRANCH:
49
+ label: "이슈 생성 시 브랜치 자동 생성"
50
+ help: "이슈가 열리면 base 브랜치 기준으로 브랜치를 자동으로 만듭니다. 원치 않으면 기본값(아니오)을 그대로 두세요."
51
+ example: "true"
52
+
48
53
  # workflow filename (partial match) -> human readable short name
49
54
  # wizard uses this to build "[usage]" in env questions.
50
55
  _workflow_names:
@@ -60,3 +65,4 @@ _workflow_names:
60
65
  FLUTTER-ANDROID-SELFHOSTED: "안드로이드 자체배포"
61
66
  FLUTTER-ANDROID-PLAYSTORE: "플레이스토어 배포"
62
67
  FLUTTER-ANDROID-FIREBASE: "Firebase 배포"
68
+ ISSUE-HELPER: "이슈 헬퍼"
@@ -36,6 +36,11 @@
36
36
  # {{MAIN_BRANCH}}-push workflows (e.g. RELEASE-PUBLISH) for that merge.
37
37
  # - secrets.AI_API_KEY (+ vars.AI_API_BASE_URL / vars.AI_MODEL): preferred
38
38
  # summary engine. Falls back to GitHub Models, then to rule-based.
39
+ #
40
+ # Issue #90: without WORKFLOW_PAT, a wait-for-merge-and-trigger-release job
41
+ # polls until the automerge actually lands, then wakes RELEASE-PUBLISH via
42
+ # workflow_dispatch — GITHUB_TOKEN can always trigger that event, unlike
43
+ # the push/release events blocked by GitHub's anti-recursion policy.
39
44
  # ===================================================================
40
45
 
41
46
  name: PROJECT-AUTO-CHANGELOG-CONTROL
@@ -268,3 +273,51 @@ jobs:
268
273
  fi
269
274
  exit 1
270
275
  fi
276
+
277
+ wait-for-merge-and-trigger-release:
278
+ name: Wait for merge and trigger RELEASE-PUBLISH (WORKFLOW_PAT 없이도 동작)
279
+ needs: changelog-and-merge
280
+ runs-on: ubuntu-latest
281
+ timeout-minutes: 35
282
+ permissions:
283
+ pull-requests: read
284
+ actions: write
285
+ steps:
286
+ - name: Poll for merge completion
287
+ id: wait
288
+ env:
289
+ GH_TOKEN: ${{ github.token }}
290
+ GH_REPO: ${{ github.repository }}
291
+ run: |
292
+ PR_NUMBER=${{ github.event.pull_request.number }}
293
+ MAX_WAIT=1800
294
+ INTERVAL=20
295
+ ELAPSED=0
296
+ MERGED=false
297
+
298
+ while [ $ELAPSED -lt $MAX_WAIT ]; do
299
+ PR_STATE=$(gh pr view "$PR_NUMBER" --json state -q .state 2>/dev/null || echo "")
300
+ if [ "$PR_STATE" = "MERGED" ]; then
301
+ MERGED=true
302
+ echo "PR #$PR_NUMBER 병합 확인 (경과 ${ELAPSED}s)"
303
+ break
304
+ fi
305
+ sleep $INTERVAL
306
+ ELAPSED=$((ELAPSED + INTERVAL))
307
+ done
308
+
309
+ if [ "$MERGED" != "true" ]; then
310
+ echo "::warning::PR #$PR_NUMBER 병합을 ${MAX_WAIT}s 내에 확인하지 못했습니다 — RELEASE-PUBLISH 자동 트리거를 건너뜁니다. WORKFLOW_PAT이 등록되어 있다면 기존 경로로 정상 진행되며, 없다면 필요 시 RELEASE-PUBLISH를 수동으로 workflow_dispatch 하세요."
311
+ fi
312
+
313
+ echo "merged=$MERGED" >> "$GITHUB_OUTPUT"
314
+
315
+ - name: Trigger RELEASE-PUBLISH
316
+ if: steps.wait.outputs.merged == 'true'
317
+ continue-on-error: true
318
+ env:
319
+ GH_TOKEN: ${{ github.token }}
320
+ GH_REPO: ${{ github.repository }}
321
+ run: |
322
+ gh workflow run PROJECT-COMMON-RELEASE-PUBLISH.yaml --ref {{MAIN_BRANCH}}
323
+ echo "RELEASE-PUBLISH workflow_dispatch 트리거 요청 완료 (WORKFLOW_PAT 없이도 동작)"
@@ -36,7 +36,7 @@ jobs:
36
36
  ISSUE_HELPER_BRANCH_PREFIX: ""
37
37
  ISSUE_HELPER_MAX_BRANCH_LENGTH: "100"
38
38
  ISSUE_HELPER_COMMIT_TEMPLATE: "${issueTitle} : feat : {변경 사항에 대한 설명} ${issueUrl}"
39
- ISSUE_HELPER_CREATE_BRANCH: "false"
39
+ ISSUE_HELPER_CREATE_BRANCH: "false" # @wizard ask:false
40
40
  ISSUE_HELPER_BASE_BRANCH: "{{MAIN_BRANCH}}"
41
41
  steps:
42
42
  - name: Checkout repository
@@ -37,6 +37,13 @@
37
37
  # workflow_dispatch republishes the current version (recovery path when a
38
38
  # previous run failed after the version was confirmed but before the
39
39
  # release was created). The idempotency guard keeps it safe to re-run.
40
+ #
41
+ # Issue #90: this file and its self-copy (.github/workflows/) intentionally
42
+ # differ by one step — the self-copy also triggers NPM-PUBLISH via
43
+ # workflow_dispatch right after the release is created. NPM-PUBLISH.yaml is
44
+ # a repo-only workflow (not shipped in payload), so wiring that dispatch
45
+ # here would make every wizard-installed repo call a workflow that doesn't
46
+ # exist for them on every release.
40
47
  # ===================================================================
41
48
 
42
49
  name: PROJECT-RELEASE-PUBLISH
@@ -19,6 +19,11 @@
19
19
  #
20
20
  # Release-confirm pattern: "chore(release):" — keep in sync with
21
21
  # PROJECT-COMMON-AUTO-CHANGELOG-CONTROL and PROJECT-COMMON-RELEASE-PUBLISH.
22
+ #
23
+ # Issue #90 / #61: after pushing the safety-net bump, this workflow also
24
+ # triggers RELEASE-PUBLISH via workflow_dispatch so the drift it used to
25
+ # only detect (issue #61) is now auto-recovered instead of requiring a
26
+ # manual re-run.
22
27
  # ===================================================================
23
28
 
24
29
  name: PROJECT-VERSION-CONTROL
@@ -39,6 +44,7 @@ on:
39
44
 
40
45
  permissions:
41
46
  contents: write
47
+ actions: write
42
48
 
43
49
  jobs:
44
50
  version-bump:
@@ -129,6 +135,7 @@ jobs:
129
135
  fi
130
136
 
131
137
  - name: Commit and push changes
138
+ id: commit_push
132
139
  if: steps.release_guard.outputs.skip != 'true'
133
140
  run: |
134
141
  git config --local user.email "action@github.com"
@@ -138,6 +145,7 @@ jobs:
138
145
 
139
146
  if git diff --staged --quiet; then
140
147
  echo "no changes to commit (already in sync)"
148
+ echo "pushed=false" >> "$GITHUB_OUTPUT"
141
149
  else
142
150
  NEW_VERSION="${{ steps.version.outputs.new_version }}"
143
151
  COMMIT_MSG="chore(version): bump to v${NEW_VERSION} [skip ci]"
@@ -172,12 +180,22 @@ jobs:
172
180
  echo "push failed after $MAX_RETRIES attempts"
173
181
  exit 1
174
182
  fi
183
+ echo "pushed=true" >> "$GITHUB_OUTPUT"
175
184
  else
176
185
  echo "commit failed"
177
186
  exit 1
178
187
  fi
179
188
  fi
180
189
 
190
+ - name: Trigger RELEASE-PUBLISH (safety-net bump 발행)
191
+ if: steps.release_guard.outputs.skip != 'true' && steps.commit_push.outputs.pushed == 'true'
192
+ continue-on-error: true
193
+ env:
194
+ GH_TOKEN: ${{ github.token }}
195
+ run: |
196
+ gh workflow run PROJECT-COMMON-RELEASE-PUBLISH.yaml --ref {{MAIN_BRANCH}}
197
+ echo "RELEASE-PUBLISH workflow_dispatch 트리거 요청 완료 (WORKFLOW_PAT 없이도 동작, 이슈 #61 드리프트 자동 복구)"
198
+
181
199
  - name: Summary
182
200
  if: always()
183
201
  run: |
@@ -192,11 +192,19 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
192
192
  : resolveBranchConfig({ defaultBranch: branch });
193
193
  if (showOptional && !existing?.branches) {
194
194
  const remoteBranches = await detectRemoteBranches(cwd);
195
+ // 이슈 #93 — 두 질문에 같은 이름을 입력해야만 trunk-based가 되는 암묵적 규칙 대신,
196
+ // 전략을 먼저 명시적으로 고르게 한다. 취소/그 외 값은 기존 기본 동작과 같은 pr-flow로 폴백
197
+ // (selectDeployStyle의 "ESC = 기본값" 패턴과 동일).
198
+ const strategyPick = await io.selectBranchStrategy();
199
+ const strategy = strategyPick === "trunk-based" ? "trunk-based" : "pr-flow";
195
200
  const mainB = await pickBranch(io, `릴리스 브랜치를 선택하세요 (기본: ${branch})`, branch, remoteBranches, isCancel);
196
- const devB = await pickBranch(io, "개발 브랜치를 선택하세요 (기본: develop)", "develop", remoteBranches, isCancel);
201
+ // trunk-based면 개발 브랜치 질문 자체를 생략 유일한 브랜치(main) 그대로 develop으로 쓴다.
202
+ const devB = strategy === "trunk-based"
203
+ ? mainB
204
+ : await pickBranch(io, "개발 브랜치를 선택하세요 (기본: develop)", "develop", remoteBranches, isCancel);
197
205
  branches = resolveBranchConfig({ mainBranch: mainB, developBranch: devB, defaultBranch: branch });
198
206
  if (branches.mode === "trunk-based") {
199
- io.note?.("릴리스 브랜치 = 개발 브랜치 → trunk-based 모드로 설치합니다 (RELEASE-PUBLISH 단독).", "브랜치 모드");
207
+ io.note?.(`릴리스 브랜치(${branches.main}) 하나만 사용하는 trunk-based 모드로 설치합니다 (RELEASE-PUBLISH 단독).`, "브랜치 모드");
200
208
  } else if (remoteBranches.length && !remoteBranches.includes(branches.develop)) {
201
209
  await ensureDevelopBranch({
202
210
  develop: branches.develop, remoteBranches, cwd,
@@ -159,7 +159,18 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
159
159
  const commonDir = join(projectTypesDir, "common");
160
160
  if (exists(commonDir)) {
161
161
  const notExcluded = (filename) => !(branchMode === "trunk-based" && TRUNK_BASED_EXCLUDED.has(filename));
162
- processDir(commonDir, workflowsDir, envOptsFor("common"), dirCtx, counters, notExcluded);
162
+ const c = processDir(commonDir, workflowsDir, envOptsFor("common"), dirCtx, counters, notExcluded);
163
+ // env 치환 — 타입별 폴더(copyWorkflowsForType)와 동일하게, 손대지 않기로 한 파일(unchanged/localOnly)은
164
+ // 건너뛴다. common 최상위는 지금까지 @wizard 마커가 없어 이 루프가 없어도 드러나지 않았지만,
165
+ // ISSUE_HELPER_CREATE_BRANCH(issue #94)부터는 실제로 값이 반영돼야 한다.
166
+ const untouched = [...c.unchanged, ...c.localOnly];
167
+ for (const filename of listYamlFiles(commonDir)) {
168
+ if (!notExcluded(filename)) continue;
169
+ const target = join(workflowsDir, filename);
170
+ if (!existsSync(target)) continue;
171
+ if (untouched.includes(filename)) continue;
172
+ configureEnv(target, envOptsFor("common"));
173
+ }
163
174
  }
164
175
 
165
176
  // (2~4) 타입별
@@ -48,10 +48,13 @@ export function collectAsks(payloadRoot, types = [], opts = {}) {
48
48
  const typeDefaults = new Map();
49
49
  const usages = new Map();
50
50
 
51
- // 스캔 단위: [타입, 폴더]. secret-backup은 타입이 아니라 공통이지만 @wizard 마커를 가지므로
52
- // 포함하기로 경우에만 질문 수집 대상이 된다 (이슈 #82) 종전에는 스캔 대상이 아니어서
53
- // my-project 같은 예시값이 질문 없이 그대로 설치됐다.
51
+ // 스캔 단위: [타입, 폴더]. common/ 최상위는 타입 선택과 무관하게 항상 설치되므로(복사 엔진과
52
+ // 동일 규칙 issue #94) 무조건 스캔한다. secret-backup은 common 최상위가 아니라 하위
53
+ // 폴더고, 파일 전체가 조건부로 설치되므로 포함하기로 한 경우에만 별도로 스캔한다 (이슈 #82) —
54
+ // 종전에는 스캔 대상이 아니어서 my-project 같은 예시값이 질문 없이 그대로 설치됐다.
54
55
  const units = [];
56
+ const commonDir = join(baseDir, "common");
57
+ if (exists(commonDir)) units.push(["common", commonDir, null]);
55
58
  for (const type of types) {
56
59
  const typeDir = join(baseDir, type);
57
60
  if (!exists(typeDir)) continue;
@@ -126,6 +129,12 @@ export function printFieldCard(prompts, key, info, idx = null, tot = null, log =
126
129
  log("");
127
130
  }
128
131
 
132
+ // ask 필드의 기본값이 정확히 "true"/"false"면 boolean 필드로 간주한다 (이슈 #94).
133
+ // 마커 문법(@wizard ask:...)을 바꾸지 않고 리터럴 값 형태만으로 판단 — 별도 타입 표기가 필요 없다.
134
+ function isBooleanDefault(value) {
135
+ return value === "true" || value === "false";
136
+ }
137
+
129
138
  // 지정 KEY들을 하나씩 입력받아 values에 기록 (.sh _wf_prefill_interactive 등가).
130
139
  // 빈 입력(Enter)/ESC → KEY 공통 기본값 유지 (.sh safe_read || _in="" 등가).
131
140
  async function promptEach(io, prompts, asks, todoKeys, values, log) {
@@ -139,10 +148,16 @@ async function promptEach(io, prompts, asks, todoKeys, values, log) {
139
148
  i++;
140
149
  const def = asks.defaults.get(key) ?? "";
141
150
  printFieldCard(prompts, key, { default: def, usages: asks.usages.get(key) || [] }, i, tot, log);
142
- let input = await io.text({ message: `↳ 값 입력 (Enter=기본값 «${def}» 유지):`, defaultValue: def });
143
- if (input === CANCEL || input == null || input === "") input = def;
144
- values.set(key, input);
145
151
  const label = wfField(prompts, firstTypeFor(asks.usages, key), key, "label");
152
+ let input;
153
+ if (isBooleanDefault(def)) {
154
+ const answer = await io.confirm({ message: `↳ ${label} — 활성화할까요?`, initialValue: def === "true" });
155
+ input = answer === CANCEL ? def : (answer ? "true" : "false");
156
+ } else {
157
+ input = await io.text({ message: `↳ 값 입력 (Enter=기본값 «${def}» 유지):`, defaultValue: def });
158
+ if (input === CANCEL || input == null || input === "") input = def;
159
+ }
160
+ values.set(key, input);
146
161
  log(` → ${label} = ${input}`);
147
162
  log("");
148
163
  }
@@ -182,9 +197,9 @@ export async function promptEnvPlan({
182
197
 
183
198
  // 기본값 미리보기 카드 전체 출력 (.sh 3237~3251)
184
199
  log("");
185
- log("▶ 배포 워크플로우 환경설정을 채웁니다");
200
+ log("▶ 워크플로우 환경설정을 채웁니다");
186
201
  log("");
187
- log(" 설치되는 배포 워크플로우가 사용할 값입니다. 항목마다 '무엇에 쓰이는지·설명·예시'와");
202
+ log(" 설치되는 워크플로우가 사용할 값입니다. 항목마다 '무엇에 쓰이는지·설명·예시'와");
188
203
  log(" 기본값을 함께 보여드립니다. 그대로 둬도 되고, 원하는 것만 바꿀 수 있습니다.");
189
204
  log("");
190
205
  const tot = asks.keys.length;
package/src/ui/prompts.js CHANGED
@@ -97,6 +97,25 @@ export async function selectDeployStyle() {
97
97
  });
98
98
  }
99
99
 
100
+ // 브랜치 전략 선택 (이슈 #93). 종전에는 "릴리스 브랜치"/"개발 브랜치" 두 질문에
101
+ // 같은 이름을 입력해야만 trunk-based가 됐는데, 그 규칙이 사전에 안내되지 않아
102
+ // 의도치 않게 pr-flow로 흘러갔다. 전략을 먼저 명시적으로 고르게 해 이를 없앤다.
103
+ // 옵션 순서(pr-flow 먼저)는 비-TTY 환경의 기본값과 직결되므로 바꾸지 않는다.
104
+ export async function selectBranchStrategy() {
105
+ engine.note(
106
+ "pr-flow는 develop에서 작업해 main으로 PR을 올리는 팀 협업 흐름입니다.\n" +
107
+ "trunk-based는 브랜치를 하나만 두고 그 브랜치에서 바로 작업하는 단순한 흐름입니다.",
108
+ "브랜치 전략",
109
+ );
110
+ return engine.select({
111
+ message: "브랜치 전략을 선택하세요",
112
+ options: [
113
+ { value: "pr-flow", label: "develop → main PR 흐름 (pr-flow) — 팀 협업/리뷰 프로세스가 필요할 때" },
114
+ { value: "trunk-based", label: "main 단일 브랜치 (trunk-based) — 개인/소규모 프로젝트로 브랜치 없이 단순하게 쓸 때" },
115
+ ],
116
+ });
117
+ }
118
+
100
119
  // 텍스트 입력 (빈 입력=기본값 유지).
101
120
  export async function askText(message, defaultValue = "") {
102
121
  const v = await engine.text({ message, defaultValue });