GitHub Pages 배포 계약
Pages는 정적 웹 호스팅이다. site는 Jaspr SSG + CoUI Web, app와 console은 Flutter Web으로 빌드하며 실제 API 서버는
Pages는 정적 웹 호스팅이다. site는 Jaspr SSG + CoUI Web, app와 console은 Flutter Web으로 빌드하며 실제 API 서버는
여기에 올라가지 않는다. preview는 명시적인 mock DI + 화면에 프리뷰 표기, live는 외부 API
URL·CORS·OAuth redirect·권한 동작이 검증되어야 한다. 공개 콘솔에 실제 자격증명을 심지 않는다.
설정#
templates/pages.example.json의 경로·엔트리·flutter_version·base_path를 실제 생성물에서
확인해 채운다. 일반 프로젝트 사이트는 /<repository>/, 조직 사용자 사이트/커스텀 도메인은
GitHub Pages API로 확인한 루트 경로다. 임의로 프로젝트명=저장소명이라고 가정하지 않는다.
tool/product_auto/
pages.json
build_pages.py # 이 스킬 scripts/build_pages.py와 동일
jaspr_ci.py # 이 스킬 scripts/jaspr_ci.py와 동일 (공유 호스트 포트 격리)
.github/workflows/product-pages.yml
빌드 명령은 네 surface 모두 정상 entrypoint를 사용한다. site는 web/<name>_site의
lib/main.server.dart와 jaspr: mode: static을 사용하며 jaspr build
출력 build/jaspr만
게시한다. 사이트는 독립 pub 워크스페이스라 루트 flutter pub get --enforce-lockfile이
해석하지 않는다 — 워크플로 템플릿의 Bootstrap 단계가 pages.json의 site 경로에서
dart pub get --enforce-lockfile을 따로 돌린다(사이트 pubspec.lock은 추적된다).
args에는 Flutter 프로젝트가 제공하는
--dart-define/--dart-define-from-file(공개 가능한 설정 파일만) 또는 추가 컴파일 옵션만 둔다.
target/base-href/release/output 옵션을 다시 정의하지 않는다. 앱 없는 dummy 프로젝트나
main_lite.dart로 대체하지 않는다.
build_pages.py는 다음을 강제한다:
- 네 surface 정확히 존재, 서로 다른 package path, 경로 탈출/심볼릭 링크 금지
-
site는 Jaspr SSG, 나머지는
--release --no-pub --base-href <base>/<surface>/와 임시--output으로 빌드 - 하나라도 실패/타임아웃/잘못된 base href/실행 번들 누락이면 publish 디렉토리 생성 없음
-
Jaspr
build/jaspr에는.dart_tool/package_config.json과packages/Dart 소스가 함께 생성될 수 있다. 이것들은 게시하지 않고 정적 웹 자산만 묶는다. HTML이packages/자산에 의존하면 누락된 채 게시하지 않고 차단한다. -
.git·원문 첨부가 아닌 검증한 정적 빌드 자산만 묶음. 결과에build.json으로 source SHA 기록 - 출력 디렉토리는 비어 있는 새 경로여야 함. 기존 사이트/작업 폴더를 삭제하지 않음
실행 전 flutter pub get --enforce-lockfile, 프로젝트별 codegen/analyze/tests를 수행한다.
prepare.sh는 에이전트가 실제 생성 프로젝트의 명령으로 작성하고 실행해 검증한다.
사용자에게 스크립트를 작성하라고 넘기지 않는다. 템플릿의 미존재 파일 실패는 설치 누락을
숨기지 않기 위한 장치다. build CLI는 소스 HEAD 일치와 clean 상태도 확인한다.
코드 생성 명령은 생성물의 Makefile/melos scripts를 읽어 확인하고 워크플로의 준비 단계에 반영한다.
소스에 생성 파일이 추적되는 경우 codegen 후 git diff --exit-code로 최신성도 검증한다.
외부 git 의존은 pubspec.lock의 resolved-ref로 고정한다. CoUI는 작업 시작과 배포 직전에
원격 최신 stable 릴리스를 검증한다. 오래된 핀이면 업그레이드 스킬에 따라 manifest 수정→
각 pub 루트의 실제 의존성 해석→API/시각 회귀 검증을 완료한다. Bootstrap/prepare와
아티팩트 업로드 직전에 엄격 검사를 두고 원격 인증·네트워크 실패도 실패로 처리한다.
인증은 검사 단계의 환경에만 전달하고 전역 git 설정이나 빌드 산출물에 저장하지 않는다.
최신 태그가 빌드 중 변경되어 업로드 검사가 실패하면 새 핀으로 검증·빌드를 다시 실행한다.
추가 surface — 골든 갤러리·문서 (extra_surfaces)#
네 surface 외에 같은 Pages 사이트에 싣고 싶은 정적 산출물(골든 갤러리, 설계 문서 등)은 pages.json 의
extra_surfaces 로 등록한다. Pages 사이트는 하나라서 워크플로를 따로 두면 서로 덮어쓴다.
"extra_surfaces": {
"golden": {"script": "tool/pages/surfaces/golden.sh"},
"docs": {"script": "tool/pages/surfaces/docs.sh"}
}
-
스크립트는 프로젝트 루트에서
bash <script> <OUT> <BASE_HREF>로 불리고, OUT(새 디렉토리)에index.html이 있는 정적 사이트를 만들어야 한다. 없으면 전체 빌드가 실패하고 아무것도 게시하지 않는다. - 랜딩(
index.html)과 루트build.json은 실제로 만든 surface 만 나열한다. - 이름은 소문자 URL 조각(
golden,docs)이며 네 기본 surface 이름과 겹칠 수 없다.
웹 빌드 기본값#
-
build_pages.py는 모든 Flutter surface 를--no-wasm-dry-run으로 빌드한다. 저장소의 다른 CI 잡(main CI 의 웹 빌드 검증 등)도 같은 플래그를 쓴다 — wasm dry run 이 호스티드 러너(2vCPU/8GB)를 메모리로 죽인다. -
GitHub Pages 에는 SPA 폴백이 없다. Pages(preview) 빌드는
args에--dart-define=WEB_URL_STRATEGY=hash를 넣어 해시 URL 로 빌드하고, 실서비스 빌드는 path URL 을 쓴다(flutter-app-baseline §3). - 앱·콘솔·위젯북은 한글 폰트를 번들한다(같은 문서 §8). Google Fonts 런타임 로드는 첫 화면에 네모 글자를 만든다.
-
배포 파이프라인(pages.json·워크플로·Pages
build_type=workflow)은 첫 PR 에 먼저 넣고 기능 보강은 후속 PR 로 — 기능 작업이 커밋 없이 오래 머물면 배포 경로가 늦게 열린다.
CI 실행 모드와 캐시#
워크플로는 조립 단계에서 설치해 베이스라인 green을 먼저 받는다(단계 계약 §3-6). 실측 한 건(수정 전 템플릿)에서 PR run 34분, main run 33분이 걸렸고 그중 준비(codegen+analyze+test)가 매번 24분이었다. 같은 커밋을 PR과 main에서 두 번 검증하지 않도록 역할을 나눈다.
| 이벤트 | prepare.sh 모드 | 빌드 | 배포 |
|---|---|---|---|
| pull_request | full = codegen + analyze + tests |
네 surface | 없음 |
| 기본 브랜치 push | codegen | 네 surface | upload 1회 + deploy 1회 |
| workflow_dispatch | 입력 full_prepare로 선택 |
네 surface | 기본 브랜치일 때만 |
-
기본 브랜치에서도 full을 돌려야 하면(보호 규칙 없이 직접 push하는 저장소) 저장소 변수
PRODUCT_PAGES_FULL_PREPARE=true를 둔다.codegen은full의 앞부분이어야 한다. -
캐시:
~/.pub-cache(키 = 모든pubspec.lock+.fvmrc, 부분 복원 허용)와.dart_tool/build_runner(같은 키 + 커밋, 이전 커밋 상태로 부분 복원). build_runner는 입력 digest를 다시 확인하므로 캐시는 가속일 뿐 오래된 산출물을 내지 않는다. -
melos
sdkPath가.fvm/flutter_sdk를 가리키면 러너에는 그 경로가 없다. 템플릿은.fvm/flutter_sdk가 없을 때MELOS_SDK_PATH=auto로 러너 SDK를 쓰게 한다. -
비공개 git 의존은
PRODUCT_GIT_DEPS_TOKEN시크릿을 스텝 범위GIT_CONFIG_*환경변수로만 주입한다.git config --global에 토큰을 쓰지 않는다. -
인증 env의
insteadOf는git remote get-url origin에도 적용된다. 그 결과를 공개 랜딩의 저장소 링크·빌드 JSON에 쓰면 토큰이 Pages에 실린다. 링크는PAGES_REPO_URL→GITHUB_SERVER_URL/GITHUB_REPOSITORY→git config --get remote.origin.url순으로 얻고, userinfo를 제거한다. 주소를 안전하게 정리할 수 없으면 링크를 만들지 않는다. -
빌드 스크립트 마지막과
actions/upload-pages-artifact직전의 인증 env 없는 별도 스텝에서 게시 디렉토리 전체(HTML·JS·JSON·바이너리 바이트)의 GitHub 토큰·URL userinfo를 스캔한다. 검출 시 업로드를 막고 로그에는 위치·종류·건수만 낸다. 공개 링크 생성 스크립트에서git remote get-url사용도 정적 게이트로 금지한다. github-actions-hygiene를 따른다. -
공유 macOS 러너에서는 Linux 전용
PRODUCT_PAGES_SWAP_GB설정을 사용할 수 없다. 변수가 설정되어 있으면 잡이 명확히 실패한다. 호스트 메모리를 확보하고 변수는 제거한다. -
타임아웃은
cc-dev:job-timeout-budget의 실측 ×2 규칙으로 잡았다(job 70분, 준비 50분, 네 빌드 15분, deploy 15분). 새 제품에서 첫 cold run을 측정하면 그 값으로 다시 계산한다.
골든 정책#
골든은 렌더링 플랫폼에 묶인다. macOS에서 만든 기준 이미지를 Linux 러너에서 비교하지 않는다 (실측 한 건에서 골든 70개가 이 이유로 CI에서만 실패했다). 둘 중 하나를 고른다.
-
기본: 골든 테스트에
golden태그를 달고(dart_test.yaml의tags: golden:), Pages 워크플로의prepare.sh full은--exclude-tags golden으로 제외한다. 골든은 기준을 만든 플랫폼에서 구현 단계의tests증거로 검증한다. - 골든을 CI에서도 검증하려면 기준 이미지를 Linux(러너와 같은 이미지)에서 생성·갱신하고 그때만 제외 인자를 뺀다. 한 저장소에 두 플랫폼 기준을 섞지 않는다.
배포#
-
gh repo view <owner/repo>/gh api repos/<owner/repo>/pages로 권한·build_type 확인. Pages가 없으면 요청한 저장소에build_type: workflow를 설정한다(권한/요금제 제한이면 BLOCKED). - 기존 Pages workflow가 있으면 하나로 통합. site/app/console/widgetbook별 독립 deploy-pages는 같은 사이트를 덮어쓰므로 사용하지 않는다. branch source 설정을 바꿀 때 기존 사이트를 확인한다.
- 제공 템플릿을 기존 CI 규약에 맞춰 설치.
<SDK_VERSION>같은 미치환 토큰이 남으면 제출 금지. - 변경/로컬 검증 후 사용자 배포 요청 범위에서 소스 커밋·PR·CI·머지. 브랜치 보호나 실패 검사를 우회하지 않는다. 조직에 따라 배포 환경 승인이 필요하면 승인 대기 상태로 기록한다.
-
기본 브랜치 push가 시작한 정확한 run을
gh run list --commit <sha>로 찾고 그 ID만 감시.workflow_dispatch는 push run이 없는 경우에만 사용. 자동 merge 미지원은 완료가 아니다.
서빙 검증 (deploy action 초록만으로 완료 금지)#
-
gh run view <id> --json headSha,status,conclusion,url: 기대 SHA + completed/success. -
deploy-pages의 page_url 및 각/site/build.json,/app/build.json,/console/build.json,/widgetbook/build.json: 모두 같은 SHA, surface, runtime_mode 확인. CDN 전파 대기는 5초 간격 최대 12회(1분), 이후 BLOCKED로 남기고 동일 run의 결과를 재확인한다. 새 deploy 반복 금지. -
사이트는 HTML 링크·메타데이터·자산 로딩·모바일 폭을 브라우저로 확인하고,
cc-flutter:flutter-web-mcp-verification으로 배포 Flutter release 웹을 연다. Flutter semantics 활성화, 각 surface 대표 UI 렌더와 핵심 동선, console 접근권한 또는 preview 배너, widgetbook 전체 화면 등재 + 상태 Knob 확인. 콘솔/network 오류 수집. -
새 앱의 기준은 path URL(
usePathUrlStrategy, flutter-app-baseline §3)이다 — 해시 라우팅은 기존 앱에만 남아 있다. 골든 갤러리(co_golden_gallery)를 싣는다면 같은 워크플로의 다섯 번째 경로goldens/로 합친다(golden-test-guide). -
해시 라우팅은
/#/...직접 접근+새로고침으로 확인. path 라우팅이면 Pages fallback 계약을 프로젝트에 명시하고 네 surface prefix를 보존하는 실제 직접 접근을 검사한다.404.html복사만 하고 올바른 HTTP 상태/화면이 나온다고 가정하지 않는다.
런타임 스모크#
scripts/runtime_smoke.py는 화면 커버리지 표의 라우트마다 기대 문구가 보이는지 헤드리스 Chromium으로
확인한다. Flutter 웹은 캔버스에 그리므로 semantics를 켠 뒤 semantics DOM에서 문구를 찾고,
라우트마다 스크린샷을 남기며, Flutter surface는 새로고침 후 같은 화면인지도 본다. 스펙 형식은
runtime-smoke.example.json이다(hash_routing,
viewports, 허용할 콘솔 오류 정규식, 네 surface를 모두 덮는 checks).
python3 -m pip install playwright && python3 -m playwright install chromium # 한 번
# 구현 단계: 로컬 build_pages 출력을 같은 base path로 서빙해 검사
python3 <skill>/scripts/runtime_smoke.py --spec <run-dir>/runtime-smoke.json \
--serve <build_pages-out> --base-path /<repo>/ \
--out <run-dir>/evidence/smoke-local-1.json --screenshots <run-dir>/evidence/smoke-local-1
# 배포 단계: 실제 Pages URL에 같은 스펙
python3 <skill>/scripts/runtime_smoke.py --spec <run-dir>/runtime-smoke.json \
--base-url https://<owner>.github.io/<repo>/ \
--out <run-dir>/evidence/smoke-pages-1.json --screenshots <run-dir>/evidence/smoke-pages-1
종료 코드 0은 모든 라우트 통과, 1은 실패 라우트 있음, 2는 스펙/환경 오류다. 스크립트는 수동 클릭을 대신할 뿐 시각 판정을 대신하지 않는다 — 스크린샷을 검토하고 핵심 동선의 입력·전송은 브라우저 검증으로 보탠다.
실패하면 publish receipt를 passed로 만들지 않는다. 브라우저 검증 자료가 없으면
runtime_smoke는 미완료이며 배포 URL만 얻었다고 전체 완료로 표현하지 않는다.