diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6413a74..093d4d1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,7 +19,7 @@ jobs: persist-credentials: false # 배포에 repo 자격증명이 필요 없다 — git config에 남기지 않는다 - uses: actions/setup-node@v4 with: - node-version: 24 # OIDC 요구: node 22.14+ / npm 11.5.1+ + node-version: 24 # OIDC 요구: node 22.14+ / npm 11.5.1+ — 올리면 test.yml 매트릭스에도 반영 registry-url: https://registry.npmjs.org # setup-node가 .npmrc에 남기는 _authToken placeholder가 OIDC 인증과 충돌할 수 있어 # Trusted Publishing을 지원하는 npm으로 고정한다(11.5.1+). id-token 권한이 있는 job이므로 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..194b8d2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ +name: test +# PR 시점에 테스트를 돌린다. npm test 가 릴리스 워크플로(publish.yml)에서만 돌면 +# 깨진 테스트가 배포 시점에야 발견된다 — PR 검증이 commitlint 뿐인 공백을 메운다. +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: [main] # 각각 green 인 PR 들이 병합되며 생기는 의미 충돌을 잡는다 + +permissions: + contents: read + +# 같은 브랜치에 연속 푸시하면 이전 실행을 취소한다 +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node: [20, 24] # engines 하한(>=20)과 publish.yml 이 배포에 쓰는 24 양쪽에서 검증한다 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false # 테스트는 repo 자격증명이 필요 없다 — git config 에 남기지 않는다 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: npm + - run: npm ci + # vitest(esbuild)·tsup 어느 쪽도 타입 검사를 하지 않아 여기서만 돈다 + - run: npm run typecheck + - run: npm test diff --git a/.gitignore b/.gitignore index 6dcda15..1bc4c01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ node_modules/ dist/ *.log +.DS_Store +# 로컬 Claude Code 세션 산출물 — 공개 저장소에 올리지 않는다 +.claude/ # 로컬 산출물(세션 의도 기록·작업 노트 등)이 공개 저장소에 새지 않도록 docs/는 기본 차단하고, # 공개할 문서가 생기면 아래에 예외를 명시해 허용한다 (rutter의 저장소 위생 규약 참조). diff --git a/tests/apply-cli.test.ts b/tests/apply-cli.test.ts index a05106a..77e34d4 100644 --- a/tests/apply-cli.test.ts +++ b/tests/apply-cli.test.ts @@ -7,13 +7,15 @@ import { parse } from 'yaml' const FIXTURE_PKG = new URL('./fixtures/rutter-v2', import.meta.url).pathname const CLI = join(process.cwd(), 'src/cli/index.ts') +// npx 경유가 아니라 로컬 tsx 바이너리를 직접 실행한다 (e2e.test.ts 와 같은 이유) +const TSX = join(process.cwd(), 'node_modules', '.bin', 'tsx') let env: NodeJS.ProcessEnv const run = (args: string[], cwd: string) => - execFileSync('npx', ['tsx', CLI, ...args], { encoding: 'utf8', env, cwd }) + execFileSync(TSX, [CLI, ...args], { encoding: 'utf8', env, cwd }) // stdout+stderr 함께 캡처 (경고는 stderr로 나간다) const runOut = (args: string[], cwd: string): string => { - const r = spawnSync('npx', ['tsx', CLI, ...args], { encoding: 'utf8', env, cwd }) + const r = spawnSync(TSX, [CLI, ...args], { encoding: 'utf8', env, cwd }) return (r.stdout ?? '') + (r.stderr ?? '') } const runFail = (args: string[], cwd: string): string => { diff --git a/tests/cli.test.ts b/tests/cli.test.ts index ac248e5..262932d 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -4,7 +4,8 @@ import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -const CLI = ['npx', 'tsx', 'src/cli/index.ts'] +// npx 경유가 아니라 로컬 tsx 바이너리를 직접 실행한다 (e2e.test.ts 와 같은 이유) +const CLI = [join(process.cwd(), 'node_modules', '.bin', 'tsx'), 'src/cli/index.ts'] let env: NodeJS.ProcessEnv function run(args: string[], cwd?: string): string { diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts index 3606567..2600ccb 100644 --- a/tests/e2e.test.ts +++ b/tests/e2e.test.ts @@ -5,8 +5,11 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' let env: NodeJS.ProcessEnv +// npx 경유가 아니라 로컬 tsx 바이너리를 직접 실행한다 — 테스트 cwd 가 임시 디렉토리라 +// npx 는 리포의 node_modules 를 보지 못하고 매번 네트워크에서 tsx 를 받아온다(CI 타임아웃 원인). +const TSX = join(process.cwd(), 'node_modules', '.bin', 'tsx') const run = (args: string[], cwd: string) => - execFileSync('npx', ['tsx', join(process.cwd(), 'src/cli/index.ts'), ...args], { encoding: 'utf8', env, cwd }) + execFileSync(TSX, [join(process.cwd(), 'src/cli/index.ts'), ...args], { encoding: 'utf8', env, cwd }) beforeEach(() => { env = { ...process.env, diff --git a/tests/fixtures/rutter-v2/docs/conventions/style.md b/tests/fixtures/rutter-v2/docs/conventions/style.md new file mode 100644 index 0000000..63e4909 --- /dev/null +++ b/tests/fixtures/rutter-v2/docs/conventions/style.md @@ -0,0 +1,7 @@ +# 코드 스타일 + +## Why + +일관된 스타일은 리뷰 비용을 줄인다. + +- 커밋 메시지는 한글로 작성한다. diff --git a/tests/fixtures/rutter-v2/docs/maps/projects.md b/tests/fixtures/rutter-v2/docs/maps/projects.md new file mode 100644 index 0000000..e1e7d60 --- /dev/null +++ b/tests/fixtures/rutter-v2/docs/maps/projects.md @@ -0,0 +1,5 @@ +# 조직 저장소 조망 + +| 저장소 | 설명 | +|--------|------| +| payment-api | 결제 API | diff --git a/tests/init.test.ts b/tests/init.test.ts index 16e1acd..3caeba7 100644 --- a/tests/init.test.ts +++ b/tests/init.test.ts @@ -5,8 +5,10 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' let env: NodeJS.ProcessEnv +// npx 경유가 아니라 로컬 tsx 바이너리를 직접 실행한다 (e2e.test.ts 와 같은 이유) +const TSX = join(process.cwd(), 'node_modules', '.bin', 'tsx') const run = (args: string[], cwd: string) => - execFileSync('npx', ['tsx', join(process.cwd(), 'src/cli/index.ts'), ...args], { encoding: 'utf8', env, cwd }) + execFileSync(TSX, [join(process.cwd(), 'src/cli/index.ts'), ...args], { encoding: 'utf8', env, cwd }) function makeRutter(): string { const r = mkdtempSync(join(tmpdir(), 'rutter-')) @@ -52,7 +54,7 @@ describe('pilot init', () => { // 않았다"는 보안 불변식(성공 로그 없음 + CLAUDE.md 없음 + config 비어있음)만 검증한다. let stdout = '' try { - stdout = execFileSync('npx', ['tsx', join(process.cwd(), 'src/cli/index.ts'), 'init'], + stdout = execFileSync(TSX, [join(process.cwd(), 'src/cli/index.ts'), 'init'], { encoding: 'utf8', env, cwd: proj, input: 'n\n' }) } catch { // 실패 자체가 기대되는 경로 — 아래 불변식만 확인한다