Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -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://skct.agenticfabworks.com/ /skct_tool
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SKCT Tool legacy redirect

예전 GitHub Pages 주소의 방문자를 `https://skct.agenticfabworks.com/`으로 보내는 최소 리디렉트 저장소입니다. 기존 기본 경로는 `/skct_tool`이며, 뒤의 경로·쿼리·해시는 새 주소에도 유지합니다.

이 저장소에는 실제 서비스 소스, 인증 정보, 사용자 데이터가 없습니다. 검색 결과 중복을 막기 위해 HTML은 `noindex, nofollow`, `robots.txt`는 전체 수집 금지를 사용합니다.

## 검증

```powershell
node scripts/check-redirect.mjs https://skct.agenticfabworks.com/ /skct_tool
```

`.github/workflows/quality.yml`도 push와 pull request에서 같은 검사를 실행합니다. Pages 공개 여부는 별도의 GitHub 저장소 설정입니다. 배포 워크플로가 존재해도 Pages가 자동으로 활성화되지는 않습니다.
52 changes: 52 additions & 0 deletions scripts/check-redirect.mjs
Original file line number Diff line number Diff line change
@@ -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 <target-url> <legacy-base-path>");
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(/<meta\s+name=["']robots["']\s+content=["']noindex,\s*nofollow["']\s*\/?>/i.test(html), `${name}: robots noindex,nofollow is required`);
check(/<meta\s+name=["']referrer["']\s+content=["']no-referrer["']\s*\/?>/i.test(html), `${name}: no-referrer policy is required`);
check(html.includes(`<link rel="canonical" href="${target}">`), `${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(`<meta http-equiv="refresh" content="0; url=${target}">`), `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);
Loading