Releases

  • LINKS 1.0.8 — PostgreSQL 최초 기동 healthcheck race로 backup 복구 검증이 실패하던 결함 수정

    LINKS 1.0.8

    이 hotfix 릴리스는 이전 릴리스 1.0.7(3f6c504352) 위에 PostgreSQL 최초 기동 healthcheck 수정만 얹었습니다. 최종 소스는 9f4ada7c6c, 루트 패키지는 1.0.8, ARCA pin은 exact 0.1.18로 그대로입니다. 1.0.7에서 첫 sh backup.sh가 [6/8] Restoring into a fresh database and empty volumes에서 FATAL: database "links" does not exist로 실패할 수 있던 race를 고쳤습니다. schema는 바뀌지 않았습니다. DB 초기화와 migration이 필요 없습니다. backup 형식도 그대로입니다.

    업그레이드 주의: 1.0.7 → 1.0.8은 sh update.sh만 실행하고, 설치 뒤 sh backup.sh로 backup을 한 번 만들어 둡니다

    1. 대상·원인·수정: 공식 PostgreSQL image는 빈 data 디렉터리의 최초 기동에서 socket만 여는 임시 서버로 POSTGRES_DB를 만든 뒤 최종 서버로 다시 시작합니다. 기존 healthcheck(socket pg_isready)는 DB가 만들어지기 전의 임시 서버에서 이미 통과하므로 compose up --wait가 너무 일찍 돌아왔습니다. 그래서 backup.sh의 복구 검증(별도 project links-backup-verify의 빈 verify-postgres)이 느린 디스크에서 DB 생성 전에 접속해 [6/8]에서 실패할 수 있었습니다(BACKUP_EXIT=2, pin 해제, report.json 없음). 이제 root compose.yaml의 postgres와 ops/backup-verify.compose.yaml의 verify-postgres healthcheck가 TCP 127.0.0.1 pg_isready(임시 서버는 TCP를 열지 않음)와 대상 DB의 실제 쿼리(psql … select 1)를 함께 요구합니다. 같은 수정이 운영 postgres의 최초 기동(reset.sh 뒤 migration의 service_healthy 대기)에도 적용됩니다. healthcheck 설정이 바뀌므로 갱신 때 postgres container가 한 번 다시 만들어집니다(data는 그대로). 격리 gate에서 이전 container 종료부터 새 container healthy까지 약 5.3초(새 container 시작 → healthy 약 5.1초) 동안 PostgreSQL을 쓸 수 없었습니다. 실제 NAS의 중단 시간은 측정하지 않았습니다.
    2. 갱신 방법: 배포 디렉터리에서 sh update.sh만 실행합니다. schema는 바뀌지 않았고(infra/schema·infra/migrations·.env.example이 1.0.7과 같고 migration은 0개) reset.sh를 실행하지 않습니다. 배포 파일 중 compose.yaml과 ops/backup-verify.compose.yaml이 바뀌며 update.sh가 둘 다 받아 설치합니다. 1.0.8 설치 뒤 coordinator를 멈추고 sh backup.sh로 backup을 한 번 만들어 둡니다(운영 가이드 '운영 backup 명령'). 다음 1.0.9는 schema migration(게스트 공유 링크)을 포함하므로 그 전의 pre-update backup이 필요합니다. 1.0.7에서 실패한 backup 디렉터리(report.json 없음)는 복구에 쓰지 않습니다.
    3. 결과(R54 P3 수정과 NAS 조건 gate): 리뷰 R54의 P3(회귀 runner가 00 같은 인자로 검증 0회에도 PASS를 출력하던 문제)를 고쳤습니다. tests/run-postgres-init-race.sh가 반복 횟수와 지연을 선행 0 없는 양의 정수로만 받고 0·00·01·숫자 아님은 Docker 변경 전에 exit 2로 거부합니다. 릴리스 전에 격리 docker:24.0.2-dind(공식 Docker Compose v2.20.1, stager 1026:100, library 최상위 0:0 000 + 1026 ACL)에서 1.0.7 배포에 합성 VX Ace(rgss-wasm, ARCA 0.1.18)·MZ(web-static) ready build와 로그인 사용자의 세이브를 만든 뒤, 복구 검증 verify-postgres의 최초 기동에 15초+15초 init 지연을 넣었습니다. 1.0.7 backup.sh는 운영과 같이 [6/8]에서 database "links" does not exist로 실패했고(RED), 설치된 1.0.7 update.sh로 1.0.8에 갱신한 뒤 같은 지연에서 backup.sh가 8단계와 fresh restore drill을 모두 통과했습니다(아래 검증). Synology build(v2.20.1-6047-g6817716)와 실제 NAS 재현은 확인 전입니다.

    NAS 절차(1.0.7에서):

    cd /volume1/docker/links
    sh update.sh
    docker compose --env-file .env.production --profile runtime stop runtime-package-coordinator
    sh backup.sh
    docker compose --env-file .env.production --profile runtime up -d --no-build runtime-package-coordinator
    

    update.sh가 성공하면 단계 표시 6줄 뒤에 Updated to 1.0.8 (catalog 6f8cb1a5d406), runtime.package ready.가 출력됩니다. backup.sh는 [1/8]부터 [8/8]까지 출력한 뒤 Backup <id> PASS. Copy the whole directory off-host and keep it at least 30 days.로 끝나고, 실패하면 원인 한 줄을 출력하고 nonzero로 끝납니다. 복구 검증 단계([6/8])는 이제 새 PostgreSQL의 최초 init이 끝날 때까지 기다리므로 1.0.7보다 길어질 수 있습니다. backup 디렉터리 전체를 off-host로 복사해 둡니다.

    원본 game library는 읽기 전용으로 유지합니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    수정 내용

    • compose.yaml postgres, ops/backup-verify.compose.yaml verify-postgres: healthcheck를 pg_isready -q -h 127.0.0.1 -p 5432 -U … -d …와 PGPASSWORD=… psql -h 127.0.0.1 … -v ON_ERROR_STOP=1 -qAt -c "select 1"로 바꿨습니다. 비밀번호는 container 환경 변수 참조($${POSTGRES_PASSWORD})로만 쓰며 렌더링된 설정에 값이 들어가지 않습니다. interval·timeout·retries·start_period, image pin, read_only·user·tmpfs·network는 그대로입니다.
    • tests/run-postgres-init-race.sh(신규 gate)와 tests/postgres-init-race/(test 전용 entrypoint·override): 같은 PostgreSQL image의 entrypoint를 그대로 쓰면서 DB 생성 전과 임시 서버 종료 전에 지연을 넣어 두 service를 빈 volume으로 기동하고, up --wait가 돌아온 즉시 TCP·socket 쿼리·init 완료를 요구합니다. 1.0.7 healthcheck는 FAIL(RED), 수정 뒤 PASS입니다. 반복 횟수·지연 인자 검증(R54 P3)을 함께 고쳤습니다.
    • 바꾸지 않은 것: backup.sh(ops/backup-registry.sh)와 restore 수락 규칙, backup 형식, schema·migration, application 코드, ARCA pin.

    문서

    • 운영 가이드 '운영 backup 명령': 1.0.7 이하 [6/8] 실패의 증상·원인·조치와 수정 배포 파일
    • 완료 기준선(운영 backup 행·검증), tests README(runner 색인 run-postgres-init-race.sh와 인자 규칙, test 전용 override, 변경별 검증 선택)

    알려진 제한(후속 과제)

    • 실제 NAS에서 1.0.7 [6/8] 실패의 재현과 1.0.8의 backup.sh·복구, 갱신 때 PostgreSQL 재생성의 실제 중단 시간은 NOT RUN입니다. 배포 뒤 운영자가 위 절차로 backup을 만들고 report.json의 executionStatus·restore·evidence를 확인합니다.
    • 1.0.9 예고: 관리자 공유 링크 게스트 재생(schema migration 포함), ARCA 0.1.19(모바일 고 DPR 배율), 게스트 catalog 차단. migration이 있으므로 1.0.9 갱신 전에 이 릴리스의 backup이 있어야 합니다.
    • 1.0.7과 같은 제한: 실제 Cloudflare 경유 artifact header, VX Ace 실제 browser acceptance, Synology 실제 환경의 갱신, PowerShell coordinator 전체 실행은 NOT RUN입니다. update.sh는 network가 빠진 container를 스스로 감지하지 않습니다.

    검증과 남은 범위

    • PASS: 소스 gate(1.0.7 base, 리뷰 R54 '병합 가능')
      • run-postgres-init-race.sh 10(init 15초+15초 지연): 10회 모두 두 service가 healthy가 된 즉시 사용 가능(20/20, healthy 33–37초). 1.0.7 healthcheck의 RED(healthy 3–6초 뒤 database "<name>" does not exist)는 수정 과제(T54)의 기록을 재사용했습니다.
      • R54 P3: 수정 전 00은 검증 0회로 PASS와 "rounds":00을 출력했습니다(정적 확인). 수정 뒤 반복 횟수 00·01·0·abc·007·1x와 지연 00·015·0·abc가 Docker 변경 없이 exit 2, 1·10은 정상 실행.
      • run-ops-backup.sh: Player lock 충돌 fail closed, 8단계, fresh restore drill, rgss-wasm build 복구, save history, evidence 3. run-backup-restore.sh phase-0 drill: 11개 항목 모두 PASS(rgss-wasm active generation과 web-static 포함).
      • static(Prettier·ESLint·typecheck·docs-static), 문서 검사, git diff --check, reset 기록기 30·deploy 기록기 93 synthetic case, Compose config 3조합 Watchtower label(20·20·32)과 restore drill 렌더링(2).
      • schema gate: infra/schema·infra/migrations 변경 없음이라 NOT RUN.
    • PASS: 릴리스 gate(Compose v2.20.1, NAS 조건)
      • 환경: 격리 docker:24.0.2-dind, host Compose는 공식 v2.20.1 linux-x86_64 binary(release의 게시 checksum과 일치). 합성 CA의 HTTPS release 서버가 Gitea를, 합성 TLS registry가 git.2ex.dev를 대신했고, registry에는 실제 1.0.7 image와 이 릴리스의 1.0.8 image를 넣었습니다. 배포 파일은 1.0.7 tag와 이 릴리스 커밋의 Git blob입니다.
      • 1.0.7 설치 → reset.sh → update.sh: 12개 service, network 불일치 0, 모두 Watchtower 제외, readiness PASS. 합성 VX Ace(rgss-wasm, ARCA 0.1.18) ready, MZ ready, 로그인 사용자 세이브 201·북마크 1.
      • 복구 검증 verify-postgres에만 test 전용 init 지연(15초+15초)을 Docker PATH shim으로 넣었습니다(배포 파일은 release와 byte 동일). 1.0.7 backup.sh: drill postgres가 3초 만에 healthy → [6/8] database "links" does not exist, exit 2, pin 해제, report 없음(RED, 운영 증상과 동일).
      • 1.0.8 게시 뒤 설치된 1.0.7 update.sh: exit 0(65초), Pending migrations 줄 0, Updated to 1.0.8 (…), runtime.package ready., 7개 파일이 1.0.8과 같고 .tmp 0. postgres container 재생성(ID 변경, 새 healthcheck 적용, 새 container 시작 → healthy 5.1초). 12개 service, network 불일치 0, unhealthy 0, 12/12 Watchtower 제외, readiness PASS, catalog·Player 1.0.8. schema current(migration 0), row count 불변, 같은 사용자의 세이브 ETag·bytes·Player save 파일 목록 동일, VX Ace 0.1.18 build ready·active 유지, MZ ready.
      • coordinator 정지 → 같은 지연에서 sh backup.sh(1.0.8): exit 0(46초). drill postgres는 두 지연과 init 완료까지 31초 기다린 뒤 healthy, [1/8]–[8/8], Backup <id> PASS, report.json executionStatus PASS, restore drill schema current·migration 0, runtimeBuilds 2(rgss-wasm + web-static), runtime snapshot 78 files, save 1 namespace·2 files, evidence 2 build, active pin 0, verify project 잔여 0.
      • coordinator 재기동(7초 뒤 새 ready 줄) → 재-update: exit 0(34초), 배포 파일 불변, postgres 재생성 없음, 12 service 불일치 0, readiness PASS.
      • PostgreSQL 재생성 시간(같은 배포에서 1.0.7 → 1.0.8 healthcheck 설정 전환을 live event로 측정): 이전 container 종료 → 새 container 시작 0.22초, 시작 → healthy 5.07초, 합계 약 5.3초.
    • PASS: 최종 1.0.8 소스. 릴리스 커밋 두 개(root version, 완료 기준선의 version 문구) 뒤 문서 검사, static, git diff --check를 다시 통과했습니다.
    • 게시 이미지: 같은 최종 소스에서 build했고 gate도 이 image로 실행했습니다. image 안에서 root version 1.0.8, ARCA pin·설치본 0.1.18, migration 0개를 확인했습니다.
    • NOT RUN: 실제 NAS 재현·backup·복구와 1.0.7 → 1.0.8 갱신, 브라우저 matrix(변경은 PostgreSQL healthcheck뿐), live provider.

    배포 이미지와 파일

    • Git annotated tag는 1.0.8이고 최종 소스는 9f4ada7c6c입니다(branch release-1.0.8, 1.0.7 base).
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:1.0.8 = git.2ex.dev/hanyul/links:latest
    • 원격 digest: sha256:6f8cb1a5d406231f9851efd176ed3b5bc8cf41a993f542c8e6278ce6a3fa133a
    • release asset은 없습니다. update.sh가 tag 1.0.8의 7개 배포 파일을 HTTPS로 받습니다. 대응은 update.sh=ops/deploy-registry.sh, reset.sh=ops/reset-registry.sh, backup.sh=ops/backup-registry.sh이고, 나머지는 compose.yaml·infra/docker/Caddyfile·ops/backup-verify.compose.yaml·ops/runtime-package-coordinator.sh입니다.
    • 1.0.7 대비 바뀐 배포 파일은 compose.yaml과 ops/backup-verify.compose.yaml(healthcheck)뿐입니다.
    Downloads 2
  • LINKS 1.0.7 — backup이 VX/VX Ace build를 포함한 배포에서 실패하던 결함 수정

    LINKS 1.0.7

    이 hotfix 릴리스는 이전 릴리스 1.0.6(735ac60b1c) 위에 운영 backup 결함 수정만 얹었습니다. 최종 소스는 3f6c504352, 루트 패키지는 1.0.7, ARCA pin은 exact 0.1.18로 그대로입니다. 1.0.6 이하의 sh backup.sh는 VX/VX Ace(rgss-wasm) ready build가 하나라도 있으면 [2/8] pin 단계에서 artifact core manifest contains unknown or missing fields로 실패했습니다(web-static manifest parser를 모든 build에 적용한 결함). 이제 backup이 build adapter별 manifest 계약으로 pin·snapshot·복구 검증을 하고 RGSS build(Player tree·game/RTP CAS 객체·portable manifest)가 backup과 복구에 포함됩니다. schema는 바뀌지 않았습니다. DB 초기화와 migration이 필요 없습니다. backup 형식도 그대로(plan v2·artifact v2)이며 기존 web-static backup은 그대로 복구됩니다.

    업그레이드 주의: 1.0.6 → 1.0.7은 sh update.sh만 실행하고, 설치 뒤 반드시 sh backup.sh로 backup을 한 번 만들어 둡니다

    1. 대상·범위: 1.0.6 이하에서 sh backup.sh가 VX/VX Ace ready build가 하나라도 있으면 [2/8] Pausing runtime publication and pinning the runtime scope에서 artifact core manifest contains unknown or missing fields로 실패하던 결함을 고쳤습니다. pin·snapshot·복구 검증이 build의 adapter가 고른 계약(web-static은 artifact core manifest, rgss-wasm은 RGSS23 generation manifest v3)을 쓰고 알 수 없는 adapter·manifest는 실패합니다. rgss-wasm generation은 sealed.json이 나열한 Player tree 전체(runtime/ 하위 포함)와 game·RTP CAS 객체(package·portable manifest·artifact.json)가 snapshot·복구에 들어갑니다. backup 형식은 바뀌지 않았고(plan links-runtime-backup-plan-v2·artifact links-runtime-artifact-backup-v2) 기존 web-static backup과 호환됩니다. schema·migration·GC·contracts·배포 파일 7개는 1.0.6과 같습니다.
    2. 갱신 방법: 배포 디렉터리에서 sh update.sh만 실행합니다. schema는 바뀌지 않았고(infra/schema·infra/migrations·.env.example이 1.0.6과 같고 migration은 0개) reset.sh를 실행하지 않습니다. 바뀌는 것은 image뿐입니다. 1.0.7 설치 뒤 반드시 coordinator를 멈추고 sh backup.sh로 backup을 한 번 만들어 둡니다(운영 가이드 '운영 backup 명령'). 다음 1.0.8은 schema migration(게스트 공유 링크)을 포함하므로 그 전의 pre-update backup이 필수이며, 1.0.6 schema DB의 backup은 그 schema 버전을 사슬에 포함한 image(1.0.7)로만 만들 수 있습니다. report.json이 없는 backup 디렉터리는 실패한 backup입니다.
    3. NAS 조건 gate: 릴리스 전에 격리 docker:24.0.2-dind에서 이 갱신을 검증했습니다. host Compose는 공식 Docker Compose v2.20.1 binary(release의 게시 checksum으로 확인), stager는 1026:100, 원본 library 최상위는 0:0 000에 1026만 여는 ACL이었습니다. 1.0.6 배포에 합성 VX Ace(rgss-wasm, ARCA 0.1.18) ready build와 MZ(web-static) ready build, 로그인 사용자의 세이브를 만든 뒤 설치된 1.0.6 update.sh로 1.0.7 갱신(schema current, migration 0, 12 service network 일치, 모두 Watchtower 제외, readiness PASS), coordinator 정지 뒤 sh backup.sh 8단계 완료와 fresh restore drill(RGSS build·game/RTP CAS·save 포함), coordinator 재기동, 재-update(변경 없음)까지 모두 기대대로 끝났습니다(아래 검증). Synology build(v2.20.1-6047-g6817716)와 실제 NAS의 backup·복구는 확인 전입니다.

    NAS 절차(1.0.6에서):

    cd /volume1/docker/links
    sh update.sh
    docker compose --env-file .env.production --profile runtime stop runtime-package-coordinator
    sh backup.sh
    docker compose --env-file .env.production --profile runtime up -d --no-build runtime-package-coordinator
    

    update.sh가 성공하면 단계 표시 6줄 뒤에 Updated to 1.0.7 (catalog 91e3adb9fc5a), runtime.package ready.가 출력됩니다. backup.sh는 [1/8]부터 [8/8]까지 출력한 뒤 Backup <id> PASS. Copy the whole directory off-host and keep it at least 30 days.로 끝나고, 실패하면 원인 한 줄을 출력하고 nonzero로 끝납니다. backup 디렉터리 전체를 off-host로 복사해 둡니다.

    원본 game library는 읽기 전용으로 유지합니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    backup 수정 내용

    • packages/db/src/runtime-backup-manifest.ts(신규): build adapter별 manifest dispatch(web-static parseArtifactCoreManifest, rgss-wasm parseRgss23PlayerGenerationManifest, 둘 다 기존 export), RGSS game/RTP artifact.json 기대값(manifest에서 재구성, DB descriptor_sha256과 manifest descriptorSha256 둘 다와 대조).
    • runtime-backup.ts: scope query가 runtime_builds.adapter를 읽고 행마다 그 adapter의 parser로 manifest hash가 build hash와 같은지, 연결된 CAS 객체가 manifest가 지정한 객체인지 검증합니다. adapter와 manifest 계약 불일치, 알 수 없는 adapter, manifest가 지정하지 않은 RGSS 객체, RTP 누락은 거부합니다.
    • runtime-backup-snapshot.ts: RGSS generation은 sealed.json(links-rgss23-player-generation-v2)을 build 게시 때와 같은 parseRgss23BuildResult로 manifest에 묶고 Player tree 전체를 기대 목록에 넣습니다. 봉인 디렉터리 검사를 generation·객체 root 아래 모든 디렉터리로 넓혀 나열되지 않은 항목을 거부하고, 복구가 하위 디렉터리까지 0555로 봉인합니다. web-static 경로의 기대 파일·바이트는 이전과 같습니다.
    • 바꾸지 않은 것: plan·artifact snapshot 형식 번호, ops/backup-registry.sh·ops/backup-verify.compose.yaml, schema·migration, GC(runtime-gc.ts), contracts, 배포 파일 7개, ARCA pin.

    문서

    • 운영 가이드: 'Backup과 restore'(범위에 rgss-wasm generation·CAS 객체 명시), '운영 backup 명령'(adapter별 manifest 검증, RGSS snapshot 구성, 1.0.6 이하 결함과 수정 image 조건)
    • 완료 기준선(backup 명령 설명, 운영 backup·backup/restore·static·unit 행), 개발 계획(NAS 전환 3항), tests README(backup runner 범위)

    알려진 제한(후속 과제)

    • 실제 NAS의 sh backup.sh와 복구는 NOT RUN입니다. 배포 뒤 운영자가 위 절차로 backup을 만들고 report.json의 executionStatus·restore·evidence를 확인합니다.
    • 1.0.8 예고: 관리자 공유 링크 게스트 재생(schema migration 0001_guest_play_links 포함), ARCA 0.1.19(모바일 고 DPR 배율), 게스트 catalog 차단. migration이 있으므로 1.0.8 갱신 전에 이 릴리스의 backup이 있어야 합니다. 1.0.8을 적용하면 기존 VX/VX Ace active build는 새 generation이 필요합니다(1.0.8 노트에서 안내).
    • 구형(1.0.6 이하) image는 RGSS build가 든 snapshot을 읽지 못하므로, 이 릴리스로 만든 backup의 복구에도 1.0.7 이후 image를 씁니다.
    • 1.0.6과 같은 제한: 실제 Cloudflare 경유 artifact header, VX Ace 실제 browser acceptance, Synology 실제 환경의 갱신, PowerShell coordinator 전체 실행은 NOT RUN입니다. update.sh는 network가 빠진 container를 스스로 감지하지 않습니다. 3단계 이후 실패하면 coordinator가 정지된 채 남습니다.

    검증과 남은 범위

    • PASS: backup-rgss 수정(1.0.6 base에 cherry-pick, 코드 diff는 원 커밋과 byte 동일)
      • 관련 unit runtime-backup.test.ts(5)·runtime-backup-snapshot.test.ts(8) PASS, 전체 unit 150개 파일·1,201개 test PASS, static(Prettier·ESLint·workspace·tests typecheck·docs-static), 문서 검사, git diff --check
      • run-ops-backup.sh(web-static active/retained + rgss-wasm active seed): Player lock 충돌 fail closed, 8단계 완료, fresh restore drill PASS, rgssWasmBuildRestored PASS, evidence 3, snapshot 20 files
      • run-backup-restore.sh phase-0 drill: phase-0-backup-restore PASS(restartPersistence·freshRestore·runtimeArtifactRestore·saveSnapshot·saveHistory·savePayloadPreflight·saveFreshRestore·saveSourceBuilds·saveBrowserRestore·completedRuntimeBackupEvidence·failedBackupEvidenceRejection 모두 PASS, preserved에 rgss-wasm active generation Player tree·game/RTP CAS 객체·portable manifest 포함)
      • schema gate: infra/schema·infra/migrations·.env.example 변경 없음이라 NOT RUN
      • 리뷰(R52): 병합 가능(필수 수정 0). 1.0.6 base 적용은 Fix·Test 무충돌, Docs는 완료 기준선·tests README의 1.0.6 행을 유지한 채 backup 범위·검증과 결함 기록만 가져왔습니다
    • PASS: 릴리스 gate(Compose v2.20.1, NAS 조건)
      • 환경: 격리 docker:24.0.2-dind(overlay2), host Compose는 공식 v2.20.1 linux-x86_64 binary(release의 게시 checksum과 일치). 합성 CA의 HTTPS release 서버가 Gitea를, 합성 TLS registry가 git.2ex.dev를 대신했습니다. registry에는 실제 1.0.6 image와 이 릴리스의 1.0.7 image를 넣었습니다. 배포 파일은 1.0.6 tag와 이 릴리스 커밋의 Git blob입니다. scanner·stager 1026:100, ./data bind store, 원본 library 최상위 0:0 000 + ACL u:1026:r-x.
      • 1.0.6 설치 → reset.sh → update.sh: exit 0, 12개 service, network 불일치 0, 모두 Watchtower 제외, readiness 0/0. 합성 VX Ace(rgss-wasm, ARCA 0.1.18 build) ready, MZ(web-static) ready, 로그인 사용자 세이브 201·북마크 1.
      • 1.0.7 게시(registry latest = 1.0.7, asset 없음) 뒤 설치된 1.0.6 update.sh: exit 0(52초), Pending migrations 줄 0, Updated to 1.0.7 (catalog b6551c4d7ee8), runtime.package ready.(합성 registry digest). 7개 파일이 1.0.7과 같고 .tmp 0. 12개 service, network 불일치 0, unhealthy 0, 12/12 Watchtower 제외, package·runtime readiness exit 0, coordinator 재시작 0, catalog·Player 1.0.7. schema current(expected=recorded 14481d17…, pending 0, migration history 0행), row count 불변(installations 3, bookmarks 1, users 1, runtimeBuilds 2, activeHeads 2, jobs 5), 같은 사용자의 세이브 GET 200에 ETag·bytes·Player save 파일 목록 동일, VX Ace 0.1.18 build ready·active 유지(새 generation 불필요), MZ ready.
      • coordinator 정지 → sh backup.sh(1.0.7): exit 0(18초). [1/8]부터 [8/8]까지 8줄, Backup <id> PASS 1줄, report.json executionStatus PASS·image latest. fresh restore drill([6/8], project links-backup-verify): 복원 DB schema checksum 14481d17…(backup과 동일, migration 0), runtimeBuilds 2(rgss-wasm active + web-static active), runtime snapshot 78 files·45.7 MB(RGSS Player tree·game/RTP CAS·portable manifest 포함), save 1 namespace·2 files, evidence 2 build(rgss-wasm·web-static 각 1, restorable_until 미래), active pin 0, runtime_backup_runs 1 completed·build_count 2, verify project container·volume 잔여 0.
      • coordinator 재기동: up -d --no-build runtime-package-coordinator exit 0, 7초 뒤 Package coordinator ready (ARCA 0.1.18, …) 새 ready 줄, 재시작 0. 12 service network 불일치 0, readiness exit 0.
      • 설치 상태에서 재-update: exit 0(35초), 같은 요약 줄, 배포 파일(크기·mtime) 불변·.tmp 0, 12 service network 불일치 0, 12/12 Watchtower 제외, readiness 0/0, VX·MZ projection ready, schema current, row count 유지(playSessions만 세이브 확인 세션으로 1 증가), Player save 파일 목록 동일.
    • PASS: 최종 1.0.7 소스. 릴리스 커밋 두 개(root version, 완료 기준선의 version 문구) 뒤 문서 검사, static, git diff --check를 다시 통과했습니다.
    • 게시 이미지: 같은 최종 소스에서 build했고, gate도 이 image로 실행했습니다. image 안에서 root version 1.0.7과 ARCA pin·설치본 0.1.18을 확인했습니다.
    • NOT RUN: 실제 NAS backup·복구와 1.0.6 → 1.0.7 갱신, 실제 ARCA generation(운영 DB)의 backup, retained RGSS build 개별 drill, 브라우저 matrix(변경은 backup 경로뿐), live provider, 개발 rebuild.

    배포 이미지와 파일

    • Git annotated tag는 1.0.7이고 최종 소스는 3f6c504352입니다(branch release-1.0.7, 1.0.6 base).
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:1.0.7 = git.2ex.dev/hanyul/links:latest
    • 원격 digest: sha256:91e3adb9fc5a61cf4839dfd862fb60843cf0bb3f6014b4bb99885d0aea3d4d1a
    • release asset은 없습니다. update.sh가 tag 1.0.7의 7개 배포 파일을 HTTPS로 받습니다. 대응은 update.sh=ops/deploy-registry.sh, reset.sh=ops/reset-registry.sh, backup.sh=ops/backup-registry.sh이고, 나머지는 compose.yaml·infra/docker/Caddyfile·ops/backup-verify.compose.yaml·ops/runtime-package-coordinator.sh입니다.
    • 7개 배포 파일은 모두 1.0.6과 같습니다.
    Downloads 2
  • LINKS 1.0.6 — CDN 압축으로 인한 Player artifact 검사 실패 차단

    LINKS 1.0.6

    이 patch 릴리스는 이전 릴리스 1.0.5(604d5ffec1)부터 최종 소스 735ac60b1c까지를 묶었습니다. 루트 패키지는 1.0.6이고 ARCA pin은 exact 0.1.18로 그대로입니다. Cloudflare 같은 CDN·reverse proxy가 Player runtime artifact 응답을 압축해 브라우저의 무결성 검사가 실패하던 문제를 막았습니다. 바뀐 application 코드는 Player의 응답 header 두 곳입니다. schema는 바뀌지 않았습니다. DB 초기화와 migration이 필요 없습니다.

    업그레이드 주의: 1.0.5 → 1.0.6은 sh update.sh만 실행합니다(초기화 없음)

    1. 갱신 방법: 1.0.5에서는 배포 디렉터리에서 sh update.sh만 실행합니다. schema는 바뀌지 않았고(infra/schema·infra/migrations·.env.example이 1.0.5와 같고 migration은 0개) reset.sh를 실행하지 않습니다. 7개 배포 파일도 1.0.5와 같으며, 바뀌는 것은 image뿐입니다. 갱신 전 backup(sh backup.sh)은 선택입니다. 1.0.5부터 LINKS container는 host Watchtower의 대상이 아니므로 실행 시각을 맞출 필요가 없습니다. 1.0.4 이하에서 올 때는 1.0.5 노트의 첫 갱신 주의를 따릅니다.
    2. 고친 문제: CDN·reverse proxy가 Player runtime artifact 응답을 압축하면 Content-Encoding이 붙고 Content-Length가 빠지거나 strong ETag가 약해집니다. 그러면 게임 실행이 fetching-runtime 단계의 REQUEST_FAILED(진단 RuntimeCacheError ARTIFACT_METADATA)로 실패했습니다. 이제 Player가 artifact 응답(GET·HEAD·206)에 Cache-Control: private, max-age=0, must-revalidate, no-transform을, save GET·POST 응답에 Cache-Control: no-store, no-transform을 보냅니다. Cloudflare는 origin의 no-transform이 있으면 압축하지 않고 Content-Length를 유지합니다. 브라우저의 bytes·Content-Length·strong ETag 검사는 완화하지 않았습니다.
    3. CDN 요건과 확인 방법: 운영 가이드(docs/운영_가이드.md)의 'CDN·reverse proxy의 artifact 변형' 절을 따릅니다. /player/*에는 Cache-Control을 지우거나 덮어쓰는 proxy 규칙(Transform Rules, Cache Rules의 header 변경 등)과 본문을 바꾸는 기능(Rocket Loader, Email Obfuscation 등)을 적용하지 않습니다. 갱신 뒤 공개 주소에서 실제 browser와 같은 Accept-Encoding으로 artifact URL을 HEAD합니다. 응답에 십진수 Content-Length, "sha256-…" strong ETag, no-transform이 있고 Content-Encoding이 없어야 합니다. gateway(30007)에 직접 보낸 결과와 header가 다르면 proxy 설정을 고칩니다. token과 실제 경로는 log·신고·문서에 남기지 않습니다.
    4. NAS 조건 gate: 릴리스 전에 격리 docker:24.0.2-dind에서 이 갱신을 검증했습니다. host Compose는 공식 Docker Compose v2.20.1 binary(release의 게시 checksum으로 확인)였고, stager는 1026:100, 원본 library 최상위는 0:0 000에 1026만 여는 ACL이었습니다. 설치된 1.0.5 update.sh로 1.0.6 갱신, VX Ace 준비, gateway를 거친 실제 Player session의 artifact header 확인, 재-update까지 모두 기대대로 끝났습니다(아래 검증). Synology build(v2.20.1-6047-g6817716)와 실제 Cloudflare 경유 결과는 확인 전입니다.

    NAS 절차(1.0.5에서):

    cd /volume1/docker/links
    sh update.sh
    

    성공하면 단계 표시 6줄 뒤에 Updated to 1.0.6 (catalog 484addc10c12), runtime.package ready.가 출력됩니다. 실패하면 원인 한 줄을 출력하고 nonzero로 끝납니다.

    원본 game library는 읽기 전용으로 유지합니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    Player 응답 header

    • 상수 artifactCacheControl(artifact route의 GET·HEAD·206이 같은 header 경로를 씁니다)과 saveCacheControl(save POST 200/201, save GET binary)을 추가했습니다.
    • 기존 private·max-age=0·must-revalidate·no-store 의미는 그대로이고 no-transform만 더했습니다. 브라우저 cache 판단, CSP, origin·session, save 형식은 바뀌지 않았습니다.
    • gateway Caddy는 encode를 쓰지 않고 Player 응답 header를 그대로 전달하므로 바뀌지 않았습니다.
    • problem 응답, /player/*/session·/launch JSON, /player/frame과 catalog API 응답은 header 검사 대상이 아니어서 바꾸지 않았습니다.

    문서

    • 운영 가이드: 'CDN·reverse proxy의 artifact 변형'(금지 사항, 증상, Cloudflare 설정 주의, HEAD 확인 방법)
    • API 계약(Artifact·Save의 Cache-Control 값), 브라우저 게임 런타임 계약(runtime cache HEAD 검사와 no-transform의 관계), 완료 기준선

    알려진 제한(후속 과제)

    • 실제 Cloudflare를 거친 artifact header 확인과 VX Ace 실제 browser acceptance는 배포 뒤 확인할 항목이며 지금은 NOT RUN입니다.
    • Cloudflare Enterprise zone에서 Origin Cache Control이 꺼져 있으면 no-transform만으로 압축 차단을 보장하지 못합니다. 운영 가이드에 이 조건을 더하는 문서 보완이 후속입니다. 위 업그레이드 주의 3의 HEAD 확인이 이 경우도 검출합니다.
    • 1.0.5와 같은 제한:
      • 실제 NAS(Synology Compose build, Synology ACL·btrfs)에서의 갱신·VX/VX Ace package, PowerShell coordinator 전체 실행은 NOT RUN입니다.
      • update.sh는 network가 빠진 container를 스스로 감지하지 않습니다. 운영 가이드 '실패와 되돌리기'로 복구합니다.
      • 재확인→게시 간격이 128 MiB에서 약 1.2초 늘어납니다(봉인 사본 검증은 게임 크기에 비례).
      • /artifacts 쓰기 거부는 기존 분류를 따르고, load 뒤 권한 변경은 ARTIFACT_INVALID로 끝납니다.
      • 소유 계정이 섞인 sealed store는 지원하지 않습니다.
      • Docker 24 graphdriver에서는 Builder config digest를 pin과 직접 대조하지 않습니다(ARCA 후속).
      • 3단계 이후 실패하면 coordinator가 정지된 채 남습니다. 원인을 고친 뒤 다음 sh update.sh가 다시 기동합니다.
      • Player 세이브 관리 listener(3005)는 catalog caller token으로만 보호되며, edge network에서 TCP로 닿을 수 있습니다.
      • catalog·Player의 일반 egress는 network로 제한하지 않습니다.
      • update.sh는 opt-in profile container(translation-import 등)를 갱신하지 않습니다.

    검증과 남은 범위

    • PASS: no-transform 브랜치(병합 48e9655)
      • Player unit RED → GREEN: 수정 전 app에서 artifact·save Cache-Control 단언 2건 FAIL, 수정 뒤 apps/player/src 9개 파일·92개 test PASS(HEAD·Range 206 단언 포함)
      • 전체 unit 149개 파일·1,190개 test, static(Prettier·ESLint·typecheck·docs-static), 문서 검사, git diff --check
      • 운영 infra/docker/Caddyfile과 같은 pinned Caddy image로 합성 upstream을 거친 gateway 통과 확인(Content-Encoding 없음, Content-Length 유지)
      • 리뷰 최종 판정: 병합 가능(필수 수정 0, P3 1건은 위 Cloudflare Enterprise 문서 보완)
    • PASS: 릴리스 gate(Compose v2.20.1, NAS 조건)
      • 환경: 격리 docker:24.0.2-dind(overlay2), host Compose는 공식 v2.20.1 linux-x86_64 binary(release의 게시 checksum과 일치). 합성 CA의 HTTPS release 서버가 Gitea를, 합성 TLS registry가 git.2ex.dev를 대신했습니다. registry에는 실제 1.0.5 image와 이 릴리스의 1.0.6 image를 넣었습니다. 배포 파일은 1.0.5 tag와 이 릴리스 커밋의 Git blob입니다.
      • 운영과 같은 계정과 원본 권한: scanner·stager 1026:100, ./data bind store, 원본 library 최상위 0:0 000 + ACL u:1026:r-x.
      • 1.0.5 설치 → reset.sh → update.sh: exit 0. 12개 service, network 불일치 0, 모두 label false, readiness 0/0.
      • 1.0.6 게시(registry latest = 1.0.6, asset 없음) 뒤 설치된 1.0.5 update.sh: exit 0(53초), Updated to 1.0.6 .... 7개 파일이 release와 같고 .tmp는 0입니다. app container가 1.0.6 image로 다시 만들어졌고 12개 service 모두 label false, network 불일치 0, unhealthy 0, package·runtime readiness exit 0, coordinator 재시작 0, catalog·Player version 1.0.6이었습니다.
      • VX Ace 준비(1.0.6): seal(runtime-package-stager, 1026:100) → stage → Builder → verify-source → publish → build ready. package·build job 모두 attempt 1에 성공했고 모든 one-shot이 exit 0이며 label false였습니다. sealed 항목은 모두 1026:100이고, Builder 계정의 읽기·탐색 실패는 0입니다.
      • gateway 경유 실제 Player session: launch broker와 같은 DB 경로로 발급한 launch grant를 gateway(30007)의 POST /player/<key>/launch로 교환했습니다. 그 session으로 entrypoint(host-launch.json), player module, runtime JS, runtime WASM(44 MB), game package 5개 artifact를 Accept-Encoding: gzip, deflate, br, zstd로 요청했습니다. HEAD·GET 200과 Range 206 모두 Cache-Control … no-transform, 십진수 Content-Length, strong "sha256-…" ETag가 있었고 Content-Encoding은 없었습니다. GET 본문의 크기와 digest가 Content-Length·ETag와 일치했고, 206에는 Content-Range가 있었습니다. 브라우저 대신 curl을 썼고 Cloudflare는 거치지 않았습니다.
      • 설치 상태에서 재-update: exit 0(13초), 배포 파일 변화 없음, service container 재생성 없음(coordinator·migration one-shot만 재기동), 준비 상태 ready 유지, network 불일치 0, readiness 0/0.
    • PASS: 최종 1.0.6 소스. 릴리스 커밋 두 개(root version, 완료 기준선의 version 문구) 뒤 다음을 다시 통과했습니다: 문서 검사(761개 파일 / Markdown 32개), static(Prettier·ESLint·typecheck·docs-static), git diff --check.
    • 게시 이미지: 같은 최종 소스에서 build했고, gate도 이 image로 실행했습니다. image 안에서 root version 1.0.6과 ARCA pin·설치본 0.1.18을 확인했습니다.
    • PARTIAL: 전체 UI/제품 acceptance. 화면·fixture·package/build 성공과 header 확인을 실제 browser play PASS로 기록하지 않습니다.
    • NOT RUN:
      • 실제 Cloudflare를 거친 artifact HEAD와 VX Ace 실제 browser 실행(배포 뒤 확인)
      • 브라우저 matrix(run-browser): 바뀐 것은 Cache-Control 값이고 browser·client 코드는 Cache-Control을 읽지 않습니다
      • Synology NAS 실제 환경(Compose v2.20.1 Synology build, Synology ACL, Docker 24 btrfs)에서의 1.0.5 → 1.0.6 갱신
      • PowerShell coordinator 전체 실행
      • 실제 Gitea release·registry를 대상으로 한 전체 stack 갱신(gate는 합성 release 서버·registry)
      • 실제 library/private game과 실제 scan
      • 입력·오디오·save/reload의 실제 게임 acceptance
      • live provider
      • 개발 rebuild

    배포 이미지와 파일

    • Git annotated tag는 1.0.6이고 최종 소스는 735ac60b1c입니다.
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:1.0.6 = git.2ex.dev/hanyul/links:latest
    • 원격 digest: sha256:484addc10c1290a4ce89ca21f1b48d6160cc701dcb5c74fb756dfc2a73e2efdd
    • release asset은 없습니다. update.sh가 tag 1.0.6의 7개 배포 파일을 HTTPS로 받습니다. 대응은 update.sh=ops/deploy-registry.sh, reset.sh=ops/reset-registry.sh, backup.sh=ops/backup-registry.sh이고, 나머지는 compose.yaml·infra/docker/Caddyfile·ops/backup-verify.compose.yaml·ops/runtime-package-coordinator.sh입니다.
    • 7개 배포 파일은 모두 1.0.5와 같습니다.
    Downloads 2
  • LINKS 1.0.5 — host Watchtower와의 배포 충돌 차단

    LINKS 1.0.5

    이 patch 릴리스는 이전 릴리스 1.0.4(f9171bfa75)부터 최종 소스 604d5ffec1까지를 묶었습니다. 루트 패키지는 1.0.5이고 ARCA pin은 exact 0.1.18로 그대로입니다. 1.0.3·1.0.4 배포 실패의 원인인 host Watchtower와의 충돌을 막았습니다. 모든 LINKS container에 Watchtower 제외 label을 붙였으므로, host의 다른 프로젝트용 Watchtower가 LINKS container를 교체하지 않습니다. application 코드는 바뀌지 않았습니다(Compose·test·문서). schema는 바뀌지 않았습니다. DB 초기화와 migration이 필요 없습니다.

    업그레이드 주의: 1.0.4 → 1.0.5는 sh update.sh만 실행합니다(초기화 없음)

    1. 갱신 방법과 실행 시각: 1.0.4에서는 배포 디렉터리에서 sh update.sh만 실행합니다. host에 Watchtower가 있으면 이번 첫 갱신은 Watchtower 실행 시각과 겹치지 않게 실행합니다. label이 없는 1.0.4 container는 update.sh가 다시 만들기 전까지 아직 Watchtower 대상입니다. 실행 주기는 docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' <watchtower container> | grep WATCHTOWER_SCHEDULE로 확인합니다. 6필드 cron의 첫 칸은 초이므로 30 4 * * * *는 매시 4분 30초입니다. 이번 갱신 뒤부터는 Watchtower가 LINKS container를 건드리지 않으며, Watchtower 설정은 바꾸지 않습니다. schema는 바뀌지 않았고(infra/migrations/base.sha256이 1.0.4와 같고 migration은 0개) reset.sh를 실행하지 않습니다. 갱신 전 backup(sh backup.sh)은 선택입니다. 1.0.2 이하에서 올 때는 1.0.3과 같은 1회 curl 전환을 거칩니다(운영 가이드 '최초 설치와 이전 release에서의 전환').
    2. Watchtower 제외 label: compose.yaml의 모든 service와 backup restore drill(ops/backup-verify.compose.yaml의 verify·verify-postgres)에 com.centurylinklabs.watchtower.enable: "false"가 붙었습니다. compose run으로 만드는 one-off(package worker·stager, backup, migration)와 Builder container도 이 label을 물려받습니다.
    3. 1.0.3·1.0.4 배포 실패의 원인과 복구: 1.0.3·1.0.4 배포에서 No such container, 임시 이름 <id>_links-* container, worker의 backend network 누락(ENOTFOUND postgres)이 나타났습니다. 원인은 host의 다른 프로젝트용 Watchtower였습니다. 이 Watchtower는 label-enable 없이 모든 container를 감시하고 cleanup을 켠 상태로, update.sh 실행 중에 LINKS :latest container를 교체했습니다. 이런 실패가 다시 보이면 운영 가이드 '실패와 되돌리기'의 절차를 따릅니다. 먼저 실패 시각의 Watchtower log에 Found new image·Started new container container=links-…가 있는지 확인합니다. 있으면 LINKS project의 Created 잔여 container만 확인한 뒤 지우고 sh update.sh를 다시 실행합니다. volume·bind data·원본은 지우지 않습니다.
    4. container 재생성: label이 새로 추가됐으므로 이번 갱신에서 모든 LINKS container(postgres·gateway 포함)가 한 번 다시 만들어집니다. data는 bind·volume에 그대로 남습니다.
    5. NAS 조건 gate: 릴리스 전에 격리 docker:24.0.2-dind에서 이 갱신을 검증했습니다. host Compose는 공식 Docker Compose v2.20.1 binary(release의 게시 checksum으로 확인)였고, stager는 1026:100, 원본 library 최상위는 0:0 000에 1026만 여는 ACL이었습니다. dind 안에 nickfedor/watchtower(1.23.0)를 운영과 같은 env로 띄웠습니다(label-enable 없음, WATCHTOWER_CLEANUP=true, schedule 30 4 * * * *). 이 상태에서 설치된 1.0.4 update.sh로 1.0.5 갱신, registry latest 재게시 뒤 Watchtower 갱신 실행, VX Ace 준비, 재-update까지 모두 기대대로 끝났습니다(아래 검증). Synology build(v2.20.1-6047-g6817716)와 실제 NAS의 Watchtower 일정은 gate와 다를 수 있으므로 실제 NAS 결과는 확인 전입니다.

    NAS 절차(1.0.4에서, Watchtower 실행 시각을 피해서):

    cd /volume1/docker/links
    sh update.sh
    

    성공하면 단계 표시 6줄 뒤에 Updated to 1.0.5 (catalog 8cc47b4ca0a0), runtime.package ready.가 출력됩니다. 실패하면 원인 한 줄을 출력하고 nonzero로 끝납니다. 갱신 뒤 docker inspect --format '{{index .Config.Labels "com.centurylinklabs.watchtower.enable"}}' <links container>는 false입니다. docker logs --since 1h <watchtower container> 2>&1 | grep links-에는 Found new image·Started new container가 없어야 합니다.

    원본 game library는 읽기 전용으로 유지합니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    Watchtower 제외 label

    • compose.yaml의 x-app-base가 label을 갖고, anchor를 쓰지 않는 service(byedpi, gateway, runtime-arca-builder, runtime-package-coordinator, postgres)는 같은 label을 직접 둡니다. 운영 렌더링의 20개 service 모두에 붙습니다.
    • backup restore drill의 임시 project(verify·verify-postgres)와 test 조합(compose.test.yaml)이 더하는 service에도 붙습니다.
    • nickfedor/watchtower는 label-enable 설정과 관계없이 enable=false container를 갱신 대상에서 뺍니다. host Watchtower 설정은 바꾸지 않아도 됩니다.
    • 검사 script tests/scripts/compose-watchtower-label-check.mjs가 렌더링된 Compose에서 label 없는 service를 찾으면 실패합니다. 대상은 운영, 운영+개발, 운영+test의 전체 profile과 backup restore drill입니다.

    문서

    • 운영 가이드: label 설명, 첫 갱신 시각 확인, 갱신 뒤 label·Watchtower log 확인, 실패와 되돌리기의 Watchtower 진단 조건과 Created 잔여 container 복구 절차
    • 완료 기준선(제공 범위, Compose·registry 행), tests/README(단독 Compose 검사·변경별 검증)

    알려진 제한(후속 과제)

    • 실제 NAS(Synology Compose build, Synology ACL·btrfs)에서의 갱신·VX/VX Ace package, PowerShell coordinator 전체 실행은 NOT RUN입니다.
    • gate는 Watchtower 갱신을 한 번 직접 실행했습니다. 실제 NAS의 schedule(매시 4분 30초)과 겹친 갱신은 실행하지 않았습니다.
    • 1.0.4 이하의 label 없는 container가 Watchtower 실행과 겹치면 이번 첫 갱신도 같은 방식으로 실패할 수 있습니다. 위 업그레이드 주의 1을 따릅니다.
    • update.sh는 network가 빠진 container를 스스로 감지하지 않습니다. 운영 가이드 '실패와 되돌리기'로 복구합니다.
    • 1.0.4와 같은 제한:
      • 재확인→게시 간격이 128 MiB에서 약 1.2초 늘어납니다(봉인 사본 검증은 게임 크기에 비례).
      • /artifacts 쓰기 거부는 기존 분류를 따르고, load 뒤 권한 변경은 ARTIFACT_INVALID로 끝납니다.
      • 소유 계정이 섞인 sealed store는 지원하지 않습니다.
      • Docker 24 graphdriver에서는 Builder config digest를 pin과 직접 대조하지 않습니다(ARCA 후속).
      • 3단계 이후 실패하면 coordinator가 정지된 채 남습니다. 원인을 고친 뒤 다음 sh update.sh가 다시 기동합니다.
      • Player 세이브 관리 listener(3005)는 catalog caller token으로만 보호되며, edge network에서 TCP로 닿을 수 있습니다.
      • catalog·Player의 일반 egress는 network로 제한하지 않습니다.
      • update.sh는 opt-in profile container(translation-import 등)를 갱신하지 않습니다.

    검증과 남은 범위

    • PASS: wt-optout 브랜치(병합 c6df686)
      • label 검사 RED → GREEN: 수정 전에는 운영 전체 profile의 20개 service 모두 누락이었습니다. 수정 뒤 운영 20, 운영+개발 20, 운영+test 32, backup restore drill 2가 모두 exit 0입니다.
      • 렌더링 diff는 label 추가뿐입니다. 격리 dind Watchtower 확인(label 붙은 service·one-off는 그대로, label 없는 control만 교체)을 마쳤습니다.
      • ops-backup restore drill, reset 기록기 30·update.sh 기록기 93, static, 문서 검사, git diff --check
      • 리뷰 최종 판정: 병합 가능(P1~P3 없음)
    • PASS: 릴리스 gate(Compose v2.20.1, NAS 조건, host Watchtower)
      • 환경: 격리 docker:24.0.2-dind(overlay2), host Compose는 공식 v2.20.1 linux-x86_64 binary(release의 게시 checksum과 일치). 합성 CA의 HTTPS release 서버가 Gitea를, 합성 TLS registry가 git.2ex.dev를 대신했습니다(dind daemon과 Watchtower container에만 적용). registry에는 실제 1.0.4 image와 이 릴리스의 1.0.5 image를 넣었습니다. 배포 파일은 1.0.4 tag와 이 릴리스 커밋의 Git blob입니다.
      • 운영과 같은 계정과 원본 권한: scanner·stager 1026:100, ./data bind store, 원본 library 최상위 0:0 000 + ACL u:1026:r-x.
      • 1.0.4 설치 → reset.sh → update.sh: exit 0(106초). 12개 service, network 불일치 0, label 없음.
      • nickfedor/watchtower 1.23.0을 운영 env(label-enable 없음, cleanup, 30 4 * * * *)로 기동하고, label 없는 다른 프로젝트 control container를 함께 띄웠습니다.
      • 1.0.5 게시(registry latest = 1.0.5, asset 없음) 뒤 설치된 1.0.4 update.sh: exit 0(59초), Updated to 1.0.5 .... 7개 파일이 release와 같고 .tmp는 0입니다. 12개 container가 모두 다시 만들어져 label false를 가졌고, network 불일치 0, unhealthy 0, package·runtime readiness exit 0, coordinator 재시작 0이었습니다.
      • registry latest를 새 digest로 다시 게시(같은 1.0.5 내용에 label 하나 추가)하고 control image도 새 digest로 게시한 뒤, 같은 Watchtower container에서 갱신을 한 번 실행했습니다. Update session completed failed=0 scanned=4 updated=1이었습니다. 검사 대상은 LINKS 밖 container 4개였고, 교체된 것은 label 없는 control뿐이었습니다. LINKS container 12개는 모두 enable=false로 제외됐고 container ID·StartedAt이 바뀌지 않았습니다. scheduled Watchtower log에도 links- 줄이 0입니다. 그 뒤 latest를 게시된 1.0.5 digest로 되돌렸습니다.
      • VX Ace 준비: seal(runtime-package-stager, 1026:100) → stage → Builder → verify-source(runtime-package-stager, 1026:100) → publish → build ready. attempt 1에 성공했고 모든 one-shot이 exit 0이며 label false였습니다. sealed 항목은 모두 1026:100이고, Builder 계정의 읽기·탐색 실패는 0입니다.
      • 설치 상태에서 재-update: exit 0(14초), 배포 파일 변화 없음, container 재생성 없음(coordinator·migration one-shot만 재기동), 준비 상태 ready 유지, network 불일치 0, readiness 0/0.
    • PASS: 최종 1.0.5 소스. 릴리스 커밋 두 개(root version, 완료 기준선의 version 문구) 뒤 다음을 다시 통과했습니다: 문서 검사(760개 파일 / Markdown 32개), static(Prettier·ESLint·typecheck·docs-static), git diff --check.
    • 게시 이미지: 같은 최종 소스에서 build했고, gate도 이 image로 실행했습니다. image 안에서 root version 1.0.5와 ARCA pin·설치본 0.1.18을 확인했습니다.
    • PARTIAL: 전체 UI/제품 acceptance. 화면·fixture·package/build 성공을 실제 browser play PASS로 기록하지 않습니다.
    • NOT RUN:
      • Synology NAS 실제 환경(Compose v2.20.1 Synology build, Synology ACL, Docker 24 btrfs)에서의 1.0.4 → 1.0.5 갱신과 실제 Watchtower 일정
      • PowerShell coordinator 전체 실행
      • 실제 Gitea release·registry를 대상으로 한 전체 stack 갱신(gate는 합성 release 서버·registry)
      • 실제 library/private game과 실제 scan
      • 입력·오디오·save/reload의 실제 게임 acceptance
      • live provider
      • 개발 rebuild

    배포 이미지와 파일

    • Git annotated tag는 1.0.5이고 최종 소스는 604d5ffec1입니다.
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:1.0.5 = git.2ex.dev/hanyul/links:latest
    • 원격 digest: sha256:8cc47b4ca0a023cff1795731a9940b0250d0fad10511297ecfaee4542a8b8036
    • release asset은 없습니다. update.sh가 tag 1.0.5의 7개 배포 파일을 HTTPS로 받습니다. 대응은 update.sh=ops/deploy-registry.sh, reset.sh=ops/reset-registry.sh, backup.sh=ops/backup-registry.sh이고, 나머지는 compose.yaml·infra/docker/Caddyfile·ops/backup-verify.compose.yaml·ops/runtime-package-coordinator.sh입니다.
    • 1.0.4 대비 바뀐 배포 파일은 두 개입니다. compose.yaml과 ops/backup-verify.compose.yaml(Watchtower 제외 label)입니다. 나머지 5개는 같습니다.
    Downloads 2
  • LINKS 1.0.4 — VX/VX Ace 원본 재확인을 stager 계정으로·원본 권한 code 분리

    LINKS 1.0.4

    이 patch 릴리스는 이전 릴리스 1.0.3(5e262a8dde)부터 최종 소스 f9171bfa75까지를 묶었습니다. 루트 패키지는 1.0.4이고 ARCA pin은 exact 0.1.18로 그대로입니다. 1.0.3 운영 결함 하나를 고쳤습니다. VX/VX Ace 게시 직전의 원본 재확인을 stager 계정 one-shot으로 옮겼으므로, 원본을 stager 계정만 읽을 수 있는 NAS(ACL)에서도 준비가 끝까지 진행됩니다. 원본 읽기 거부와 저장소 거부는 서로 다른 code로 표시됩니다. schema는 바뀌지 않았습니다. DB 초기화와 migration이 필요 없습니다.

    업그레이드 주의: 1.0.3 → 1.0.4는 sh update.sh만 실행합니다(초기화 없음)

    1. 갱신 방법: 1.0.3에서는 배포 디렉터리에서 sh update.sh만 실행합니다. 1.0.2 이하에서 올 때는 1.0.3과 같은 1회 curl 전환을 거칩니다. 아래 절차의 tag만 1.0.4로 바꿔 새 update.sh를 한 번 받고, 그다음 sh update.sh를 실행합니다. 1.0.1 이하라면 먼저 host coordinator를 종료합니다(운영 가이드 '최초 설치와 이전 release에서의 전환'). schema는 바뀌지 않았고(infra/migrations/base.sha256이 1.0.3과 같고 migration은 0개) reset.sh를 실행하지 않습니다. 갱신 전 backup(sh backup.sh)은 선택입니다.
    2. 원본 재확인 계정: VX/VX Ace 게시 직전의 원본 재확인이 stager 계정(LINKS_RUNTIME_STAGER_UID:GID)의 one-shot runtime-package-stager(새 명령 verify-source)에서 실행됩니다. 이전에는 package worker(10001)가 했습니다. 그래서 원본을 stager 계정만 읽을 수 있는 NAS(Synology ACL)에서도 준비가 seal → stage → Builder → 원본 재확인 → 게시까지 진행됩니다. 10001 package worker(runtime-package-worker)는 원본 library를 mount하지 않습니다. 운영자 추가 작업은 없습니다. 바뀐 compose.yaml·coordinator script는 update.sh가 설치하고, .env.production·원본 ACL·저장소 소유권은 그대로 씁니다.
    3. 원본 권한 code 분리: 원본 읽기가 거부되면 새 code RUNTIME_SOURCE_PERMISSION_DENIED로 끝납니다. 이 code는 재시도할 수 없습니다. 원본 접근 권한, stager 계정, NAS ACL을 확인합니다. 저장소(sealed·artifact store) 거부는 계속 RUNTIME_STORE_PERMISSION_DENIED입니다. 두 code 모두 진단에는 경로 없는 원인 code(EACCES 등)만 남습니다.
    4. 1.0.3에서 실패한 준비: 1.0.3에서 RUNTIME_STORE_PERMISSION_DENIED(publishing 단계, cause EACCES)로 끝난 VX/VX Ace 준비는 재시도 불가로 기록돼 있습니다. 화면의 다시 시도 대신 관리자 게임 관리의 실행 빌드 관리에서 빌드 재생성으로 다시 요청합니다.
    5. NAS 조건 gate: 릴리스 전에 격리 docker:24.0.2-dind에서 이 갱신을 검증했습니다. host Compose는 공식 Docker Compose v2.20.1 binary(release의 게시 checksum으로 확인)였습니다. 계정은 stager 1026:100이고, 원본 library 최상위는 0:0 000에 1026만 여는 ACL을 두었습니다(10001은 읽지 못함). 1.0.3에서 VX Ace 준비가 운영과 같은 RUNTIME_STORE_PERMISSION_DENIED로 실패하는 것을 재현했습니다. 이어서 설치된 1.0.3 update.sh로 1.0.4 갱신, 빌드 재생성으로 build ready, 재-update까지 모두 성공했습니다(아래 검증). Synology build(v2.20.1-6047-g6817716)·Synology ACL과 공식 binary·Linux POSIX ACL은 다를 수 있으므로 실제 NAS 결과는 확인 전입니다.

    NAS 절차(1.0.3에서):

    cd /volume1/docker/links
    sh update.sh
    

    성공하면 단계 표시 6줄 뒤에 Updated to 1.0.4 (catalog fd371daaf742), runtime.package ready.가 출력됩니다. 실패하면 원인 한 줄을 출력하고 nonzero로 끝납니다.

    원본 game library는 읽기 전용으로 유지합니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    VX/VX Ace 원본 재확인의 stager 계정 이동

    • coordinator의 한 attempt는 seal → stage → Builder → verify-source → publish 순서입니다. seal과 verify-source는 stager 계정의 runtime-package-stager에서 실행하고, stage와 publish는 10001 package worker에서 실행합니다. sh·PowerShell coordinator 모두 verify-source를 publish 바로 앞에 두고, 둘 사이에 다른 단계는 없습니다.
    • verify-source는 seal이 남긴 봉인 identity를 엄격히 검사합니다. 그다음 원본을 같은 함수와 인자로 다시 확인하고, 일치 결과를 경로·제목 없이 checkpoint에 남깁니다. publish는 다른 작업보다 먼저 이 결과를 handoff와 대조합니다. 결과가 없거나 맞지 않으면 게시하지 않습니다(RUNTIME_PACKAGE_INVALID_OUTPUT).
    • 봉인 뒤 원본이 바뀌면 이전과 같이 RUNTIME_SOURCE_CHANGED(재시도 가능)로 끝납니다. 원본 본문 읽기는 이전과 같이 seal 2회와 재확인 1회입니다. 게시 transaction의 source lock 보장도 바뀌지 않았습니다.
    • verify-source 실패(nonzero 또는 비어 있지 않은 출력)는 publish 없이 attempt 실패로 처리하며, coordinator는 다음 job으로 넘어갑니다.

    원본 권한 code

    • RUNTIME_SOURCE_PERMISSION_DENIED(재시도 불가): seal의 복사 전 원본 검사와 verify-source의 원본 재확인에서 원본 읽기가 거부된 경우입니다. 문구는 "원본 게임 폴더를 읽을 권한이 없습니다. 관리자가 원본 접근 권한(stager 계정·NAS ACL)을 확인해야 합니다."입니다.
    • RUNTIME_STORE_PERMISSION_DENIED의 문구에서 "원본 또는"을 뺐습니다. 이제 이 code는 저장소 거부만 뜻합니다.
    • MV/MZ 원본 거부는 기존대로 BOUNDARY_REJECTED입니다.

    문서

    • 운영 가이드(1.0.3 → 1.0.4 갱신, Stager 계정과 저장소 권한, 원본 ACL), 런타임 계약, API 계약, 보안 정책, 완료 기준선, 개발 계획(NAS gate에 verify-source 추가), tests/README를 갱신했습니다.

    알려진 제한(후속 과제)

    • 원본 재확인부터 게시까지의 간격이 늘었습니다. 늘어난 부분은 publish one-shot 시작, release 검증, 봉인 사본 검증입니다. 128 MiB 합성 원본에서 약 1.2초가 늘었고, 봉인 사본 검증은 게임 크기에 비례합니다. 그 사이 원본 변경은 이전과 같이 게시 transaction의 DB source lock과 scan 관찰에 맡깁니다.
    • 실제 NAS(Synology ACL·btrfs)에서의 VX/VX Ace package 실행과 PowerShell coordinator 전체 실행은 NOT RUN입니다.
    • 1.0.3 배포 때 Compose 2.20.1에서 관찰된 사례가 있습니다. 중단된 up이 network 없는 container를 남길 수 있는데, update.sh는 이를 감지하지 않습니다. 운영자는 docker compose --env-file .env.production --profile runtime up -d --no-deps --force-recreate <service>로 복구합니다.
    • 1.0.3과 같은 제한:
      • /artifacts 쓰기 거부는 기존 분류를 따릅니다.
      • load 뒤에 권한이 바뀌면 ARTIFACT_INVALID로 끝납니다.
      • 소유 계정이 섞인 sealed store는 지원하지 않습니다.
      • Docker 24 graphdriver에서는 Builder config digest를 pin과 직접 대조하지 않습니다(ARCA 후속).
      • 3단계 이후 실패하면 coordinator가 정지된 채 남습니다. 원인을 고친 뒤 다음 sh update.sh가 다시 기동합니다.
      • Player 세이브 관리 listener(3005)는 catalog caller token으로만 보호되며, edge network에서 TCP로 닿을 수 있습니다.
      • catalog·Player의 일반 egress는 network로 제한하지 않습니다.
      • update.sh는 opt-in profile container(translation-import 등)를 갱신하지 않습니다.

    검증과 남은 범위

    • PASS: pkg-verify 브랜치(병합 c217f0d)
      • unit: 전체 149 files / 1,190 tests, 관련 5 files / 119 tests
      • schema 32 files / 228 tests, static, runtime-workers, reset 기록기 30·update.sh 기록기 93
      • Compose config 3종, PowerShell coordinator parse·sh -n, 문서 검사, git diff --check
      • 격리 dind e2e(Compose 2.29.1): stager 1026:100과 1026 전용 원본에서 수정 전 publishing RUNTIME_STORE_PERMISSION_DENIED 재현 → 수정 뒤 build ready. 기본 10001:10001도 ready. 원본을 못 읽는 오구성은 RUNTIME_SOURCE_PERMISSION_DENIED
      • 리뷰 최종 판정: 병합 가능(P1~P3 없음)
    • PASS: 릴리스 gate(Compose v2.20.1, NAS 조건)
      • 환경: 격리 docker:24.0.2-dind(overlay2, BusyBox ash), host Compose는 공식 v2.20.1 linux-x86_64 binary(release의 게시 checksum과 일치). 합성 CA의 HTTPS release 서버가 Gitea를, 합성 TLS registry가 git.2ex.dev를 대신했습니다(dind daemon에만 적용). registry에는 실제 1.0.3 image와 이 릴리스의 1.0.4 image를 넣었고, 배포 파일은 1.0.3 tag와 이 릴리스 커밋의 Git blob입니다.
      • 운영과 같은 계정과 원본 권한: scanner·stager 1026:100, ./data bind store, 원본 library 최상위 0:0 000 + ACL u:1026:r-x. 10001의 find /library ! -readable은 /library 1줄, 1026은 0줄
      • 1.0.3 설치 → reset.sh → update.sh: exit 0(106초), Updated to 1.0.3 .... MV/MZ 준비 성공. VX Ace 준비는 seal·stage·Builder 뒤 publishing에서 RUNTIME_STORE_PERMISSION_DENIED(cause EACCES, 재시도 불가)로 실패. 운영 로그와 같습니다
      • 1.0.4 게시(registry latest = 1.0.4, asset 없음) 뒤 설치된 1.0.3 update.sh: exit 0(47초), coordinator 정지 → pull → 전체 갱신·healthy → coordinator 재기동 → Updated to 1.0.4 .... 7개 파일이 release와 같고 .tmp는 0. package worker의 렌더링 mount에 /library가 없습니다
      • 실패한 VX Ace 준비를 관리자 빌드 재생성 경로(force 요청)로 다시 요청: seal(runtime-package-stager, 1026:100) → stage → Builder → verify-source(runtime-package-stager, 1026:100) → publish → build ready. 모든 one-shot이 exit 0, attempt 1에 성공. sealed 항목은 모두 1026:100이고 Builder 계정의 읽기·탐색 실패는 0
      • 설치 상태에서 재-update: exit 0(13초), 배포 파일 변화 없음, 준비 상태 ready 유지
      • 매 배포 뒤 결과: 12개 service의 network가 선언과 일치(불일치 0), unhealthy 0, package·runtime readiness exit 0, coordinator 재시작 0
    • PASS: 최종 1.0.4 소스. 릴리스 커밋 두 개(root version, 완료 기준선의 version 문구) 뒤 다음을 다시 통과했습니다: 문서 검사(760개 파일 / Markdown 32개), static(Prettier·ESLint·typecheck·docs-static), git diff --check.
    • 게시 이미지: 같은 최종 소스에서 build했고, gate도 이 image로 실행했습니다. image 안에서 root version 1.0.4와 ARCA pin·설치본 0.1.18을 확인했습니다.
    • PARTIAL: 전체 UI/제품 acceptance. 화면·fixture·package/build 성공을 실제 browser play PASS로 기록하지 않습니다.
    • NOT RUN:
      • Synology NAS 실제 환경(Compose v2.20.1 Synology build, Synology ACL, Docker 24 btrfs)에서의 1.0.3 → 1.0.4 갱신과 VX/VX Ace package
      • PowerShell coordinator 전체 실행
      • 실제 Gitea release·registry를 대상으로 한 전체 stack 갱신(gate는 합성 release 서버·registry)
      • 실제 library/private game과 실제 scan
      • 입력·오디오·save/reload의 실제 게임 acceptance
      • live provider
      • 개발 rebuild

    배포 이미지와 파일

    • Git annotated tag는 1.0.4이고 최종 소스는 f9171bfa75입니다.
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:1.0.4 = git.2ex.dev/hanyul/links:latest
    • 원격 digest: sha256:fd371daaf7427f4a725ab35a31e8214e6cd0114a3e9049c0a5b2af5be034ea09
    • release asset은 없습니다. update.sh가 tag 1.0.4의 7개 배포 파일을 HTTPS로 받습니다. 대응은 update.sh=ops/deploy-registry.sh, reset.sh=ops/reset-registry.sh, backup.sh=ops/backup-registry.sh이고, 나머지는 compose.yaml·infra/docker/Caddyfile·ops/backup-verify.compose.yaml·ops/runtime-package-coordinator.sh입니다.
    • 1.0.3 대비 바뀐 배포 파일은 두 개입니다. compose.yaml(package worker의 원본 library mount 제거)과 ops/runtime-package-coordinator.sh(publish 직전 verify-source 단계)입니다. 나머지 5개는 같습니다.
    Downloads 2
  • LINKS 1.0.3 — VX/VX Ace stager 계정 봉인·package 실패 원인 노출·SHA-256 없는 update.sh

    LINKS 1.0.3

    이 patch 릴리스는 이전 릴리스 1.0.2(4c4a5d3715)부터 최종 소스 5e262a8dde까지를 묶었습니다. 루트 패키지는 1.0.3이고 ARCA pin은 exact 0.1.18로 그대로입니다. 주요 변경은 세 가지입니다. VX/VX Ace 원본 봉인을 stager 계정 one-shot으로 바꿔 LINKS_RUNTIME_STAGER_UID/GID가 10001이 아닌 NAS에서도 package가 동작합니다. package·저장소 권한 실패가 구체 code와 원인 code로 표시됩니다. update.sh는 배포 파일 SHA-256 대조 없이 release tag의 파일을 HTTPS로 받습니다. schema는 바뀌지 않았으므로 DB 초기화와 migration이 필요 없습니다.

    업그레이드 주의: 1.0.2 → 1.0.3은 update.sh를 한 번만 직접 받습니다(초기화 없음)

    1. 1회 전환: 1.0.2의 update.sh는 release asset deploy-files.sha256을 요구합니다. 1.0.3부터는 이 asset이 없으므로, 설치된 1.0.2 update.sh를 그대로 실행하면 아무것도 바꾸지 않고 Release 1.0.3 does not support automatic updates; follow the manual procedure in the operations guide. 한 줄로 멈춥니다. 배포 디렉터리에서 아래 명령으로 새 update.sh를 한 번 받아 실행합니다. 이후 갱신은 sh update.sh 한 줄입니다. 1.0.2의 coordinator는 이미 Compose service이므로 host coordinator 종료 같은 추가 작업은 없습니다.
    2. schema 변경 없음, backup 선택: 기준 schema(infra/migrations/base.sha256)는 1.0.2와 같고 migration은 0개입니다. reset.sh를 실행하지 않습니다. 이번 갱신 전 backup(sh backup.sh)은 선택입니다.
    3. VX/VX Ace 원본 봉인 계정: 원본 봉인(seal)이 package worker(10001)가 아니라 stager 계정(LINKS_RUNTIME_STAGER_UID:GID)의 one-shot Compose service runtime-package-stager에서 실행됩니다. 그래서 stager 계정이 10001이 아닌 NAS(예: 1026:100)에서도 VX/VX Ace 준비가 동작합니다. 운영자 추가 작업은 없습니다. 새 service는 package worker와 같은 image를 쓰고 update.sh가 설치하는 compose.yaml·coordinator script에 들어 있습니다. 이미 stager 계정 소유인 sealed store와 MV/MZ 입력을 그대로 씁니다. 단 sealed store 전체가 stager 계정 소유여야 합니다(혼재 소유는 지원하지 않습니다). 1.0.2에서 실패로 끝난 VX/VX Ace 준비(RUNTIME_PREPARATION_FAILED, MV/MZ 준비가 먼저 있던 store에서는 SEALED_INPUT_INVALID로도 끝납니다)는 화면의 다시 시도로 다시 요청합니다. 관리자는 빌드 재생성도 쓸 수 있습니다.
    4. 구체 실패 code: package·저장소 권한 실패가 화면과 버그 신고에 구체 code(RUNTIME_STORE_PERMISSION_DENIED 등)와 경로 없는 원인 code(EACCES·EPERM·EROFS 등)로 표시됩니다. 이전에는 RUNTIME_PREPARATION_FAILED나 ARCA_PACKAGE_FAILED로 뭉뚱그려졌습니다.
    5. 배포 파일 SHA-256 대조 제거: update.sh는 더 이상 deploy-files.sha256 asset을 받거나 배포 파일 hash를 대조하지 않습니다. 배포 파일의 무결성은 HTTPS 전송(최초 URL과 redirect 모두 HTTPS만)과 Git release tag에 맡깁니다. 서명 인증은 없습니다. 1.0.3 릴리스에는 asset이 없습니다. 1.0.2의 기존 asset은 1.0.2 update.sh가 쓰므로 그대로 둡니다.
    6. NAS host Compose 2.20.1: 릴리스 전에 격리 docker:24.0.2-dind의 host Compose를 공식 Docker Compose v2.20.1 binary(release의 게시 checksum으로 확인)로 바꿔 이 전환을 검증했습니다. 1.0.2 설치 → 1.0.2 update.sh의 무변경 중단 → 1회 curl 전환 → VX Ace 준비 → 재-update까지 모두 성공했습니다(아래 검증). Synology build(v2.20.1-6047-g6817716)의 자체 패치와 공식 binary는 다를 수 있으므로 실제 NAS 결과는 확인 전입니다.

    NAS 절차(1.0.2에서 한 번):

    cd /volume1/docker/links
    curl -fsSL https://git.2ex.dev/hanyul/LINKS/raw/tag/1.0.3/ops/deploy-registry.sh -o update.sh
    sh update.sh
    

    성공하면 단계 표시 6줄 뒤에 Updated to 1.0.3 (catalog 6420862bb806), runtime.package ready.가 출력됩니다. 실패하면 원인 한 줄을 출력하고 nonzero로 끝납니다. 이후 갱신은 cd /volume1/docker/links && sh update.sh입니다.

    원본 game library는 읽기 전용으로 유지합니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    VX/VX Ace 원본 봉인의 stager 계정 분리

    • coordinator의 한 attempt는 이제 seal → stage → Builder → publish 순서입니다. seal은 새 one-shot service runtime-package-stager가 실행합니다. 계정은 LINKS_RUNTIME_STAGER_UID:GID이고 group_add 10001, network는 backend입니다. 원본 library는 read-only로, sealed store는 쓰기로 mount하며 package·artifact·RTP 저장소는 mount하지 않습니다.
    • stage를 실행하는 package worker(10001)는 sealed store를 read-only로 mount합니다. sealed store를 쓰는 계정은 MV/MZ의 runtime-stager와 같은 stager 계정 하나입니다.
    • 봉인된 파일은 0444, 디렉터리는 0555이며 Builder 계정(10001)이 읽고 탐색할 수 있습니다.

    package 실패 원인 노출

    • sealed store 자체의 파일 작업이 단계 계정에 거부되면 준비는 RUNTIME_STORE_PERMISSION_DENIED(재시도 불가)로 끝납니다. 버그 신고 진단에는 경로 없는 cause(EACCES·EPERM·EROFS)가 남습니다. root·.staging·하위 디렉터리, 봉인 metadata·payload 파일 읽기 거부가 모두 이 code로 분류됩니다.
    • package 실패는 원본·staging·공간 원인을 ARCA_PACKAGE_FAILED로 덮지 않고 원래 code로 기록합니다. 작업 기록의 error_detail은 경로 없는 형식을 유지합니다.

    SHA-256 없는 update.sh

    • 1단계는 최신 release tag의 ops/deploy-registry.sh를 먼저 받습니다. 이 파일이 없거나(HTTP 404) 자동 갱신 표식 줄 # links-auto-update: 2가 없으면(1.0.2 이하) 나머지를 받지 않고 자동 갱신 미지원으로 멈춥니다.
    • 7개 파일은 각각 curl이 성공하고 비어 있지 않아야 합니다. 하나라도 받지 못하거나 비어 있으면 받은 파일을 모두 지우고, 기존 파일과 container를 바꾸지 않은 채 멈춥니다. 내용이 같은 파일(cmp)은 교체하지 않습니다.
    • 이전 실행이 남긴 .tmp(1.0.2의 deploy-files.sha256.tmp 포함)는 모든 모드에서 시작할 때 지웁니다.
    • 자동 갱신 미지원 release(1.0.2 이하)를 설치하거나 그 release로 되돌릴 때의 수동 절차는 SHA-256 대조 없이 7개 파일을 모두 받은 뒤에만 교체합니다.

    문서

    • 운영 가이드: 1.0.2에서의 전환, update.sh 1단계·실패 메시지, release 절차(asset 없음), 수동 절차, Stager 계정과 저장소 권한, RUNTIME_STORE_PERMISSION_DENIED 복구.
    • AGENTS.md(배포 파일 SHA-256 금지), API 계약, 보안 정책, 아키텍처, 런타임 계약, 완료 기준선, 개발 계획, tests/README, README도 갱신했습니다.

    알려진 제한(후속 과제)

    • /artifacts 쓰기 거부는 이번에 바꾸지 않았고 기존 분류를 따릅니다.
    • load 뒤에 권한이 바뀌면 ARTIFACT_INVALID로 끝납니다.
    • 소유 계정이 섞인 sealed store는 지원하지 않습니다. sealed store 전체가 stager 계정 소유여야 합니다.
    • 실제 NAS에서의 실행은 NOT RUN입니다.
    • 1.0.2와 같은 제한:
      • Docker 24 graphdriver에서는 Builder config digest를 pin과 직접 대조하지 않습니다(ARCA 후속).
      • 3단계 이후 실패하면 coordinator는 정지된 채 남습니다. 원인을 고친 뒤 다음 sh update.sh가 다시 기동합니다.
      • Player 세이브 관리 listener(3005)는 catalog caller token으로만 보호되며 edge network에서 TCP로 닿을 수 있습니다.
      • catalog·Player의 일반 egress를 network가 제한하지 않습니다.
      • update.sh는 opt-in profile container(translation-import 등)를 갱신하지 않습니다. 갱신 뒤 직접 다시 기동합니다.

    검증과 남은 범위

    • PASS: drop-deploy-sha256 브랜치(병합 6888a56)
      • update.sh 기록기 93개 PASS(수정 전 script로는 64개 FAIL), 수동 절차 블록의 부분 파일·빈 파일·전체 성공 사례(dash·BusyBox)
      • 문서 검사, git diff --check
      • HTTPS dind e2e(Docker 24.0.2): asset 없는 release의 최초 설치·자기 교체, reset, 배포, 표식 없는 release의 무변경 중단, 1.0.2 update.sh의 무변경 중단 뒤 1회 curl 전환
      • 리뷰 최종 판정: 병합 가능
    • PASS: pkg-uid 브랜치(병합 6cf8f1f)
      • 전체 unit 149 files / 1,167 tests, static, 문서 검사
      • 격리 dind e2e(stager 1026:100): 수정 전 3 attempt 실패 재현 → 수정 뒤 seal → stage → Builder → publish → build ready. RGSS·MV/MZ 봉인 사본 읽기 거부의 RED → GREEN
      • 리뷰 최종 판정: 병합 가능
    • PASS: 릴리스 gate(Compose v2.20.1)
      • 환경: 격리 docker:24.0.2-dind(overlay2, BusyBox ash), host Compose는 공식 v2.20.1 linux-x86_64 binary(release의 게시 checksum과 일치)입니다. 합성 CA의 HTTPS release 서버가 Gitea를, 합성 TLS registry가 git.2ex.dev를 대신했습니다(dind daemon에만 적용). registry에는 실제 1.0.2 image와 이 릴리스의 1.0.3 image를 넣었습니다. 배포 파일은 1.0.2 tag와 이 릴리스 커밋의 Git blob입니다.
      • 운영과 같은 계정: scanner·stager 1026:100, ./data bind store
      • 1.0.2 최초 설치 → reset.sh → update.sh: exit 0(107초). sealed store 1026:100, MV/MZ 준비 성공. VX Ace 준비는 1.0.2 결함으로 3 attempt 모두 실패(SEALED_INPUT_INVALID)
      • 1.0.3 게시 뒤 설치된 1.0.2 update.sh: 자동 갱신 미지원 줄, exit 1, 배포 파일·container 변화 없음
      • 운영 가이드의 1회 curl → sh update.sh: exit 0(47초), Updated to 1.0.3 ..., 7개 파일이 release와 같음, .tmp 0
      • 1.0.2에서 실패한 VX Ace 준비를 다시 요청: runtime-package-stager one-shot(exit 0)이 1026:100으로 봉인 → stage → Builder(exit 0) → publish → build ready. sealed 항목은 모두 1026:100이고 Builder 계정의 읽기·탐색 실패는 0
      • 설치 상태에서 재-update: exit 0(13초), 배포 파일 변화 없음
      • 매 배포 뒤 12개 service의 network가 선언과 일치(불일치 0)했고 unhealthy는 0, package·runtime readiness는 exit 0, coordinator 재시작 0이었습니다.
    • PASS: 최종 1.0.3 소스. 릴리스 커밋 두 개(root version, 완료 기준선의 version 문구) 뒤 문서 검사(759개 파일 / Markdown 32개), static(Prettier·ESLint·typecheck·docs-static), git diff --check를 다시 통과했습니다.
    • 게시 이미지: 같은 최종 소스에서 build했습니다. image 안에서 root version 1.0.3, ARCA pin·설치본 0.1.18을 확인했습니다.
    • PARTIAL: 전체 UI/제품 acceptance. 화면·fixture·package/build 성공을 실제 browser play PASS로 기록하지 않습니다.
    • NOT RUN:
      • Synology NAS 실제 환경(Compose v2.20.1 Synology build, Docker 24 btrfs)에서의 1.0.2 → 1.0.3 전환과 sh update.sh
      • 실제 Gitea release·registry에서의 전체 stack 갱신(gate는 합성 release 서버·registry)
      • 실제 library/private game과 실제 scan
      • 입력·오디오·save/reload의 실제 게임 acceptance
      • live provider
      • 개발 rebuild

    배포 이미지와 파일

    • Git annotated tag는 1.0.3이고 최종 소스는 5e262a8dde입니다.
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:1.0.3 = git.2ex.dev/hanyul/links:latest
    • 원격 digest: sha256:6420862bb806cd543dd5e54c38e8ac0d9fe2eb5c183daf4e8494e17265a5cf46
    • release asset은 없습니다. update.sh가 tag 1.0.3의 7개 배포 파일을 HTTPS로 받습니다(update.sh=ops/deploy-registry.sh, reset.sh=ops/reset-registry.sh, backup.sh=ops/backup-registry.sh, 그리고 compose.yaml·infra/docker/Caddyfile·ops/backup-verify.compose.yaml·ops/runtime-package-coordinator.sh).
    • 1.0.2 대비 바뀐 배포 파일: compose.yaml(runtime-package-stager service, package worker의 sealed store read-only), ops/deploy-registry.sh(SHA-256 없는 다운로드·자동 갱신 표식), ops/runtime-package-coordinator.sh(seal 단계). 나머지 4개는 같습니다.
    Downloads 2
  • LINKS 1.0.2 — sh update.sh 한 명령 배포·coordinator Compose service

    LINKS 1.0.2

    이 patch 릴리스는 이전 릴리스 1.0.1(9cdd56ca4e)부터 최종 소스 4c4a5d3715까지를 묶었습니다. 루트 패키지는 1.0.2이고 ARCA pin은 exact 0.1.18로 그대로입니다. NAS 갱신은 이제 sh update.sh 한 명령입니다. 이 명령이 최신 release를 찾고, 배포 파일을 받아 대조하고, image 버전을 확인합니다. package coordinator의 정지·기동과 readiness 확인도 함께 합니다. package coordinator는 Compose service가 되어 재부팅 뒤 자동으로 기동합니다. schema는 바뀌지 않았으므로 DB 초기화와 migration이 필요 없습니다.

    업그레이드 주의: 1.0.1 → 1.0.2는 한 번만 수동으로 전환합니다(초기화 없음)

    1. 1회 수동 전환: 1.0.1의 update.sh는 자동 갱신을 모르므로 이번에만 아래 순서를 따릅니다.
      1. 실행 중인 host coordinator(sh ops/runtime-package-coordinator.sh …)를 종료합니다. foreground면 그 터미널에서 Ctrl+C를 누르고, background면 ps로 PID를 찾아 kill <PID>를 실행합니다.
      2. .runtime-package-coordinator.lock 파일은 지우지 않습니다. lock은 프로세스가 끝나면 풀립니다. coordinator가 아직 lock을 잡고 있으면 새 update.sh가 아무것도 바꾸지 않고 A host package coordinator is running; ...으로 멈춥니다.
      3. Task Scheduler 작업을 만들었다면 중지하고 삭제합니다. 작업을 삭제해도 이미 실행 중인 프로세스는 끝나지 않습니다.
      4. 새 update.sh를 받아 실행합니다(아래 명령). 이후 갱신은 sh update.sh 한 줄입니다.
    2. coordinator Compose service: coordinator는 Compose service runtime-package-coordinator로 실행됩니다. 구성은 docker:24.0.9-cli, network none, 재시작 정책 unless-stopped이며 배포 디렉터리를 read-only로 mount합니다. Docker socket을 mount하므로 이 service는 host root와 같은 권한을 가집니다. NAS 재부팅 뒤에는 Docker가 자동으로 기동하므로 Task Scheduler 작업이 필요 없습니다. 로그는 docker compose --env-file .env.production logs runtime-package-coordinator로 봅니다.
    3. shell override 거부: update.sh와 reset.sh는 Compose 값(compose.yaml이 interpolation하는 변수와 COMPOSE_PROJECT_NAME·COMPOSE_FILE·COMPOSE_PROFILES·COMPOSE_ENV_FILES)이 shell에 설정돼 있으면 빈 값이어도 아무것도 바꾸지 않고 Shell override is not supported: <NAME>. Set it in .env.production.으로 멈춥니다. 모든 값은 .env.production에 literal 값으로 둡니다. ${HOME} 같은 host shell 참조는 쓰지 않습니다. host와 coordinator container의 환경이 달라 서로 다른 값으로 풀리기 때문입니다.
    4. LINKS_REGISTRY_TAG: 평소에는 비우거나 latest로 둡니다. rollback 때만 <tag>@sha256:<digest> pin을 넣습니다. digest 없는 버전 tag는 update.sh가 거부합니다.
    5. HTTPS와 manifest: update.sh는 최초 URL과 redirect 모두 HTTPS만 사용합니다. release asset deploy-files.sha256과 7개 배포 파일의 SHA-256을 자동으로 대조하고, 하나라도 다르면 아무것도 바꾸지 않습니다. 이 대조는 무결성 확인이며 서명 인증이 아닙니다.
    6. NAS host Compose 2.20.1: NAS의 host Compose는 v2.20.1(Synology build)입니다. 릴리스 전에 공식 Docker Compose v2.20.1 binary(SHA-256 대조)로 바꾼 격리 docker:24.0.2-dind에서 이 릴리스의 배포 파일을 검증했습니다. reset → update, 재-update, container 재생성 update 세 시나리오 모두 성공했습니다. 12개 service의 network 연결은 선언과 모두 일치했고 package·runtime readiness는 종료 코드 0이었습니다. Synology build의 자체 패치와 공식 binary는 다를 수 있으므로 실제 NAS 결과는 확인 전입니다.
    7. DB 초기화 불필요: 기준 schema checksum은 14481d17c5947342be029bf6f850d2f862372b988dcbc1809360cdd7b333c918(infra/migrations/base.sha256)로 1.0.1과 같고 migration은 0개입니다. reset.sh를 실행하지 않습니다.

    NAS 절차(1.0.1에서 한 번):

    cd /volume1/docker/links
    # host coordinator를 Ctrl+C 또는 kill로 종료한 뒤(lock 파일 삭제 금지, Task Scheduler 작업이 있으면 삭제)
    curl -fL --proto '=https' https://git.2ex.dev/hanyul/LINKS/raw/tag/1.0.2/ops/deploy-registry.sh -o update.sh
    sh update.sh
    

    성공하면 단계 표시 6줄 뒤에 Updated to 1.0.2 (catalog f1c3cf7014ad), runtime.package ready.가 출력됩니다. 실패하면 원인 한 줄을 출력하고 nonzero로 끝납니다. 이후 갱신은 cd /volume1/docker/links && sh update.sh입니다.

    원본 game library는 읽기 전용으로 유지합니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    sh update.sh 한 명령 배포

    • 1단계 release 파일: Gitea releases/latest에서 tag를 읽고 asset deploy-files.sha256을 받습니다. manifest는 정확히 7개 경로를 한 번씩 담아야 하며, 각 줄은 64자리 hex와 두 칸 구분 형식이어야 합니다. 그다음 7개 파일을 <이름>.tmp로 받아 대조합니다. asset이 없는 release(1.0.1 이하)에서는 Release <tag> does not support automatic updates; ...로 멈춥니다.
    • 자기 교체: 받은 update.sh가 실행 중인 script와 다르면 대조를 마친 새 script를 한 번 실행하고, 이후 단계는 새 release의 규칙으로 진행합니다. 받은 파일은 5단계 직전에 설치합니다. 그 전의 실패와 처리 가능한 중단 signal(HUP·INT·TERM)은 받은 파일을 지우고 끝냅니다. 이전 실행이 남긴 .tmp는 시작할 때 지웁니다.
    • image 버전 확인: pull한 image의 root version이 release tag와 같아야 진행합니다. 다르면 The registry image is version <v>, not release <tag>; try again later.로 멈춥니다.
    • coordinator: pull 전에 coordinator를 정지합니다. 다른 service를 up --wait로 갱신하고 catalog의 package 기능을 확인한 뒤 coordinator를 단독으로 기동합니다. 이번 기동의 Package coordinator ready (...) 줄을 최대 5분 기다립니다. 같은 container·StartedAt·RestartCount=0인지 확인해 ready 뒤 재시작이나 종료를 감지하고, 마지막으로 전체 readiness를 확인합니다.
    • 최초 설치: compose.yaml이 없는 디렉터리에서는 파일만 설치하고 sh reset.sh → sh update.sh를 안내합니다.
    • 거부: 인자, digest 없는 LINKS_REGISTRY_TAG, 실행 중인 host coordinator(lock), HTTPS가 아닌 저장소 URL, shell override는 아무것도 바꾸기 전에 거부합니다.

    package coordinator Compose service

    • compose.yaml에 runtime-package-coordinator service를 추가했습니다. 구성은 docker:24.0.9-cli, init, read-only rootfs, cap_drop: ALL + DAC_READ_SEARCH, no-new-privileges, network none, /tmp tmpfs 256 MB, stop_grace_period 60초, restart: unless-stopped입니다.
    • coordinator는 자기 mount에서 배포 디렉터리의 host 경로를 읽어 Compose의 project directory로 씁니다. 그래서 상대 bind 경로와 project 이름이 host Compose와 같게 해석됩니다.
    • 시작 준비가 끝나면 Package coordinator ready (ARCA <version>, builder <image ID 앞 12자>), 대기에 들어가면 Waiting for package jobs.를 출력합니다. 정지 요청을 받으면 실행 중인 Builder container를 바로 정지합니다.
    • Docker socket mount는 보안 정책에 예외로 기록했습니다. 개발 Windows에서는 이 service를 쓰지 않고 기존 host coordinator를 유지합니다.

    shell override 거부와 정리

    • update.sh와 reset.sh가 같은 규칙으로 Compose 값의 shell override를 거부합니다. 값은 출력하지 않습니다.
    • update.sh는 이전 실행의 .tmp를 설치하지 않으며, 중단 signal을 받으면 받은 파일을 지우고 129·130·143으로 끝납니다.

    문서

    • 운영 가이드: NAS 배포 두 줄, update.sh 단계·실패 메시지·되돌리기, 최초 설치와 1.0.1에서의 전환, release asset 첨부 절차, coordinator service 운영.
    • 보안 정책(Docker socket 예외), 아키텍처, 런타임 계약, 완료 기준선, 개발 계획, tests/README, README도 갱신했습니다.

    알려진 제한(후속 과제)

    • Docker 24 graphdriver에서는 Builder config digest를 pin과 직접 대조하지 않습니다. 이를 강화하려면 ARCA가 config digest를 공개 API로 제공해야 합니다(ARCA 후속).
    • 3단계 이후 실패하면 coordinator는 정지된 채 남습니다. 원인을 고친 뒤 다음 sh update.sh가 다시 기동합니다.
    • 1.0과 같은 제한:
      • Player 세이브 관리 listener(3005)는 catalog caller token으로만 보호되며 edge network에서 TCP로 닿을 수 있습니다.
      • catalog·Player의 일반 egress를 network가 제한하지 않습니다.
      • update.sh는 opt-in profile container(translation-import 등)를 갱신하지 않습니다. 갱신 뒤 직접 다시 기동합니다.

    검증과 남은 범위

    • PASS: one-command-update 브랜치(병합 ede4c1b)에서 통과한 검사
      • update.sh 기록기 98/98, reset.sh 기록기 30/30, coordinator unit 31/31
      • 전체 unit 149 files / 1,135 tests, static, 문서 검사, Compose config 3개 조합, BusyBox sh -n, 격리 foundation
      • HTTPS dind e2e(Docker 24.0.2): 최초 설치·자기 교체, reset, 배포, HTTPS만 허용, ready 뒤 종료 감지, 재부팅
      • 리뷰 최종 판정: 병합 가능
    • PASS: 릴리스 gate(Compose v2.20.1)
      • 환경: 격리 docker:24.0.2-dind(overlay2, BusyBox ash). host Compose는 공식 v2.20.1 linux-x86_64 binary로 바꿨습니다(SHA-256 804aa825c58b375901ba9dc9d0aa1648ce305d903a58f2a5c42bb5c6873b02bf, release의 .sha256·checksums.txt와 일치).
      • 입력: 이 릴리스와 byte가 같은 7개 배포 파일을 합성 HTTPS release로 제공하고, 실제 registry image를 익명으로 pull했습니다.
      • 최초 설치(자기 교체) → reset.sh → update.sh: 118초, exit 0
      • 재-update: exit 0
      • container 재생성 update(catalog·player 포함 8개 재생성): exit 0
      • 매 시나리오 뒤 12개 service의 network가 선언과 일치(누락 0)했고, unhealthy는 0이었으며 package·runtime readiness는 exit 0이었습니다.
    • PASS: 최종 1.0.2 소스. 릴리스 커밋 두 개(root version, 완료 기준선의 version 문구) 뒤 다음을 다시 통과했습니다.
      • 문서 검사(756개 파일 / Markdown 32개)
      • static(Prettier·ESLint·typecheck·docs-static)
      • git diff --check
    • 게시 이미지: 같은 최종 소스에서 build했습니다. image 안에서 root version 1.0.2, ARCA pin·설치본 0.1.18, base.sha256을 확인했습니다.
    • PARTIAL: 전체 UI/제품 acceptance. 화면·fixture·package/build 성공을 실제 browser play PASS로 기록하지 않습니다.
    • NOT RUN:
      • Synology NAS 실제 환경(Compose v2.20.1 Synology build, Docker 24 btrfs)에서의 1.0.1 → 1.0.2 전환과 sh update.sh
      • Docker 24에서 Builder package 전체 실행
      • 실제 library/private game과 실제 scan
      • 입력·오디오·save/reload의 실제 게임 acceptance
      • live provider
      • 개발 rebuild

    배포 이미지와 파일

    • Git annotated tag는 1.0.2이고 최종 소스는 4c4a5d3715입니다.
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:1.0.2 = git.2ex.dev/hanyul/links:latest
    • 원격 digest: sha256:f1c3cf7014ad65aa00dd3895bc1d3c964c852ecbf4995420bb973b05fcebdcfb
    • release asset deploy-files.sha256(아래 7줄)을 첨부했습니다. update.sh가 이 asset으로 배포 파일을 자동 대조합니다.
    • 아래 SHA-256은 checkout의 CRLF 변환본이 아니라 1.0.2 태그의 Git blob 원본 byte로 계산했습니다.
    • NAS 배포 목록은 7개입니다(update.sh=ops/deploy-registry.sh, reset.sh=ops/reset-registry.sh, backup.sh=ops/backup-registry.sh).
    SHA-256 파일 1.0.1 대비
    c7b480fab3238bbefc008501bf2e65ab50b95c985dba2b8896f85af6e262caec compose.yaml 변경 (runtime-package-coordinator service)
    6e1fd3f2f73faee52abca28c8552cde417f496d86fc3d78c7001dec480e171a3 infra/docker/Caddyfile 동일
    39a4c55050e76043c8ca70f11f59dbdf08fff7ef38c1f1ec878f96194988d9d8 ops/backup-verify.compose.yaml 동일
    9671a00c1a13723cf16b0eced336241f25299f45bbed5ca4044af23f9d81ac72 ops/deploy-registry.sh 변경 (한 명령 자동 갱신)
    cf6169a8ffa577d40b70ea476e10b662fc898a68ebfc28b43c513cf9c1b7bf33 ops/reset-registry.sh 변경 (shell override 거부)
    89375a986b3cb64cf74145541701b32c6a0e983a72ca46be21a8bcfad7beaa3d ops/backup-registry.sh 동일
    6ba741a66e9744c0cd01ce8e3df81b94a55ae3f7d41b6f1d05200eedc5dccdb9 ops/runtime-package-coordinator.sh 변경 (Compose service 모드·상태 줄·Builder 즉시 정지)
    c7b480fab3238bbefc008501bf2e65ab50b95c985dba2b8896f85af6e262caec  compose.yaml
    6e1fd3f2f73faee52abca28c8552cde417f496d86fc3d78c7001dec480e171a3  infra/docker/Caddyfile
    39a4c55050e76043c8ca70f11f59dbdf08fff7ef38c1f1ec878f96194988d9d8  ops/backup-verify.compose.yaml
    9671a00c1a13723cf16b0eced336241f25299f45bbed5ca4044af23f9d81ac72  ops/deploy-registry.sh
    cf6169a8ffa577d40b70ea476e10b662fc898a68ebfc28b43c513cf9c1b7bf33  ops/reset-registry.sh
    89375a986b3cb64cf74145541701b32c6a0e983a72ca46be21a8bcfad7beaa3d  ops/backup-registry.sh
    6ba741a66e9744c0cd01ce8e3df81b94a55ae3f7d41b6f1d05200eedc5dccdb9  ops/runtime-package-coordinator.sh
    
    Downloads 3
  • LINKS 1.0.1 — ARCA 0.1.18·Docker 24 호환 Builder·byedpi latest

    LINKS 1.0.1

    이 patch 릴리스는 이전 릴리스 1.0(edac251454)부터 최종 소스 9cdd56ca4e까지를 묶었습니다. 루트 패키지는 1.0.1이고, ARCA pin은 exact 0.1.17 → 0.1.18으로 올렸습니다. ARCA 0.1.18의 Builder archive는 Docker 24와 호환되므로 Synology Docker 24.0.2에서도 VX/VX Ace package 기능을 쓸 수 있습니다. byedpi image는 digest pin 대신 :latest tag를 따릅니다. schema는 바뀌지 않았으므로 DB 초기화와 migration이 필요 없습니다.

    업그레이드 주의 — 1.0 → 1.0.1은 sh update.sh로 갱신합니다(초기화 없음)

    1. ARCA 0.1.18과 Builder image 참조: Builder archive는 Docker 24 호환 형식(OCI layout + manifest.json)입니다. coordinator는 daemon이 해석한 image ID로 Builder를 실행합니다. Docker 24 graphdriver에서는 config digest, Docker 25 이상의 containerd store에서는 manifest digest입니다. native runtime은 0.1.17과 같습니다. 다만 Builder digest가 바뀌었으므로 기존 RGSS active build는 다음 실행 준비 때 새 generation이 됩니다. 0.1.17 pin 때와 같은 규칙이며, 실행 중 session을 끝내거나 save를 지우지 않습니다. coordinator script(ops/runtime-package-coordinator.sh)·compose.yaml·image는 같은 release로 함께 교체합니다. 0.1.17 image에 새 script를 쓰면 Docker 24의 load 단계에서 실패합니다.
    2. coordinator 운영 전제:
      • 같은 daemon에서 rgss23-builder:<version> tag를 바꾸는 작업이 load 시작부터 image ID 취득까지의 구간과 겹치면 안 됩니다. 다른 디렉터리의 coordinator나 수동 docker load·docker tag도 여기에 해당합니다.
      • Docker 24는 coordinator 시작 때와 job마다 Builder archive(약 62 MB)를 TMPDIR(기본 /tmp)을 거쳐 다시 load합니다. TMPDIR은 배포 디렉터리 밖의 쓰기 가능한 절대 경로여야 합니다. tmpfs라면 archive와 동시 사용분을 담을 용량이 있는지 확인합니다.
      • load 단계가 실패하면 sh coordinator가 실패를 보존합니다. 시작 단계에서는 종료하고, claim 뒤에는 attempt 실패로 기록합니다. 실패 단계마다 진단 한 줄을 stderr에 남깁니다.
    3. byedpi :latest: byedpi service는 git.2ex.dev/hanyul/2ex-hentai-byedpi:latest(pull_policy: always)를 씁니다. update.sh의 compose pull과 개발 기동은 매번 최신 image를 받습니다. registry에 접속할 수 없으면 provider-live profile 기동이 실패합니다. 1.0의 compose.yaml은 registry에서 사라진 digest를 가리키므로, byedpi image가 없는 host에서는 pull이 실패합니다. 1.0.1의 compose.yaml로 교체합니다.
    4. DB 초기화 불필요: 기준 schema checksum은 14481d17c5947342be029bf6f850d2f862372b988dcbc1809360cdd7b333c918(infra/migrations/base.sha256)로 1.0과 같고 migration은 0개입니다. reset.sh를 실행하지 않습니다. 1.0 backup은 1.0.1 image로 복구할 수 있습니다.

    NAS 절차:

    1. package coordinator(Task Scheduler 작업)를 정상 정지합니다. docker ps --filter name=links-arca-에 남은 Builder container가 있으면 정지합니다.
    2. 1.0.1 태그의 배포 파일 7개를 받아 아래 SHA-256과 모두 같을 때만 교체합니다. 운영 가이드 운영 배포의 받기·대조 블록에 release_version='1.0.1'을 넣어 씁니다.
    3. sh update.sh
    4. coordinator를 시작합니다.
    5. update.sh가 마지막 줄에 출력한 --package readiness 명령을 실행합니다. 종료 코드가 0이어야 배포가 완료된 것입니다.
    cd /volume1/docker/links
    sh update.sh
    # coordinator 시작 뒤
    docker compose --env-file '.env.production' exec -T catalog node apps/worker/dist/runtime-readiness-command.js --package
    

    원본 game library는 읽기 전용으로 유지합니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    ARCA 0.1.18과 Docker 24 호환 Builder image 참조

    • @rgss23/arca를 exact 0.1.18로 올렸습니다(package.json·pnpm-lock.yaml·pnpm-workspace.yaml minimumReleaseAgeExclude). lockfile integrity는 registry, ARCA 게시 identity, 로컬 tarball과 일치합니다. SDK module은 0.1.17과 같습니다.

    • coordinator(ops/runtime-package-coordinator.sh·.ps1)는 Builder image를 다음 순서로 정합니다.

      1. release의 manifest digest가 daemon에서 해석되면 load 없이 그 값을 씁니다.
      2. 해석되지 않으면 검증된 archive를 다시 load합니다.
      3. load 뒤에는 package worker image의 exact pin으로 붙은 rgss23-builder:<version> tag의 image ID를 씁니다.

      Builder 실행에는 이렇게 정한 ID를 넘깁니다. pin이 정확한 숫자.숫자.숫자가 아니면 거부합니다. worker 출력 형식과 claim 4필드는 바뀌지 않았습니다.

    • Builder 검사 runner(tests/run-rgss23-builder.ps1·run-rgss23-builder-input.ps1)도 같은 image ID 규칙을 따릅니다. 0.1.18 archive의 load 출력에는 digest가 없기 때문입니다.

    • sh coordinator가 Builder 선택 단계의 one-shot 실패를 보존합니다. pin 조회 Compose 실패, archive reader 실패, load 실패가 있으면 종료하거나 attempt 실패로 기록합니다. archive는 mktemp -d 임시 디렉터리를 거쳐 load하고 그 디렉터리는 즉시 지웁니다.

    byedpi image :latest

    • compose.yaml의 byedpi는 digest pin(registry에서 삭제됨) 대신 :latest tag와 pull_policy: always를 씁니다. 2ex-hentai registry는 최신 image만 보관합니다. 보안 정책의 base image digest pin 규칙에 byedpi 예외를 적었습니다.

    문서

    • Docker 24 host의 Builder archive load 불가를 BLOCKED 항목으로 기록했고, ARCA 0.1.18 반영으로 해제했습니다. NAS의 실제 Docker 24 확인은 개발 계획의 완료 gate로 옮겼습니다.
    • 런타임 계약과 운영 가이드에 다음을 적었습니다.
      • Builder image 참조 규칙
      • coordinator 운영 전제(tag 변경 배제, TMPDIR 용량, 실패 진단)
      • stage handoff의 보장 범위: claim과 stage가 같은 release인지만 검사합니다.
    • 완료 기준선의 dind 판정 수치를 정정했습니다(판정 그룹 10개 PASS, failed assertions 0).

    알려진 제한(후속 과제)

    • Docker 24 graphdriver에서는 Builder config digest를 pin과 직접 대조하지 않습니다. tag에서 고른 image가 그 release라는 점은 위 운영 전제로 보장합니다. 이를 강화하려면 ARCA가 config digest를 공개 API로 제공해야 합니다(개발 계획의 비차단 후속).
    • 1.0과 같은 제한:
      • Player 세이브 관리 listener(3005)는 catalog caller token으로만 보호되며 edge network에서 TCP로 닿을 수 있습니다.
      • catalog·Player의 일반 egress를 network가 제한하지 않습니다.
      • update.sh는 opt-in profile container(translation-import 등)를 갱신하지 않습니다. 갱신 뒤 직접 다시 기동해야 합니다.

    검증과 남은 범위

    • PASS — arca-0118 브랜치(병합 8529256)에서 통과한 gate:
      • 빌드·정적 검사: cache 없는 test image build, lockfile 재해석, static, unit 149 files / 1,122 tests
      • ARCA 결속: arca-npm-check, release-set, runtime-binding
      • Builder: builder-input, builder(합성 VX Ace, 원본 save 제외), Builder 격리
      • worker·harness: runtime-workers, PowerShell coordinator harness 11/11
      • browser: 합성 0.1.18 SDK sandbox-rgss, player-restart Chromium·Firefox, run-browser mz·mv × Chromium·Firefox
      • 문서 검사
      • dind: Docker 24.0.2(overlay2)·29(containerd)에서 sh coordinator와 실제 compose.yaml Builder inspect의 판정 그룹 10개가 PASS했습니다(각 daemon, failed assertions 0).
    • PASS — byedpi-pin 브랜치(병합 73d998a): compose config 3개 조합, 격리 project의 pull·기동·1080 접속, 문서 검사.
    • PASS — docs-arca-docker24(dca1f00)·docs-dind-count(c975897): 문서 검사, git diff --check.
    • PASS — 최종 1.0.1 소스: 릴리스 커밋 두 개(root version, 완료 기준선 version 문구) 뒤 다음을 다시 통과했습니다.
      • 문서 검사(756개 파일 / Markdown 32개)
      • static(Prettier·ESLint·typecheck·docs-static)
      • git diff --check
    • 게시 이미지: 같은 최종 소스에서 build했습니다. image 안에서 root version 1.0.1, ARCA pin·설치본 0.1.18, base.sha256을 확인했습니다.
    • PARTIAL — 전체 UI/제품 acceptance: 화면·fixture·package/build 성공을 실제 browser play PASS로 기록하지 않습니다.
    • NOT RUN:
      • Synology NAS 실제 Docker 24(btrfs)에서 coordinator --once·load·package·--package readiness. dind는 대리 검증입니다.
      • Docker 24에서 Builder package 전체 실행(dind는 inspect까지)
      • Docker 24 재load 시간 측정
      • 동시 retag 경쟁 재현
      • byedpi를 거친 실제 provider 요청과 NAS pull
      • 실제 library/private game과 실제 scan
      • 입력·오디오·save/reload의 실제 게임 acceptance
      • 실제 모바일 기기·보조 기술
      • live provider
      • 개발 rebuild

    배포 이미지와 파일

    • Git annotated tag는 1.0.1이고 최종 소스는 9cdd56ca4e입니다.
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:1.0.1 = git.2ex.dev/hanyul/links:latest
    • 원격 digest: sha256:5f76bc224504ad4c46c2146645849ec247ae2b1e8a19b0aec7a311e773034467
    • 아래 SHA-256은 checkout의 CRLF 변환본이 아니라 1.0.1 태그의 Git blob 원본 byte로 계산했습니다.
    • NAS 배포 목록은 7개입니다(update.sh=ops/deploy-registry.sh, reset.sh=ops/reset-registry.sh, backup.sh=ops/backup-registry.sh).
    • 1.0 대비 바뀐 파일은 **compose.yaml**과 ops/runtime-package-coordinator.sh 두 개입니다. 둘을 image와 함께 반드시 교체합니다.
    SHA-256 파일 1.0 대비
    4ea38db6094a01d77d80b93b6048f47377dd0354a1e8e2ec7b8008b5c233c042 compose.yaml 변경 (byedpi :latest·pull_policy: always)
    6e1fd3f2f73faee52abca28c8552cde417f496d86fc3d78c7001dec480e171a3 infra/docker/Caddyfile 동일
    39a4c55050e76043c8ca70f11f59dbdf08fff7ef38c1f1ec878f96194988d9d8 ops/backup-verify.compose.yaml 동일
    e516bea3688ad135556105772b5b8d603795bb04c7d7ce6ca760cbcf15714d52 ops/deploy-registry.sh 동일
    88d7b26d510ee67a25760dae8a9611be25592ea651a8d9870afaf9e714f37c54 ops/reset-registry.sh 동일
    89375a986b3cb64cf74145541701b32c6a0e983a72ca46be21a8bcfad7beaa3d ops/backup-registry.sh 동일
    50d3bb374dde35464f8e2d2c07a7dda95771149e7f2e8727bf03237e444ce8ab ops/runtime-package-coordinator.sh 변경 (daemon이 해석한 Builder image ID, one-shot 실패 보존)
    4ea38db6094a01d77d80b93b6048f47377dd0354a1e8e2ec7b8008b5c233c042  compose.yaml
    6e1fd3f2f73faee52abca28c8552cde417f496d86fc3d78c7001dec480e171a3  infra/docker/Caddyfile
    39a4c55050e76043c8ca70f11f59dbdf08fff7ef38c1f1ec878f96194988d9d8  ops/backup-verify.compose.yaml
    e516bea3688ad135556105772b5b8d603795bb04c7d7ce6ca760cbcf15714d52  ops/deploy-registry.sh
    88d7b26d510ee67a25760dae8a9611be25592ea651a8d9870afaf9e714f37c54  ops/reset-registry.sh
    89375a986b3cb64cf74145541701b32c6a0e983a72ca46be21a8bcfad7beaa3d  ops/backup-registry.sh
    50d3bb374dde35464f8e2d2c07a7dda95771149e7f2e8727bf03237e444ce8ab  ops/runtime-package-coordinator.sh
    
    Downloads 2
  • LINKS 1.0 — forward migration 사슬·운영 명령 단순화·문서 정리

    LINKS 1.0

    이전 릴리스 0.22.1(5cf5e1f2c3)부터 최종 소스 edac251454까지를 묶은 1.0 릴리스입니다. 루트 패키지는 1.0.0, ARCA는 exact 0.1.17(변경 없음)입니다. 1.0부터 schema 변경은 DB 초기화 대신 forward migration으로 배포합니다. 기준 schema에 migration ledger(links_schema_migration_history)가 추가되어 checksum이 바뀌었고, 그 checksum이 migration 사슬의 시작점입니다. 운영 명령 update.sh·reset.sh는 인자 없는 단일 동작으로 단순화했습니다.

    업그레이드 주의 — 0.x → 1.0은 마지막으로 한 번 DB 초기화(sh reset.sh)가 필요합니다

    1.0 기준 schema checksum 14481d17c5947342be029bf6f850d2f862372b988dcbc1809360cdd7b333c918(infra/migrations/base.sha256)이 forward migration 사슬의 시작점입니다(현재 migration 0개). 0.22.1 이하 schema(743448f6…)로 만든 DB는 사슬 밖이므로 1.0 image는 0.x DB를 열지 않습니다. update.sh는 container를 바꾸지 않고 Run: sh reset.sh로 멈춥니다. 1.0 이후 release부터는 초기화 없이 update.sh가 migration으로 갱신합니다.

    1. 전환 절차(한 번):

      1. 필요한 데이터가 있으면 아직 교체하지 않은 0.22.1 배포 파일로 sh backup.sh를 실행하고, 그 backup을 data/backups 밖으로 옮깁니다(reset.sh가 지웁니다). backup은 같은 schema 버전 image에서만 복구할 수 있으므로 이 backup은 1.0으로 가져올 수 없고 0.22.1 image로 되돌릴 때만 씁니다.
      2. package coordinator를 정지합니다.
      3. 1.0 태그의 배포 파일 7개를 받아 아래 SHA-256과 모두 같을 때만 교체합니다(운영 가이드 운영 배포의 받기·대조 블록, release_version='1.0').
      4. sh reset.sh → sh update.sh.
      5. coordinator를 시작하고 update.sh가 마지막 줄에 출력한 --package readiness 명령을 실행합니다. 종료 코드 0이어야 배포 완료입니다.
      6. 최초 관리자 등록(다른 사용자는 초대 링크로 다시 가입), 라이브러리 스캔과 실행 준비를 다시 진행합니다.
      cd /volume1/docker/links
      sh reset.sh
      sh update.sh
      # coordinator 시작 뒤
      docker compose --env-file '.env.production' exec -T catalog node apps/worker/dist/runtime-readiness-command.js --package
      
    2. update.sh·reset.sh는 인자를 받지 않습니다: --defer-package-readiness·--execute·--delete-rtp와 환경 파일 인자를 삭제했고, 어떤 인자든 Unknown argument로 Docker 호출 전에 거부합니다. 둘 다 .env.production만 읽습니다. reset.sh는 확인 없는 하드리셋이며 RTP 캐시·backups·translation-imports를 포함한 data 저장소 전체를 지웁니다(환경 파일·원본 게임·Compose·배포 script는 남습니다).

    3. 운영 디렉터리에 state/가 없습니다: translation import 기본 입력 경로가 ./state/translation-imports에서 **./data/translation-imports**로 바뀌었습니다. LINKS_TRANSLATION_IMPORT_SOURCE를 지정하지 않았다면 입력 파일을 새 위치에 둡니다.

    4. DB 비밀번호는 모두 서로 달라야 합니다: 적용기가 provision 전에 모든 login role 비밀번호를 bootstrap superuser 비밀번호(LINKS_DATABASE_PASSWORD·POSTGRES_PASSWORD)와, 그리고 서로와 대조합니다. .env.production에 같은 값이 있으면 migration container가 role 이름만 담은 오류로 실패하고 update.sh가 멈춥니다(role은 바뀌지 않음). 배포 전에 모든 LINKS_*_DATABASE_PASSWORD를 서로 다르게 둡니다.

    5. 알려진 제한(후속 과제): Player 세이브 관리 listener(3005)는 catalog caller token으로만 보호되며 edge network에서 TCP로 닿을 수 있습니다. catalog·Player의 일반 egress를 network가 제한하지 않습니다. update.sh는 opt-in profile container(translation-import 등)를 갱신하지 않으므로 갱신 뒤 직접 다시 기동합니다.

    원본 game library는 읽기 전용으로 유지하며, reset.sh는 library와 겹치거나 symlink인 저장소를 지우기 전에 거부합니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    Schema forward migration

    • 기준 schema(infra/schema/NNN_<domain>.sql)는 계속 빈 DB에 적용하는 현재 schema 전체입니다. schema를 바꾸는 변경은 도메인 파일 수정과 같은 내용을 적용하는 infra/migrations/NNNN_<name>.sql(머리 -- from:·-- target: 기준 schema checksum)을 함께 추가합니다. 둘 중 하나만 바꾸면 schema test가 실패합니다.
    • 적용기(migration service)는 advisory lock 아래에서 DB 상태를 판정합니다.
      • empty: 기준 schema를 적용하고 버전을 기록합니다.
      • current: login role만 다시 provision합니다.
      • migratable: pending migration 전체, 단계별 ledger 기록, 버전 갱신을 한 transaction으로 적용합니다. 한 단계라도 실패하면 아무것도 반영하지 않습니다.
      • chain-incomplete: image에 migration이 빠진 결함입니다. 초기화 대상이 아니며 DB를 바꾸지 않습니다.
      • mismatch·legacy-migrations·unversioned: 사슬 밖이며 초기화가 필요합니다.
      • schema-status.js는 같은 판정을 pendingCount·pending과 함께 출력합니다.
    • ledger links_schema_migration_history(이름·파일 SHA-256·from/target checksum·적용 시각)를 추가했고 catalog scope의 쓰기 권한은 회수했습니다.
    • 검증은 빈 DB fresh 적용과 migration 적용 결과의 구조 등가입니다. pg_catalog 기반 구조 비교 helper가 relation·column·type·constraint·index·view·function·trigger·policy·grant·comment 등을 정규화해 비교하고, 실제 사슬이 base.sha256에서 현재 기준 schema까지 이어지는지 확인합니다.
    • backup 복구 gate는 같은 schema 버전 또는 사슬 안 이전 버전 backup만 받습니다. 사슬 안 backup은 pg_restore 뒤 migration을 적용하고, ledger가 그 경로의 migration을 같은 파일 SHA-256으로 기록했는지 확인합니다. 사슬 밖·구 migration 시대 backup은 거부합니다.
    • 작업 규칙(AGENTS.md)과 schema 정책 문서를 DB 초기화 전제에서 1.0 forward migration 사슬 기준으로 바꿨습니다.

    운영 명령 단순화

    • reset.sh(ops/reset-registry.sh) 하드리셋:
      • 인자·확인 없이 모든 profile의 container·network·named volume을 내리고, Volume 표의 모든 저장소 내용을 지운 뒤 소유자·권한(postgres 0700, 나머지 0755)으로 다시 만듭니다.
      • 경로는 env 파일을 직접 읽지 않고 Compose 렌더링의 bind source(출력하지 않음)를 씁니다. 그래서 shell 변수 우선·따옴표·export·interpolation·~·.. 해석이 실제 mount와 같습니다.
      • down 전에 다음 저장소를 거부하고 아무것도 바꾸지 않습니다: 원본 library와 같거나 그 상위·하위인 저장소, 경로나 중간 디렉터리가 symlink인 저장소, filesystem root로 해석되는 library·저장소.
      • 성공하면 Done. Run: sh update.sh 한 줄을 출력합니다.
    • update.sh(ops/deploy-registry.sh) latest 갱신:
      • LINKS_REGISTRY_TAG(비어 있으면 latest)로 갱신합니다.
      • [3/5]에서 PostgreSQL이 정지돼 있으면 postgres만 먼저 기동하고, 새 image의 schema-status.js로 상태를 항상 확인합니다.
      • migratable이면 Pending migrations: <n>을 출력하고 up 때 migration이 적용합니다. chain-incomplete는 image 결함 메시지로, 사슬 밖은 Run: sh reset.sh로 container를 바꾸지 않고 멈춥니다.
      • container 변경 뒤 실패에는 Rollback: 한 줄을 출력합니다. migration이 이미 적용된 뒤의 실패에는 tag rollback 대신 pre-update backup을 이전 image로 복구하라고 안내합니다.
      • 성공 출력은 단계 표시와 마지막 두 줄(coordinator 시작과 --package 명령)입니다.
    • 운영 배포 디렉터리는 state/를 쓰지 않습니다.
    • 기록기: 배포 wrapper 40개 사례, reset 24개 사례입니다. Compose 의미의 dotenv 렌더링 mock을 공용 모듈로 분리했습니다.

    보안

    • 적용기가 9개 login role(runtime stager·builder·GC, launch broker, translation worker, runtime packager, catalog, scanner, metadata worker)의 비밀번호를 bootstrap superuser와, 그리고 서로와 한 번에 대조합니다. 충돌은 role 이름만으로 모아 보고합니다.
    • 검사는 기준 schema 적용·provision 전에 하므로 실패하면 어떤 role도 바뀌지 않습니다. 일부 조합만 보던 기존의 분산 비교를 대체합니다.

    문서

    • 개발 계획과 완료 기준선을 1.0 기준으로 정리했습니다(완료 항목 삭제, 현재 제공 범위·최신 검증만 유지). README·운영 가이드·테스트 전략은 운영 세 명령과 forward migration 기준으로 다시 썼습니다.
    • 계약·규칙 문서의 어긋난 서술과 죽은 anchor를 현재 코드에 맞췄습니다(Player 3005 노출, catalog egress, wrapper 거부 단언, 비밀번호 대조 범위 등). 문서 link 검사 결과는 484개 / 깨짐 0입니다.

    검사

    • engine-filter browser check를 현재 UI의 상세 필터 dialog 흐름(엔진 select·조건 적용·조건 chip·URL 복원·전체 보기)과 Firefox 실행에 맞췄습니다.

    검증과 남은 범위

    • PASS — migrations 브랜치(병합 af86da4): unit 25, schema 214, backup-restore, static, 문서 검사.
    • PASS — opssimple 브랜치(병합 ccd43b1): 배포 wrapper 기록기 40개·reset 기록기 24개 사례, compose config, BusyBox sh 실행, 실제 Compose 렌더링 probe, 격리 foundation, static, 문서 검사.
    • PASS — docs-1.0 브랜치(병합 660f3d6): run-docs, run-static, link 검사 484개 / 깨짐 0.
    • PASS — pre10-fixes 브랜치(병합 cadb746): packages/db unit 76, schema 228, static, engine-filter browser check Chromium·Firefox, 문서 검사.
    • PASS — 최종 1.0 소스: 릴리스 커밋 두 개(root version, 완료 기준선 version 문구) 뒤 문서 검사(747개 파일 / Markdown 30개), static, git diff --check를 다시 통과했습니다. 게시 이미지는 같은 최종 소스에서 build했고, image 안의 root version 1.0.0·ARCA 0.1.17·base.sha256을 확인했습니다.
    • PARTIAL — 전체 UI/제품 acceptance: 화면·fixture·package/build 성공을 실제 browser play PASS로 기록하지 않습니다.
    • NOT RUN:
      • 실제 0.x → 1.0 NAS 초기화·배포
      • NAS 설치 Compose의 실제 .env.production으로 migration 적용
      • 실제 registry pull·container 교체 중 --package readiness
      • 실제 1.0 이후 migration 적용(아직 migration 0개)
      • 실제 library/private game과 실제 scan
      • 입력·오디오·save/reload의 실제 게임 acceptance
      • 실제 모바일 기기·보조 기술
      • live provider
      • 개발 rebuild

    배포 이미지와 파일

    • Git annotated tag: 1.0, 최종 소스 edac251454.
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:1.0 = git.2ex.dev/hanyul/links:latest.
    • 원격 digest: sha256:9cb0a3455902bc899f4d0746410ae9422f492cb8426735a21974324423b5cd5a.
    • 아래 SHA-256은 checkout의 CRLF 변환본이 아닌 1.0 태그의 Git blob 원본 byte 기준입니다.
    • NAS 배포 목록은 7개입니다(update.sh=ops/deploy-registry.sh, reset.sh=ops/reset-registry.sh, backup.sh=ops/backup-registry.sh).
    • 0.22.1 대비 바뀐 파일은 compose.yaml, ops/deploy-registry.sh, ops/reset-registry.sh 세 개입니다. 0.x → 1.0 전환은 새 reset.sh로 실행해야 하므로 7개를 모두 1.0 파일로 맞춥니다.
    SHA-256 파일 0.22.1 대비
    ce841dde3a12e46a05677ca78fa229cce690899d2b51eb99da131e5b64a112a1 compose.yaml 변경 (translation import 기본 경로 ./data/translation-imports)
    6e1fd3f2f73faee52abca28c8552cde417f496d86fc3d78c7001dec480e171a3 infra/docker/Caddyfile 동일
    39a4c55050e76043c8ca70f11f59dbdf08fff7ef38c1f1ec878f96194988d9d8 ops/backup-verify.compose.yaml 동일
    e516bea3688ad135556105772b5b8d603795bb04c7d7ce6ca760cbcf15714d52 ops/deploy-registry.sh 변경 (인자 없음, migration 상태 분기, 정지 postgres 선기동)
    88d7b26d510ee67a25760dae8a9611be25592ea651a8d9870afaf9e714f37c54 ops/reset-registry.sh 변경 (인자 없는 하드리셋, Compose 렌더링 경로 판정)
    89375a986b3cb64cf74145541701b32c6a0e983a72ca46be21a8bcfad7beaa3d ops/backup-registry.sh 동일
    79c5fbd082ada3117e0915af76c714ed498ea018f151a998cd23f53d92fc3530 ops/runtime-package-coordinator.sh 동일
    ce841dde3a12e46a05677ca78fa229cce690899d2b51eb99da131e5b64a112a1  compose.yaml
    6e1fd3f2f73faee52abca28c8552cde417f496d86fc3d78c7001dec480e171a3  infra/docker/Caddyfile
    39a4c55050e76043c8ca70f11f59dbdf08fff7ef38c1f1ec878f96194988d9d8  ops/backup-verify.compose.yaml
    e516bea3688ad135556105772b5b8d603795bb04c7d7ce6ca760cbcf15714d52  ops/deploy-registry.sh
    88d7b26d510ee67a25760dae8a9611be25592ea651a8d9870afaf9e714f37c54  ops/reset-registry.sh
    89375a986b3cb64cf74145541701b32c6a0e983a72ca46be21a8bcfad7beaa3d  ops/backup-registry.sh
    79c5fbd082ada3117e0915af76c714ed498ea018f151a998cd23f53d92fc3530  ops/runtime-package-coordinator.sh
    
    Downloads 2
  • LINKS 0.22.1 — 배포 wrapper 기본 흐름·pids_limit 제거·스캔 상태 popover

    LINKS 0.22.1

    이전 릴리스 0.22(a96fbde7d0)부터 최종 소스 5cf5e1f2c3까지의 patch 릴리스입니다. 루트 패키지는 0.22.1, ARCA는 exact 0.1.17(변경 없음)입니다. 기준 schema는 0.22와 같고, 운영 compose.yaml과 배포 wrapper(ops/deploy-registry.sh → NAS update.sh)가 바뀌었습니다.

    업그레이드 주의 — DB 초기화는 필요 없지만, 배포 wrapper·Compose·coordinator·image를 같은 0.22.1 태그로 함께 교체합니다

    기준 schema는 바뀌지 않았습니다(743448f6d2891794e5ba98834d9f5055c3f5c58e22276ed701de36650f4b2b07 유지). 0.22 DB·사용자·패스키·세이브·backup·실행 build를 그대로 사용하며 reset.sh를 실행하지 않습니다. ARCA pin과 coordinator script가 0.22와 같으므로 실행 build 재준비도 필요 없습니다.

    1. pids_limit 제거: 공통 compose.yaml의 byedpi·log-reader·backup에서 pids_limit을 지웠습니다. NAS 커널이 PIDs cgroup을 지원하지 않아 나던 경고가 사라지며, 이제 어떤 service에도 PID 수 제한이 없습니다.

    2. 배포 wrapper는 option을 받지 않습니다: 0.22의 sh update.sh --defer-package-readiness는 Unknown option으로 Docker 호출 전에 거부됩니다. VX/VX Ace(ARCA package) 실행이 기본 구성이므로 wrapper는 1단계에서 Compose가 렌더링한 catalog의 LINKS_RUNTIME_PACKAGE_ENABLED로 LINKS_RUNTIME_PACKAGER_DATABASE_PASSWORD가 비어 있지 않은지 확인하고, 비어 있으면 멈춥니다(shell 변수가 빈 값으로 설정돼 있어도 그 값이 우선합니다). 갱신 뒤에는 항상 미완료 안내와 함께 coordinator 시작과 아래 --package readiness 명령을 안내하며, 그 명령이 종료 코드 0이어야 갱신 완료입니다. 그래서 운영 배포는 coordinator 정지 → 같은 0.22.1 태그의 update.sh·compose.yaml·ops/runtime-package-coordinator.sh 등을 아래 해시로 대조해 함께 교체 → sh update.sh → coordinator 시작 → 출력된 명령 실행의 단일 절차입니다(운영 가이드 운영 배포).

      cd /volume1/docker/links
      docker compose --env-file '.env.production' exec -T catalog node apps/worker/dist/runtime-readiness-command.js --package
      
    3. 스캔 상태 상세 위치 변경(관리자 UI): 우측 하단 고정창·header 아래 흐름 배치였던 스캔 상태 상세가 상단바 스캔 표시기의 hover·키보드 초점·탭 popover로 바뀌었습니다. 데이터·API 변경은 없습니다.

    원본 game library는 읽기 전용으로 유지하며 변경하지 않습니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    운영 배포

    • compose.yaml에서 pids_limit: 64 세 곳을 삭제했습니다. 보안 격리 정책과 운영 가이드 요구 사항에 "공통 Compose의 어떤 service에도 pids_limit을 선언하지 않는다"를 적었습니다. develop·test·backup-verify Compose에는 원래 없었습니다.
    • 배포 wrapper 기본 흐름:
      • 인자는 환경 파일 하나뿐이며 -로 시작하는 인자는 모두 거부합니다(--defer-package-readiness 포함). 두 번째 환경 파일도 거부합니다.
      • [1/5]에서 compose config 렌더링 결과를 출력하지 않고 변수로만 받아, packager 비밀번호로 파생되는 LINKS_RUNTIME_PACKAGE_ENABLED가 "true"인지로 판정합니다. 따옴표·주석·export·interpolation·CRLF·마지막 중복 값은 Compose 해석을 그대로 따르며, 이 단계에서 실패하면 Docker 호출은 config 하나뿐입니다. 형식이 잘못된 env 줄은 Compose 자체 오류(stderr)에 그 내용이 포함될 수 있습니다.
      • up --wait 뒤 catalog의 package 기능이 켜졌는지 다시 확인하고, 아니면 같은 release의 compose.yaml을 쓰라는 오류와 rollback digest를 출력합니다.
      • [5/5]는 coordinator가 정지한 상태를 전제로 stage·build readiness만 확인합니다. 성공하면 "NOT complete" 안내, 배포 디렉터리에서 실행할 정확한 --package 명령(환경 파일 경로 인용), 실패 시 이전 coordinator script·compose.yaml·image digest를 함께 되돌리는 안내를 출력합니다.
      • LINKS_REGISTRY_TAG가 CRLF 환경 파일에 있을 때 rollback 안내가 깨지던 결함을 고쳤습니다.
    • 배포 wrapper 기록기(tests/scripts/check-deploy-registry.mjs)를 27개 사례로 다시 썼습니다. 성공 경로·실패 경로·Docker 호출 0 거부, 비밀번호 판정(shell 빈 값·따옴표 빈 값과 주석·export·CRLF·중복 마지막 값·interpolation 등), 출력된 --package 명령을 그대로 실행해 coordinator 전 exit 1·heartbeat 뒤 exit 0, 모든 출력에 합성 비밀 표식이 없음을 단언합니다. mock의 dotenv 해석은 같은 13개 입력을 실제 Docker Compose 렌더링과 대조했습니다.
    • RED 보존: 0.22 script는 같은 기록기에서 27개 중 19개, 수정 중간판은 16개 실패했고, 렌더링 결과를 출력하는 변형도 16개 실패해 비출력 단언이 동작함을 확인했습니다.
    • 운영 가이드의 운영 배포를 coordinator를 포함한 하나의 5단계 절차로 다시 썼고, NAS ARCA package coordinator에서 option 언급을 지우고 비밀번호가 운영 필수임을 적었습니다. .env.example의 비밀번호 주석도 같은 규칙으로 맞췄습니다.

    관리자 UI — 스캔 상태 표시기

    • 상단바의 스캔 표시기 하나만 상시 보입니다(관리자 전용, 이전 상태창과 같은 노출 조건). 진행 중 스캔 중 · <단계>, 중단 요청 뒤 스캔 중단 요청됨, 끝난 뒤 닫기 전까지 스캔 완료·스캔 실패·스캔 중단됨을 표시하고 tone은 기존 token만 씁니다.
    • 상세는 표시기의 popover(role=dialog)로 엽니다.
      • 마우스: hover로 열고, 표시기와 popover를 벗어난 뒤 150ms가 지나면 닫힙니다(그 사이 재진입하면 유지).
      • 키보드: :focus-visible 초점일 때만 열고 초점이 밖으로 나가면 닫습니다. Escape는 닫고 초점을 표시기로 돌리며, Enter/Space로 다시 엽니다. 페이지 입력란에 초점이 있으면 초점을 빼앗지 않고 닫기만 합니다.
      • 터치: 탭으로 toggle하고 바깥을 탭하면 닫습니다.
    • 끝난 scan은 닫기 전까지 완료 상태를 유지합니다. 닫기는 열림 이유와 대기 중인 hover timer를 모두 버리고 표시기를 지운 뒤 초점을 본문으로 옮깁니다. 다음 scan 요청 때 popover가 저절로 열리지 않습니다.
    • popover의 경과 시간은 열려 있는 동안만 1초마다 갱신합니다. 닫힌 동안 interval이 없고, 다시 열면 현재 시각에서 바로 계산합니다.
    • 접근 이름은 보이는 문구를 따릅니다. 표시기 버튼과 관리자에서 보기에는 aria-label이 없고, 닫기는 dialog 제목 문맥의 보이는 닫기입니다. live region은 표시기 버튼만 감싸므로 경과 시간과 열림·닫힘은 낭독되지 않습니다.
    • popover는 sticky header 안에 배치되어 header 바로 아래에서 viewport 안에 들고, 관리자 탭·스캔 옵션 메뉴 위에 그려지며, 320–1440px·글자 200%에서 가로 넘침이 없습니다.
    • 우측 하단 고정창, header 아래 흐름 slot·spacer, 접기 toggle과 관련 CSS·layout 변수를 삭제했습니다.
    • browser check 행렬을 교체했습니다. catalog-browser-check는 표시기·hover·이탈·키보드·Escape·scroll·좁은 폭·터치·닫기, 다음 요청에서 닫힌 상태 유지와 1초 interval 수를 확인하고, library-browser-check는 홈·라이브러리·상세 화면의 popover 노출을 확인합니다. 통합 runner도 표시기 hover와 popover로 바꿨습니다.

    문서·검증 갱신

    • 운영 가이드: 운영 배포 단일 절차, wrapper 1단계 판정, 최종 readiness 명령 블록(배포 디렉터리에서 실행), 관리자 scan 절차의 상단바 표시기 안내.
    • 완료 기준선·tests README: 기록기 27건과 RED 수치, mock 한계, 표시기 검증 범위(unit은 reducer/helper·static markup, 실제 mount는 browser check).
    • 보안 격리 정책: PIDs 제한 없음.

    검증과 남은 범위

    • PASS — pids 브랜치(병합 3990d18): compose config --profile '*' 4조합(compose / +develop / +test / +develop+test) 종료 코드 0과 렌더링 pids_limit 0건, 배포 wrapper 기록기 27개 사례, run-foundation(격리 Compose, PIDs 경고 0건), static, 문서 검사, git diff --check.
    • PASS — scanfloat 브랜치(병합 e0520c1과 같은 tree, main 3990d18 반영 뒤): apps/web unit 38개 파일 / 167개 테스트, static, catalog-browser-check Chromium·Firefox × 글자 100%·200%, library-browser-check Chromium·Firefox, run-catalog-browser, 문서 검사, git diff --check. 수정 전 표시기로 같은 check를 돌려 기대대로 실패하는 RED도 보존했습니다. personal-library·admin-operations·background-jobs browser check는 대상 표면이 바뀌지 않아 1차 수정(b635239)의 PASS를 유지합니다.
    • PASS — 최종 0.22.1 소스: 릴리스 커밋 두 개(root version, 완료 기준선 version 문구) 뒤 문서 검사(747개 파일 / Markdown 30개, mount 6개 / 29개 문서 파일), static과 git diff --check를 다시 통과했습니다. 게시 이미지는 같은 최종 소스에서 build했고 image 안의 root version 0.22.1·ARCA 0.1.17을 확인했습니다.
    • PARTIAL — 전체 UI/제품 acceptance: 화면·fixture·package/build 성공을 실제 browser play PASS로 기록하지 않습니다.
    • NOT RUN: 실제 library/private game과 실제 library scan, 입력·오디오·save/reload의 실제 게임 acceptance, NAS 설치 Compose의 렌더링·dotenv 해석과 BusyBox sh에서의 wrapper 실행, 실제 registry pull·container 교체·coordinator 교체 중 --package readiness, 실제 모바일 기기·보조 기술 낭독, live provider, fresh schema 테스트(schema 변경 없음), 개발 rebuild와 NAS/production 배포.

    배포 이미지와 파일

    • Git annotated tag: 0.22.1, 최종 소스 5cf5e1f2c3.
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:0.22.1 = git.2ex.dev/hanyul/links:latest.
    • 원격 digest: sha256:f78426e91a2c221f33fa02eed518d824ad8e5ac499f1470c0df10f2e0f2a0902.
    • 아래 SHA-256은 checkout의 CRLF 변환본이 아닌 0.22.1 태그의 Git blob 원본 byte 기준입니다. NAS 배포 목록은 앞의 6개이며, reset-registry.sh는 전체 초기화 때만, .ps1은 Windows 개발 coordinator입니다. 0.22 대비 바뀐 파일은 **compose.yaml**과 ops/deploy-registry.sh(NAS update.sh) 두 개이고 나머지는 0.22와 byte 동일합니다.
    SHA-256 파일 0.22 대비
    cce561572e41f26fe368814c190423a8b48364ed231a35b20119964062411480 compose.yaml 변경 (pids_limit 제거)
    6e1fd3f2f73faee52abca28c8552cde417f496d86fc3d78c7001dec480e171a3 infra/docker/Caddyfile 동일
    39a4c55050e76043c8ca70f11f59dbdf08fff7ef38c1f1ec878f96194988d9d8 ops/backup-verify.compose.yaml 동일
    9872d5e5ae4046915625e76dbbe8cb750ff42fb7396563826f56c4f305bfc128 ops/deploy-registry.sh 변경 (option 없음, packager 비밀번호 필수)
    89375a986b3cb64cf74145541701b32c6a0e983a72ca46be21a8bcfad7beaa3d ops/backup-registry.sh 동일
    79c5fbd082ada3117e0915af76c714ed498ea018f151a998cd23f53d92fc3530 ops/runtime-package-coordinator.sh 동일
    84bcc60de28de692fba4d7bea16bf008a8ac55abe394e26f87e541abf5db52dc ops/reset-registry.sh 동일
    0593ef6347103a9ceffb65969c63794ce6ab6f5321e094a7681b61f54ca19104 ops/runtime-package-coordinator.ps1 동일
    cce561572e41f26fe368814c190423a8b48364ed231a35b20119964062411480  compose.yaml
    6e1fd3f2f73faee52abca28c8552cde417f496d86fc3d78c7001dec480e171a3  infra/docker/Caddyfile
    39a4c55050e76043c8ca70f11f59dbdf08fff7ef38c1f1ec878f96194988d9d8  ops/backup-verify.compose.yaml
    9872d5e5ae4046915625e76dbbe8cb750ff42fb7396563826f56c4f305bfc128  ops/deploy-registry.sh
    89375a986b3cb64cf74145541701b32c6a0e983a72ca46be21a8bcfad7beaa3d  ops/backup-registry.sh
    79c5fbd082ada3117e0915af76c714ed498ea018f151a998cd23f53d92fc3530  ops/runtime-package-coordinator.sh
    84bcc60de28de692fba4d7bea16bf008a8ac55abe394e26f87e541abf5db52dc  ops/reset-registry.sh
    0593ef6347103a9ceffb65969c63794ce6ab6f5321e094a7681b61f54ca19104  ops/runtime-package-coordinator.ps1
    
    Downloads 2