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
1 change: 0 additions & 1 deletion .github/workflows/generator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ jobs:
id: auto-commit
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_options: '--no-verify'
commit_message: 'chore: auto-generate data and og images'

- name: Comment on PR
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/slug-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Slug Check

on:
pull_request:
branches: [main]

jobs:
slug-check:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: pnpm

- run: pnpm install --frozen-lockfile
- run: pnpm check:slugs
Comment on lines +9 to +25

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 2 months ago

To fix the problem, we should explicitly restrict the GITHUB_TOKEN permissions for this workflow to the minimum needed. The job only checks code (checkout, install, run pnpm check:slugs) and does not need to write to the repository or modify issues, PRs, or other resources. The minimal appropriate permission is contents: read. We can declare this either at the workflow root (applies to all jobs) or within the specific slug-check job. Since there is only one job, either is fine; adding it at the workflow root is slightly clearer and follows the recommendation pattern.

Concretely, in .github/workflows/slug-check.yml, add a permissions: block with contents: read (and nothing else) near the top of the file, at the root level, alongside name and on. This will ensure the GITHUB_TOKEN used by actions/checkout is limited to read-only access to repository contents, satisfying the CodeQL rule without changing any existing functionality of the workflow.

Suggested changeset 1
.github/workflows/slug-check.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/slug-check.yml b/.github/workflows/slug-check.yml
--- a/.github/workflows/slug-check.yml
+++ b/.github/workflows/slug-check.yml
@@ -1,5 +1,8 @@
 name: Slug Check
 
+permissions:
+    contents: read
+
 on:
     pull_request:
         branches: [main]
EOF
@@ -1,5 +1,8 @@
name: Slug Check

permissions:
contents: read

on:
pull_request:
branches: [main]
Copilot is powered by AI and may make mistakes. Always verify output.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"prepare": "husky",
"check": "astro check",
"generate:og": "node scripts/generate-og.mjs",
"generate:data": "node scripts/update-data.mjs"
"generate:data": "node scripts/update-data.mjs",
"check:slugs": "node scripts/check-slugs.mjs"
},
"dependencies": {
"@astrojs/mdx": "^5.0.2",
Expand Down
71 changes: 71 additions & 0 deletions scripts/check-slugs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env node
import { promises as fs } from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { SUPPORTED_LANGS, LANGUAGES } from '../src/config.mjs'

const RESERVED_SLUGS = {
tr: new Set(['articles', 'categories', 'tags', '404']),
en: new Set(['en', 'en/articles', 'en/categories', 'en/tags', 'en/404']),
}

let hasErrors = false

const __dirname = new URL('.', import.meta.url).pathname
const repoRoot = path.resolve(__dirname, '..')

;(async () => {
for (const lang of SUPPORTED_LANGS) {
const contentDir = path.join(repoRoot, 'src/content/articles', lang)
console.log(`Checking slugs for language: ${lang}`)

let mdFiles
try {
mdFiles = (await fs.readdir(contentDir)).filter((f) => f.endsWith('.md') || f.endsWith('.mdx'))
} catch {
console.log(` No content directory for language '${lang}': ${contentDir}`)
console.log(` Skipping.`)
continue
}

const slugMap = new Map()
const reserved = RESERVED_SLUGS[lang] || new Set()
const langPrefix = LANGUAGES[lang]?.prefix || ''

for (const file of mdFiles) {
const filePath = path.join(contentDir, file)
const raw = await fs.readFile(filePath, 'utf8')
const { data } = matter(raw)

const slug = data.slug?.trim()
if (!slug) {
console.log(` ✗ ${file} — MISSING slug field in frontmatter`)
hasErrors = true
continue
}

if (reserved.has(slug) || (langPrefix && slug.startsWith(langPrefix.slice(1) + '/'))) {
console.log(` ✗ ${file} — RESERVED slug '${slug}' (conflicts with static route)`)
hasErrors = true
continue
}

if (slugMap.has(slug)) {
const original = slugMap.get(slug)
console.log(` ✗ ${file} — DUPLICATE slug '${slug}' (already used by ${original})`)
hasErrors = true
} else {
slugMap.set(slug, file)
console.log(` ✓ ${slug} — slug OK`)
}
}
}

if (hasErrors) {
console.log('\nFound slug errors. Exiting with code 1.')
process.exit(1)
} else {
console.log('\nAll slugs are valid.')
process.exit(0)
}
})()
Loading