From ec677f5ec506d38ae2a2e69e82fdc0d522fa2531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=83=81=ED=9B=88?= Date: Mon, 31 Aug 2026 13:03:54 +0900 Subject: [PATCH] chore: validate legacy redirect contract --- .github/dependabot.yml | 10 +++++++ .github/workflows/quality.yml | 23 ++++++++++++++++ README.md | 13 +++++++++ scripts/check-redirect.mjs | 52 +++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/quality.yml create mode 100644 README.md create mode 100644 scripts/check-redirect.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..812ca36 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: tuesday + time: "03:30" + timezone: Asia/Seoul + open-pull-requests-limit: 3 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..bb7f646 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,23 @@ +name: Redirect quality + +on: + push: + pull_request: + +permissions: + contents: read + +concurrency: + group: redirect-quality-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 22 + - run: node scripts/check-redirect.mjs https://tools.agenticfabworks.com/ /agenticfabworks_tools diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea0e21f --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# AgenticFabWorks Tools legacy redirect + +예전 GitHub Pages 주소의 방문자를 `https://tools.agenticfabworks.com/`으로 보내는 최소 리디렉트 저장소입니다. 기존 기본 경로는 `/agenticfabworks_tools`이며, 뒤의 경로·쿼리·해시는 새 주소에도 유지합니다. + +이 저장소에는 실제 서비스 소스, 인증 정보, 사용자 데이터가 없습니다. 검색 결과 중복을 막기 위해 HTML은 `noindex, nofollow`, `robots.txt`는 전체 수집 금지를 사용합니다. + +## 검증 + +```powershell +node scripts/check-redirect.mjs https://tools.agenticfabworks.com/ /agenticfabworks_tools +``` + +`.github/workflows/quality.yml`도 push와 pull request에서 같은 검사를 실행합니다. Pages 공개 여부는 별도의 GitHub 저장소 설정입니다. 배포 워크플로가 존재해도 Pages가 자동으로 활성화되지는 않습니다. diff --git a/scripts/check-redirect.mjs b/scripts/check-redirect.mjs new file mode 100644 index 0000000..ebce980 --- /dev/null +++ b/scripts/check-redirect.mjs @@ -0,0 +1,52 @@ +import fs from "node:fs"; + +const [target, base] = process.argv.slice(2); +const errors = []; + +function check(condition, message) { + if (!condition) errors.push(message); +} + +if (!target || !base) { + console.error("usage: node scripts/check-redirect.mjs "); + process.exit(2); +} + +let origin; +try { + const parsed = new URL(target); + check(parsed.protocol === "https:", "target URL must use HTTPS"); + check(parsed.pathname === "/" && !parsed.search && !parsed.hash, "target URL must be an origin root ending in /"); + origin = parsed.origin; +} catch { + errors.push("target URL is invalid"); +} + +check(base.startsWith("/") && base.length > 1 && !base.endsWith("/"), "legacy base path must start with one / and have no trailing /"); + +const required = ["index.html", "404.html", "robots.txt"]; +for (const file of required) { + check(fs.existsSync(file) && fs.statSync(file).size > 0, `${file} must exist and be non-empty`); +} + +if (errors.length === 0) { + const pages = [["index.html", fs.readFileSync("index.html", "utf8")], ["404.html", fs.readFileSync("404.html", "utf8")]]; + for (const [name, html] of pages) { + check(//i.test(html), `${name}: robots noindex,nofollow is required`); + check(//i.test(html), `${name}: no-referrer policy is required`); + check(html.includes(``), `${name}: canonical URL must be ${target}`); + check(html.includes(`const base = "${base}";`), `${name}: legacy base must be ${base}`); + check(html.includes("location.pathname.startsWith(base)"), `${name}: redirect must recognize the legacy base path`); + check(html.includes("location.pathname.slice(base.length) || \"/\""), `${name}: redirect must preserve the path suffix`); + const redirect = "location.replace(`" + origin + "${path}${location.search}${location.hash}`);"; + check(html.includes(redirect), `${name}: redirect must preserve path, query, and hash`); + check(!/[A-Za-z]:\\(?:Users|dev)\\|\/Users\//i.test(html), `${name}: personal or stale absolute path found`); + } + check(pages[0][1].includes(``), `index.html: fallback refresh must target ${target}`); + check(!/http-equiv=["']refresh["']/i.test(pages[1][1]), "404.html: refresh tag must remain absent so the script can preserve the path"); + check(fs.readFileSync("robots.txt", "utf8").replace(/\r\n/g, "\n").trim() === "User-agent: *\nDisallow: /", "robots.txt must block all crawling"); +} + +const result = { base, errors, status: errors.length ? "fail" : "pass", target }; +console.log(JSON.stringify(result)); +process.exit(errors.length ? 1 : 0);