This repository has been archived by the owner on Aug 2, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'jaoafa:main' into main
- Loading branch information
Showing
35 changed files
with
743 additions
and
285 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1 @@ | ||
@book000 | ||
@Hiratake | ||
* @book000 @Hiratake |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
yarn.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import { Octokit } from "@octokit/rest"; | ||
import { Page, test } from "@playwright/test"; | ||
import { execSync } from "child_process"; | ||
import fs from "fs"; | ||
import ImgurAnonymousUploader from "imgur-anonymous-uploader"; | ||
|
||
// 環境変数 | ||
// GITHUB_REPOSITORY: 所有者およびリポジトリの名前 | ||
// ISSUE_NUMBER: プルリクの番号 (envでgithub.event.numberを渡す必要あり) | ||
// GITHUB_TOKEN: GitHub API トークン (envでsecrets.GITHUB_TOKENを渡す必要あり) | ||
// BASE_SHA: プルリクのベース SHA | ||
// IMGUR_CLIENT_ID: Imgur クライアント ID (オプション) | ||
|
||
const OWNER = process.env.GITHUB_REPOSITORY!.split("/")[0]; | ||
const REPO = process.env.GITHUB_REPOSITORY!.split("/")[1]; | ||
const ISSUE_NUMBER = parseInt(process.env.ISSUE_NUMBER); | ||
const IMGUR_CLIENT_ID = process.env.IMGUR_CLIENT_ID; | ||
const BASE_SHA = process.env.BASE_SHA; | ||
|
||
async function scrollFullPage(page: Page) { | ||
await page.evaluate(async () => { | ||
await new Promise<void>((resolve) => { | ||
let totalHeight = 0; | ||
const distance = 100; | ||
const timer = setInterval(() => { | ||
const scrollHeight = document.body.scrollHeight; | ||
window.scrollBy(0, distance); | ||
totalHeight += distance; | ||
|
||
if (totalHeight >= scrollHeight) { | ||
clearInterval(timer); | ||
resolve(); | ||
} | ||
}, 100); | ||
}); | ||
}); | ||
} | ||
|
||
test("page screenshot", async ({ page }) => { | ||
const files = execSync( | ||
"git diff --diff-filter=ACMR --name-only " + BASE_SHA + " HEAD", | ||
{ | ||
cwd: process.env.GITHUB_WORKSPACE + "/content/", | ||
} | ||
) | ||
.toString() | ||
.split("\n"); | ||
|
||
fs.mkdirSync("screenshots", { recursive: true }); | ||
|
||
const octokit = new Octokit({ | ||
auth: process.env.GITHUB_TOKEN, | ||
}); | ||
|
||
const screenshot_files = []; | ||
for (const file of files.filter((s) => s.endsWith(".md"))) { | ||
console.log("File: " + file); | ||
const filename = file.endsWith("index.md") | ||
? file.slice(0, -8) | ||
: file.endsWith(".md") | ||
? file.slice(0, -3) | ||
: file; | ||
const url = "http://localhost:3000/" + filename; | ||
console.log("URL: " + url); | ||
await page.goto(url, { waitUntil: "domcontentloaded" }); | ||
await scrollFullPage(page); | ||
await page.evaluate(() => { | ||
window.scrollBy(0, -document.body.scrollHeight); | ||
}); | ||
await page.screenshot({ | ||
path: "screenshots/" + file + ".png", | ||
fullPage: true, | ||
}); | ||
screenshot_files.push(file); | ||
} | ||
|
||
const screenshot_urls = []; | ||
if (IMGUR_CLIENT_ID) { | ||
const uploader = new ImgurAnonymousUploader(IMGUR_CLIENT_ID); | ||
for (const screenshot_file of screenshot_files) { | ||
const result = await uploader.upload( | ||
"screenshots/" + screenshot_file + ".png" | ||
); | ||
if (!result.success) { | ||
console.error("Error: " + result); | ||
continue; | ||
} | ||
console.log( | ||
"Imgur uploaded: " + result.url + " (" + result.deleteHash + ")" | ||
); | ||
screenshot_urls.push({ | ||
url: result.url, | ||
file: screenshot_file, | ||
}); | ||
} | ||
} | ||
|
||
const images = screenshot_urls.map( | ||
(o) => | ||
`<details>\n<summary>${o.file}</summary>\n\n## ${o.file} \n\n![${o.file}](${o.url})\n</details>\n` | ||
); | ||
const comments = await octokit.issues.listComments({ | ||
owner: OWNER, | ||
repo: REPO, | ||
issue_number: ISSUE_NUMBER, | ||
per_page: 100, | ||
}); | ||
for (const comment of comments.data) { | ||
if (!comment.body.trim().startsWith("# Page Preview Images")) { | ||
continue; | ||
} | ||
await octokit.issues.deleteComment({ | ||
owner: OWNER, | ||
repo: REPO, | ||
comment_id: comment.id, | ||
}); | ||
console.log("Deleted comment: " + comment.id); | ||
} | ||
await octokit.issues.createComment({ | ||
owner: OWNER, | ||
repo: REPO, | ||
issue_number: ISSUE_NUMBER, | ||
body: ` | ||
# Page Preview Images | ||
${images.join("\n")} | ||
`, | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"dependencies": { | ||
"@octokit/rest": "^18.12.0", | ||
"@playwright/test": "^1.16.3", | ||
"playwright": "^1.16.3", | ||
"imgur-anonymous-uploader": "^1.1.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { PlaywrightTestConfig } from "@playwright/test"; | ||
const config: PlaywrightTestConfig = { | ||
webServer: { | ||
command: "yarn start", | ||
cwd: process.env.GITHUB_WORKSPACE, | ||
port: 3000, | ||
}, | ||
timeout: 0 | ||
}; | ||
export default config; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules | ||
yarn.lock | ||
markdown-header.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import axios from "axios"; | ||
import * as crypto from "crypto"; | ||
import * as fs from "fs"; | ||
import * as matter from "gray-matter"; | ||
import * as path from "path"; | ||
import { exit } from "process"; | ||
|
||
interface FileDetails { | ||
filePath: string; | ||
fields: { | ||
[key: string]: any; | ||
}; | ||
oldFile: string | null; | ||
isChanged: boolean; | ||
} | ||
|
||
function getGrayMatter(content: string): { | ||
data: { | ||
[key: string]: any; | ||
}; | ||
} { | ||
// @ts-ignore | ||
return matter(content); | ||
} | ||
|
||
(async () => { | ||
function getAllFiles(searchPath: string) { | ||
const files = fs.readdirSync(searchPath); | ||
const results: string[] = []; | ||
|
||
files.forEach((file) => { | ||
const fullPath = path.normalize(searchPath + "/" + file); | ||
const stat = fs.statSync(fullPath); | ||
|
||
if (stat.isDirectory()) { | ||
results.push(...getAllFiles(fullPath)); | ||
} else { | ||
results.push(fullPath); | ||
} | ||
}); | ||
return results; | ||
} | ||
|
||
function getHeaderFields(filePath: string): { | ||
[key: string]: any; | ||
} { | ||
const content = fs.readFileSync(filePath, "utf8"); | ||
const { data } = getGrayMatter(content); | ||
return data; | ||
} | ||
|
||
async function getHTTPContent(url: string): Promise<any | null> { | ||
try { | ||
const response = await axios.get(url); | ||
if (response.status !== 200) { | ||
return null; | ||
} | ||
return response.data; | ||
} catch (error) { | ||
console.warn(error); | ||
return null; | ||
} | ||
} | ||
|
||
const files = getAllFiles(".") | ||
.filter((x) => x.endsWith(".md")) | ||
.filter((x) => !x.includes("node_modules")) | ||
.filter((x) => !x.includes("README.md")); | ||
|
||
const requiredFields = { | ||
blog: ["title", "category", "author", "createdAt", "updatedAt"], | ||
other: ["title", "description", "createdAt", "updatedAt"], | ||
}; | ||
|
||
async function forBlog( | ||
fileDetails: FileDetails, | ||
authors: [{ [key: string]: any }], | ||
categories: [{ [key: string]: any }] | ||
) { | ||
const file = fileDetails.filePath; | ||
const oldFile = fileDetails.oldFile; | ||
const fields = fileDetails.fields; | ||
const isChanged = fileDetails.isChanged; | ||
|
||
// 執筆者の確認 | ||
const author = fields["author"]; | ||
if (authors.findIndex((x) => x.slug === author) === -1) { | ||
console.warn(`${file}: 執筆者が正しくありません (${author})`); | ||
return false; | ||
} | ||
|
||
// カテゴリーの確認 | ||
const category = fields["category"]; | ||
if (categories.findIndex((x) => x.slug === category) === -1) { | ||
console.warn(`${file}: カテゴリが正しくありません (${category})`); | ||
return false; | ||
} | ||
|
||
if (oldFile !== null && isChanged) { | ||
const oldFields = getGrayMatter(oldFile).data; | ||
if (oldFields["updatedAt"] == fields["updatedAt"]) { | ||
console.warn( | ||
`${file}: 更新日時が変更されていません (${fields["updatedAt"]})` | ||
); | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
async function forOther(fileDetails: FileDetails) { | ||
const file = fileDetails.filePath; | ||
const oldFile = fileDetails.oldFile; | ||
const fields = fileDetails.fields; | ||
const isChanged = fileDetails.isChanged; | ||
|
||
if (oldFile !== null && isChanged) { | ||
const oldFields = getGrayMatter(oldFile).data; | ||
if (oldFields["updatedAt"].toString() == fields["updatedAt"].toString()) { | ||
console.warn( | ||
`${file}: 更新日時が変更されていません (${fields["updatedAt"]})` | ||
); | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
const authors = await getHTTPContent( | ||
"https://raw.githubusercontent.com/jaoafa/jaoweb/master/content/blog/authors.json" | ||
); | ||
const categories = await getHTTPContent( | ||
"https://raw.githubusercontent.com/jaoafa/jaoweb/master/content/blog/categories.json" | ||
); | ||
|
||
let isValid = true; | ||
// %f: %m | ||
for (const file of files) { | ||
const fields = getHeaderFields(file); | ||
const fileType = file.includes("blog") ? "blog" : "other"; | ||
const requiredFieldsForFile = requiredFields[fileType]; | ||
for (const field of requiredFieldsForFile) { | ||
if (!fields[field]) { | ||
console.warn(`${file}: 必要なフィールド ${field} がありません。`); | ||
isValid = false; | ||
} | ||
} | ||
|
||
const oldFile = await getHTTPContent( | ||
`https://raw.githubusercontent.com/jaoafa/jaoweb-docs/main/${file.replace( | ||
/\\/g, | ||
"/" | ||
)}` | ||
); | ||
const content = fs.readFileSync(file, "utf8"); | ||
|
||
const fileDetails: FileDetails = { | ||
filePath: file, | ||
fields: fields, | ||
oldFile: oldFile, | ||
isChanged: | ||
crypto.createHash("sha256").update(oldFile).digest("hex") != | ||
crypto.createHash("sha256").update(content).digest("hex"), | ||
}; | ||
|
||
if (fileType == "blog") { | ||
const vaild = await forBlog(fileDetails, authors, categories); | ||
if (!vaild) isValid = false; | ||
} else { | ||
const vaild = await forOther(fileDetails); | ||
if (!vaild) isValid = false; | ||
} | ||
} | ||
|
||
process.exitCode = isValid ? 0 : 1; | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"devDependencies": { | ||
"axios": "^0.24.0", | ||
"gray-matter": "^4.0.3", | ||
"typescript": "^4.4.4", | ||
"@types/node": "^16.11.6" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "ES2018", | ||
"module": "ESNext", | ||
"moduleResolution": "Node", | ||
"lib": ["ESNext", "ESNext.AsyncIterable", "DOM"], | ||
"esModuleInterop": true, | ||
"allowJs": true, | ||
"sourceMap": true, | ||
"strict": true, | ||
"noEmit": true, | ||
"experimentalDecorators": true, | ||
"baseUrl": "." | ||
}, | ||
"exclude": ["node_modules"] | ||
} |
Oops, something went wrong.