diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e175fcd..f262499 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: build: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version-file: .node-version @@ -28,7 +28,7 @@ jobs: lint: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version-file: .node-version @@ -39,7 +39,7 @@ jobs: typecheck: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version-file: .node-version @@ -52,13 +52,15 @@ jobs: needs: [build, lint, typecheck] runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version-file: .node-version cache: npm - run: npm ci - - run: git diff origin/main..HEAD --name-only | xargs npm run validate-blog -- + - run: | + git fetch origin main + git diff origin/main..HEAD --name-only | xargs npm run validate-blog -- update-blogmeta: needs: [validate-blog] @@ -66,28 +68,30 @@ jobs: contents: write runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + # Checkout pull request HEAD commit instead of merge commit + with: + ref: ${{ github.event.pull_request.head.ref }} - uses: actions/setup-node@v3 with: node-version-file: .node-version cache: npm - run: npm ci - - name: Update blog meta - id: update_blogmeta + - name: Update Blog Meta run: | - git diff origin/main..HEAD --name-only | xargs npm run update-blogmeta -- - echo "{UPDATE_RESULT}_{$?}" >> "$GITHUB_OUTPUT" - continue-on-error: true - - run: exit ${{ steps.update_blogmeta.outputs.UPDATE_RESULT }} - if: steps.update_blogmeta.outputs.UPDATE_RESULT != '1' && steps.update_blogmeta.outputs.UPDATE_RESULT != '0' + git fetch origin main + git diff origin/main..HEAD --name-status | xargs npm run update-blogmeta -- - name: Git Commit run: | - git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add . - git commit -m "[Bot] Update blog meta" - git push origin ${{github.head_ref}} - if: steps.update_blogmeta.outputs.UPDATE_RESULT == '0' + git add -N . + if ! git diff --exit-code --quiet + then + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add . + git commit -m "[Bot] Update Blog Meta" + git push + fi deploy: if: github.ref == 'refs/heads/main' diff --git a/tools/update-blogmeta.ts b/tools/update-blogmeta.ts index fea9a63..9c96cd1 100644 --- a/tools/update-blogmeta.ts +++ b/tools/update-blogmeta.ts @@ -1,46 +1,95 @@ import * as fs from 'fs' import * as path from 'path' -const ok = validateExistingFiles() -if (!ok) process.exit(2) -updateBlogMeta() +type FileStatus = + | [status: 'A' | 'C' | 'D' | 'M' | 'T', path: string] + | [status: `R${number}`, fromPath: string, toPath: string] type BlogMeta = { postDate: string updateDate?: string } +const gitDiffs = validateArgs(chunkArray(process.argv.slice(2), 2)) +if (gitDiffs === null) process.exit(1) +updateBlogMeta(gitDiffs) -function validateExistingFiles() { +function validateArgs(gitDiffs: string[][]): FileStatus[] | null { let ok = true - for (const file of process.argv.slice(2)) { - if (!fs.existsSync(file) || !fs.statSync(file).isFile()) { - console.log(`\u001b[31m[ERROR]\tThe file was not found: ${file}\u001b[m`) + function validateFile(path: string) { + if (!fs.existsSync(path) || !fs.statSync(path).isFile()) { + console.log(`\u001b[31m[ERROR]\tThe file was not found: ${path}\u001b[m`) ok = false + return false } + return true } - return ok + + const result: FileStatus[] = [] + + for (const status of gitDiffs) { + if (status.length != 2 && status.length != 3) { + console.log( + `\u001b[31m[ERROR]\tArgument is invalid format.\n Please set results of \`git diff --name-status\`. \u001b[m`, + ) + ok = false + continue + } + + if (status.length === 2) { + if (!['A', 'C', 'D', 'M', 'T'].includes(status[0]!)) { + console.log( + `\u001b[33m[WARNING]\tThe status does not supported: ${status[0]}\t${status[1]}\u001b[m`, + ) + continue + } + + if (status[0] === 'D' || validateFile(status[1]!)) { + result.push(status as ['A' | 'C' | 'D' | 'M' | 'T', string]) + } + } else if (status.length === 3) { + if ( + status[0]!.startsWith('R') && + Number.isInteger(Number(status[0]!.slice(1))) + ) { + if (validateFile(status[1]!) && validateFile(status[2]!)) { + result.push(status as [`R${number}`, string, string]) + } + } else + console.log( + `\u001b[33m[WARNING]\tThe status does not supported: ${status[0]}\t${status[1]}\u001b[m`, + ) + } + } + return ok ? result : null } /** * blog meta のファイルの日付を更新します */ -function updateBlogMeta() { - const targetFiles = process.argv - .slice(2) - .map(path.parse) - .filter( - (parsedPath) => - parsedPath.dir === 'src/content/blogs' && parsedPath.ext === '.md', +function updateBlogMeta(gitDiffs: readonly FileStatus[]) { + const targetFiles = gitDiffs + .map(([status, filePath, toPath]) => + toPath === undefined + ? ([status, path.parse(filePath)] as const) + : ([status, path.parse(toPath)] as const), ) - if (targetFiles.length === 0) { - console.log( - '\u001b[31m[ERROR]\tSet more than one target files to args.\u001b[m', - ) - process.exit(1) - } + .filter(predicateTargetFiles) + const updatedMetas = gitDiffs + .map(([status, filePath, toPath]) => [status, toPath ?? filePath] as const) + .filter(([status, filePath]) => { + const { dir, ext } = path.parse(filePath) + return ( + status !== 'D' && dir === 'src/content/blog-metas' && ext === '.json' + ) + }) + + for (const [_, { dir, name, ext }] of targetFiles) { + const updatedName = + ext === '.md' || ext === '.mdx' + ? name + : dir.slice('src/content/blogs/'.length).split('/')[0]! - for (const { dir, name } of targetFiles) { - const blogMetaPath = path.resolve(dir, `./blog-meta/${name}.json`) + const blogMetaPath = `src/content/blog-metas/${updatedName}.json` let meta: BlogMeta | null = null if (fs.existsSync(blogMetaPath)) { @@ -48,15 +97,75 @@ function updateBlogMeta() { fs.readFileSync(blogMetaPath, { encoding: 'utf-8' }), ) as BlogMeta - meta.updateDate = new Date().toISOString() + if (updatedMetas.every(([_, filePath]) => filePath !== blogMetaPath)) + meta.updateDate = new Date().toISOString() + else { + // 今回のPRで既に更新済み + + if (meta.updateDate) meta.updateDate = new Date().toISOString() + else meta.postDate = new Date().toISOString() + } } else { meta = { postDate: new Date().toISOString(), } } - fs.writeFileSync(blogMetaPath, JSON.stringify(meta, undefined, 2)) + fs.writeFileSync(blogMetaPath, JSON.stringify(meta, undefined, 2) + '\n') console.log(`[INFO]\tUpdate blog meta: ${blogMetaPath}`) } } + +function predicateTargetFiles([status, file]: readonly [ + 'A' | 'C' | 'D' | 'M' | 'T' | `R${number}`, + path.ParsedPath, +]): boolean { + // markdown + if ( + (['A', 'C', 'M', 'T'].includes(status) || status.startsWith('R')) && + file.dir === 'src/content/blogs' && + (file.ext === '.md' || file.ext === '.mdx') + ) + return true + + // 画像 + if ( + (['A', 'C', 'D', 'M', 'T'].includes(status) || status.startsWith('R')) && + file.dir.startsWith('src/content/blogs/') && + [ + '.jpg', + '.jpeg', + '.jfif', + '.pjpeg', + '.pjp', + '.png', + '.svg', + '.webp', + '.gif', + '.avif', + '.apng', + ].includes(file.ext) + ) + return true + + return false +} + +function chunkArray(array: T[], chunkSize: number) { + const result: T[][] = [] + let tempArray: T[] = [] + for (const val of array) { + if (tempArray.length < chunkSize) { + tempArray.push(val) + } else { + result.push(tempArray) + tempArray = [val] + } + } + + if (tempArray.length !== 0) { + result.push(tempArray) + } + return result +} diff --git a/tools/validate-blog.ts b/tools/validate-blog.ts index 0232fda..737ac36 100644 --- a/tools/validate-blog.ts +++ b/tools/validate-blog.ts @@ -6,12 +6,13 @@ process.exit(validateBlog() ? 0 : 1) * 変更されたファイルが許可されているものに含まれているか判定します * * 許可されているファイルは以下のとおりです。 - * - src/content/blogs/*.md (blogs, tags, authors 以外) + * - src/content/blogs/*.{md, mdx} * - src/content/blogs/ より一階層下にある画像 * - src/content/authors/*.json - * - src/content/blog-metas/*.json (blogs, tags, authors 以外) + * - src/content/authors/ にある画像 + * - src/content/blog-metas/*.json * - src/content/tags/*.json - * - src/assets/icons/blog/*.svg + * - src/content/tags/ にある画像 */ function validateBlog() { let ok = true @@ -21,48 +22,20 @@ function validateBlog() { if (parsedPath.dir === 'src/content/blogs') { // ブログ本体 - if ( - parsedPath.ext === '.md' && - !['blogs', 'tags', 'authors'].includes(parsedPath.name) - ) - continue + if (parsedPath.ext === '.md' || parsedPath.ext === '.mdx') continue } else if (parsedPath.dir === 'src/content/blog-metas') { // ブログのメタ情報 - if ( - parsedPath.ext === '.json' && - !['blogs', 'tags', 'authors'].includes(parsedPath.name) - ) - continue - } else if (parsedPath.dir.startsWith('src/content/blogs/')) { - // 画像 - if ( - [ - '.jpg', - '.jpeg', - '.jfif', - '.pjpeg', - '.pjp', - '.png', - '.svg', - '.webp', - '.gif', - '.avif', - '.apng', - ].includes(parsedPath.ext) - ) - continue + if (parsedPath.ext === '.json') continue } else if ( - // ブログの著者・タグ - ['src/content/authors', 'src/content/tags'].includes(parsedPath.dir) && - parsedPath.ext === '.json' + parsedPath.dir.match(/^src\/content\/blogs\/[^(#/)]*\/[^(#/)]*$/) ) { - continue + // 画像 + if (isImage(parsedPath.ext)) continue } else if ( - // ブログのアイコン - parsedPath.dir === 'src/assets/icons/blog' && - parsedPath.ext === '.svg' + ['src/content/authors', 'src/content/tags'].includes(parsedPath.dir) ) { - continue + // ブログの著者・タグ + if (parsedPath.ext === '.json' || isImage(parsedPath.ext)) continue } console.log( @@ -73,3 +46,19 @@ function validateBlog() { return ok } + +function isImage(ext: string) { + return [ + '.jpg', + '.jpeg', + '.jfif', + '.pjpeg', + '.pjp', + '.png', + '.svg', + '.webp', + '.gif', + '.avif', + '.apng', + ].includes(ext) +}