From 19d8575df99bf7b3350adf8693e9e1fe114c16ab Mon Sep 17 00:00:00 2001 From: Jaybe Date: Fri, 14 Nov 2025 23:39:05 +0900 Subject: [PATCH 1/3] =?UTF-8?q?chore(ci):=20=EB=9D=BC=EB=B2=A8=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0(auto-label/auto-pr)=20=E2=80=94=20=EA=B8=B0=EC=A1=B4?= =?UTF-8?q?=20'automerge'=EB=A7=8C=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/auto-label.yml | 19 ++++++------------- .gitea/workflows/auto-pr.yml | 19 ++++++------------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/.gitea/workflows/auto-label.yml b/.gitea/workflows/auto-label.yml index 723c48d..f66113a 100644 --- a/.gitea/workflows/auto-label.yml +++ b/.gitea/workflows/auto-label.yml @@ -20,22 +20,15 @@ jobs: echo "CI_TOKEN not set; skipping labeling"; exit 0; fi OWNER=$(echo "$REPO" | cut -d'/' -f1) NAME=$(echo "$REPO" | cut -d'/' -f2) - echo "Resolving label id for 'automerge' in $OWNER/$NAME" + echo "Resolving existing label id for 'automerge' in $OWNER/$NAME (no creation)" curl -sS -o /tmp/labels.json -H "Authorization: token ${CI_TOKEN}" \ "${GITEA_BASE_URL}/api/v1/repos/${OWNER}/${NAME}/labels?limit=1000" || true - # Robust parse without jq: find the id preceding name "automerge" - LID=$(tr '\n' ' ' < /tmp/labels.json | grep -o '"id":[0-9][0-9]*[^\}]*"name":"automerge"' | head -n1 | sed -E 's/.*"id":([0-9]+).*/\1/' || true) + # Extract all ids whose name == automerge, prefer the smallest id (oldest) + LID=$(tr '\n' ' ' < /tmp/labels.json | \ + grep -o '"id":[0-9][0-9]*[^\}]*"name":"automerge"' | \ + sed -E 's/.*"id":([0-9]+).*/\1/' | sort -n | head -n1 || true) if [ -z "${LID:-}" ]; then - echo "Label 'automerge' not found. Creating..." - curl -sS -o /tmp/create_label.json -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: token ${CI_TOKEN}" \ - "${GITEA_BASE_URL}/api/v1/repos/${OWNER}/${NAME}/labels" \ - -d '{"name":"automerge","color":"#0ea5e9"}' - LID=$(tr '\n' ' ' < /tmp/create_label.json | sed -E 's/.*"id":([0-9]+).*/\1/' | head -n1 || true) - fi - if [ -z "${LID:-}" ]; then - echo "Failed to resolve or create label id; skipping"; exit 0; fi + echo "No existing 'automerge' label found; skipping labeling"; exit 0; fi echo "Labeling PR #$PR_NUMBER with id $LID" curl -sS -X POST \ -H "Content-Type: application/json" \ diff --git a/.gitea/workflows/auto-pr.yml b/.gitea/workflows/auto-pr.yml index 87d8fe2..33e3bb0 100644 --- a/.gitea/workflows/auto-pr.yml +++ b/.gitea/workflows/auto-pr.yml @@ -55,19 +55,12 @@ jobs: done if [ -n "${PR_NUMBER:-}" ]; then - echo "Resolving label id for 'automerge' in $OWNER/$NAME" + echo "Resolving existing label id for 'automerge' in $OWNER/$NAME (no creation)" curl -sS -o /tmp/labels.json -H "Authorization: token ${CI_TOKEN}" \ - "${GITEA_BASE_URL}/api/v1/repos/${OWNER}/${NAME}/labels" - LID=$(awk 'BEGIN{RS=","} /"name":"automerge"/ {if (match($0,/"id":([0-9]+)/,m)) {print m[1]; exit}}' /tmp/labels.json || true) - if [ -z "${LID:-}" ]; then - echo "Label 'automerge' not found. Creating..." - curl -sS -o /tmp/create_label.json -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: token ${CI_TOKEN}" \ - "${GITEA_BASE_URL}/api/v1/repos/${OWNER}/${NAME}/labels" \ - -d '{"name":"automerge","color":"#0ea5e9"}' - LID=$(sed -n 's/.*"id":\([0-9]*\).*/\1/p' /tmp/create_label.json | head -n1 || true) - fi + "${GITEA_BASE_URL}/api/v1/repos/${OWNER}/${NAME}/labels?limit=1000" || true + LID=$(tr '\n' ' ' < /tmp/labels.json | \ + grep -o '"id":[0-9][0-9]*[^\}]*"name":"automerge"' | \ + sed -E 's/.*"id":([0-9]+).*/\1/' | sort -n | head -n1 || true) if [ -n "${LID:-}" ]; then echo "Adding 'automerge' (id=$LID) label to PR #$PR_NUMBER" curl -sS -X POST \ @@ -76,7 +69,7 @@ jobs: "${GITEA_BASE_URL}/api/v1/repos/${OWNER}/${NAME}/issues/${PR_NUMBER}/labels" \ -d '{"labels":['"${LID}"']}' || true else - echo "Failed to resolve label id; skipping label add" + echo "No existing 'automerge' found; skip labeling" fi else echo "PR number could not be determined; skipping label add" -- 2.52.0 From 0e70cee93c990ee64115626921da9620bc267484 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Fri, 14 Nov 2025 23:45:08 +0900 Subject: [PATCH 2/3] =?UTF-8?q?test(zip):=20Export=20ZIP=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EA=B0=95=ED=99=94(assets=20=EC=9C=A0=EB=AC=B4/?= =?UTF-8?q?=EC=84=9C=EB=B8=8C=ED=8F=B4=EB=8D=94=20=ED=8F=AC=ED=95=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/exporter/zip.validation.test.ts | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 lib/exporter/zip.validation.test.ts diff --git a/lib/exporter/zip.validation.test.ts b/lib/exporter/zip.validation.test.ts new file mode 100644 index 0000000..c6ee0dc --- /dev/null +++ b/lib/exporter/zip.validation.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest' +import JSZip from 'jszip' +import { buildZip } from '@/lib/exporter/zip' + +const TE = new TextEncoder() + +describe('Export ZIP 검증 강화', () => { + it('assets가 비어있을 때 index.html/styles.css/app.js만 포함한다', async () => { + const zipBytes = await buildZip({ + html: 'aok', + css: 'body{color:#111}', + js: 'console.log("ok")', + assets: {}, + }) + const zip = await JSZip.loadAsync(zipBytes) + const names = Object.keys(zip.files).sort() + expect(names).toEqual(['app.js','index.html','styles.css']) + }) + + it('assets가 있으면 모든 파일을 그대로 포함한다(서브폴더 포함)', async () => { + const assets = { + 'assets/img/logo.png': new Uint8Array([0,1,2,3]), + 'assets/svg/icon.svg': TE.encode(''), + 'assets/fonts/inter.woff2': new Uint8Array([4,5,6]), + } + const zipBytes = await buildZip({ + html: 'aok', + css: 'body{color:#111}', + js: 'console.log("ok")', + assets, + }) + const zip = await JSZip.loadAsync(zipBytes) + const names = Object.keys(zip.files) + expect(names).toContain('index.html') + expect(names).toContain('styles.css') + expect(names).toContain('app.js') + expect(names).toContain('assets/img/logo.png') + expect(names).toContain('assets/svg/icon.svg') + expect(names).toContain('assets/fonts/inter.woff2') + + const logo = await zip.files['assets/img/logo.png'].async('uint8array') + expect(Array.from(logo)).toEqual([0,1,2,3]) + }) +}) -- 2.52.0 From 80e7586154531c5167cbc0d0f74fd8f19fa2a17e Mon Sep 17 00:00:00 2001 From: Jaybe Date: Fri, 14 Nov 2025 23:53:28 +0900 Subject: [PATCH 3/3] =?UTF-8?q?test(zip):=20=ED=86=B5=ED=95=A9/=EB=A9=94?= =?UTF-8?q?=ED=83=80=20=EC=8A=A4=EB=83=85=EC=83=B7=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80(exportPage+buildZip,=20SEO/OG/An?= =?UTF-8?q?alytics=20=EB=B3=B4=EC=A1=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zip.validation.integrated.test.ts | 53 +++++++++++++++++++ lib/exporter/zip.validation.meta.test.ts | 28 ++++++++++ 2 files changed, 81 insertions(+) create mode 100644 lib/exporter/zip.validation.integrated.test.ts create mode 100644 lib/exporter/zip.validation.meta.test.ts diff --git a/lib/exporter/zip.validation.integrated.test.ts b/lib/exporter/zip.validation.integrated.test.ts new file mode 100644 index 0000000..0244912 --- /dev/null +++ b/lib/exporter/zip.validation.integrated.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest' +import JSZip from 'jszip' +import { exportPage } from '@/lib/exporter/html' +import { buildZip } from '@/lib/exporter/zip' +import { AssetManager } from '@/lib/assets/assetManager' +import { pageSchema, type Page } from '@/lib/schema/page' + +function makePageWithLocalAsset(): { page: Page; am: AssetManager } { + const am = new AssetManager({ maxBytes: 1024 * 1024, allowedExts: ['png','svg','jpg','jpeg','webp'] }) + // simulate adding a local image and referencing it by original name via local: prefix + const bytes = new Uint8Array([7,8,9]) + // add as "hero.png" + am.add({ name: 'hero.png', type: 'image/png', bytes }) + + const base: Page = pageSchema.parse({ + title: 'Integrated ZIP', + locale: 'en', + theme: { primaryColor: '#2563eb', fontFamily: 'Inter' }, + sections: [ + { type: 'hero', props: { heading: 'Hello', subheading: 'World', imageUrl: 'local:hero.png' } }, + { type: 'cta', props: { text: 'Go', href: '#' } }, + ], + form: { actionUrl: '', fields: [], spamProtection: { honeypotFieldName: '_hp', minSubmitSeconds: 2 } }, + analytics: { ga4MeasurementId: 'G-TEST', metaPixelId: '', customHeadHtml: '' }, + seo: { title: 't', description: 'd' }, + }) + return { page: base, am } +} + +describe('Export ZIP 통합 검증', () => { + it('exportPage 출력 + buildZip 결과에서 assets 경로/메타가 보존된다', async () => { + const { page, am } = makePageWithLocalAsset() + const out = exportPage(page, { assetManager: am }) + const assets = am.toZipStructure() + const zipBytes = await buildZip({ html: out.html, css: out.css, js: out.js, assets }) + + const zip = await JSZip.loadAsync(zipBytes) + const names = Object.keys(zip.files) + // assets 파일이 존재해야 함 + const assetKey = Object.keys(assets)[0] + expect(names).toContain(assetKey) + + // index.html에 canonical/meta/ga4 스니펫 보존 + const html = await zip.files['index.html'].async('string') + expect(html).toMatch(/]*src="([^"]+)"/) + expect(match && match[1]).toBe(assetKey) + }) +}) diff --git a/lib/exporter/zip.validation.meta.test.ts b/lib/exporter/zip.validation.meta.test.ts new file mode 100644 index 0000000..5934287 --- /dev/null +++ b/lib/exporter/zip.validation.meta.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest' +import JSZip from 'jszip' +import { buildZip } from '@/lib/exporter/zip' + +describe('Export ZIP 메타 보존 검증', () => { + it('index.html의 SEO/OG/Analytics 메타가 그대로 보존된다', async () => { + const html = ` + + + + + + + ok` + const css = 'body{color:#222}' + const js = 'console.log("ok")' + + const zipBytes = await buildZip({ html, css, js, assets: {} }) + const zip = await JSZip.loadAsync(zipBytes) + const htmlContent = await zip.files['index.html'].async('string') + + expect(htmlContent).toMatch(/