Skip to content

Commit 7df972f

Browse files
authored
ci: move scripts to a cli (#237)
1 parent 3bdf55f commit 7df972f

32 files changed

+543
-353
lines changed

β€Ž.github/workflows/ci.ymlβ€Ž

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,27 @@ jobs:
8080

8181
- name: Build project
8282
run: pnpm run build
83+
84+
check:
85+
name: Check Content
86+
runs-on: ubuntu-latest
87+
88+
steps:
89+
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
90+
- run: corepack enable
91+
- uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4
92+
with:
93+
node-version: 20.5
94+
cache: pnpm
95+
96+
- name: Install dependencies
97+
run: pnpm install
98+
99+
- name: Check puzzle parts
100+
run: pnpm run cli check puzzle
101+
102+
- name: Check logos
103+
run: pnpm run cli check logos
104+
105+
- name: Check Package Redirects
106+
run: pnpm run cli check packages-redirects

β€Ž.github/workflows/generate-monthly-updates.ymlβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
- name: Install dependencies
2929
run: pnpm install
3030

31-
- run: pnpm jiti scripts/releases-article.ts
31+
- run: pnpm run cli generate releases-article
3232

3333
- name: Commit
3434
run: |
@@ -61,7 +61,7 @@ jobs:
6161
title: 'content: create monthly updates',
6262
head: 'monthly-updates',
6363
base: 'main',
64-
body: `- [ ] format the article by creating paragraphs\n\nThis PR is automatically generated by a script._`,
64+
body: `- [ ] format the article by creating paragraphs`,
6565
labels: ['content'],
6666
})
6767
}

β€Ž.github/workflows/update-packages.ymlβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ jobs:
2828
- name: Install dependencies
2929
run: pnpm install
3030

31-
- run: pnpm jiti scripts/update-packages.ts
31+
- run: pnpm run cli github sync-packages
3232
env:
3333
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

β€Žbin/commands/check.tsβ€Ž

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineCommand } from 'citty'
2+
3+
export const check = defineCommand({
4+
meta: {
5+
name: 'check',
6+
},
7+
subCommands: {
8+
'puzzle': () => import('./check/puzzle').then(m => m.puzzle),
9+
'logos': () => import('./check/logos').then(m => m.logos),
10+
'packages-redirects': () => import('./check/packages-redirects').then(m => m.packagesRedirects),
11+
},
12+
})

β€Žbin/commands/check/logos.tsβ€Ž

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { exit } from 'node:process'
2+
import { defineCommand } from 'citty'
3+
import { consola } from 'consola'
4+
import { getPackages } from '../../utils/content'
5+
import { getLogos } from '../../utils/public'
6+
import { formatTree } from '../../utils/format'
7+
8+
export const logos = defineCommand({
9+
meta: {
10+
name: 'Logos',
11+
},
12+
run() {
13+
const packages = getPackages()
14+
const logos = getLogos()
15+
16+
// Check if each package have a logo
17+
const packagesWithoutLogo = packages.filter(name => !logos.includes(name))
18+
19+
if (packagesWithoutLogo.length)
20+
consola.error(`A logo is needed:\n${formatTree(packagesWithoutLogo)}`)
21+
22+
// Check if each logo have a package
23+
const logosWithoutPackage = logos.filter(name => !packages.includes(name))
24+
25+
if (logosWithoutPackage.length)
26+
consola.error(`A package is needed:\n${formatTree(logosWithoutPackage)}`)
27+
28+
if (packagesWithoutLogo.length || logosWithoutPackage.length)
29+
return exit(1)
30+
31+
return exit(0)
32+
},
33+
})
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { exit } from 'node:process'
2+
import { defineCommand } from 'citty'
3+
import { getPackages } from '../../utils/content'
4+
import { getPackagesRedirectsPath } from '../../utils/config'
5+
import { formatTree } from '../../utils/format'
6+
7+
export const packagesRedirects = defineCommand({
8+
meta: {
9+
name: 'Packages Redirects',
10+
description: 'Check that all packages have a redirect rule',
11+
},
12+
async run() {
13+
const packages = getPackages()
14+
const redirects = await import(getPackagesRedirectsPath()).then(m => m.default)
15+
16+
const redirectsKeys = Object.keys(redirects).map(name => name.replace('/', ''))
17+
18+
// Check if each package have a redirect rule
19+
const missingRedirects = packages.filter(name => !redirectsKeys.includes(name))
20+
21+
if (missingRedirects.length)
22+
console.error(`Missing redirects:\n${formatTree(missingRedirects)}`)
23+
24+
// Check if each redirect rule have a package
25+
const missingPackages = redirectsKeys.filter(name => !packages.includes(name))
26+
27+
if (missingPackages.length)
28+
console.error(`Missing packages:\n${formatTree(missingPackages)}`)
29+
30+
if (missingRedirects.length || missingPackages.length)
31+
return exit(1)
32+
33+
return exit(0)
34+
},
35+
})

β€Žbin/commands/check/puzzle.tsβ€Ž

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { exit } from 'node:process'
2+
import { defineCommand } from 'citty'
3+
import { consola } from 'consola'
4+
import { getPackages } from '../../utils/content'
5+
import { getPuzzleParts } from '../../utils/public'
6+
import { formatTree } from '../../utils/format'
7+
8+
export const puzzle = defineCommand({
9+
meta: {
10+
name: 'puzzle',
11+
},
12+
run: () => {
13+
const packages = getPackages()
14+
const puzzleParts = getPuzzleParts()
15+
16+
// Check if each package have a puzzle part (light and dark)
17+
const packagesWithoutLightPuzzlePart = packages.filter(name => !puzzleParts.light.includes(name))
18+
19+
if (packagesWithoutLightPuzzlePart.length)
20+
consola.error(`A light puzzle part is needed:\n${formatTree(packagesWithoutLightPuzzlePart)}`)
21+
22+
const packagesWithoutDarkPuzzlePart = packages.filter(name => !puzzleParts.dark.includes(name))
23+
24+
if (packagesWithoutDarkPuzzlePart.length)
25+
consola.error(`A dark puzzle part is needed:\n${formatTree(packagesWithoutDarkPuzzlePart)}`)
26+
27+
// Check if each puzzle part (light and dark) have a package
28+
const lightPuzzlePartsWithoutPackage = puzzleParts.light.filter(name => !packages.includes(name))
29+
30+
if (lightPuzzlePartsWithoutPackage.length)
31+
consola.error(`A package (from light) is needed:\n${formatTree(lightPuzzlePartsWithoutPackage)}`)
32+
33+
const darkPuzzlePartsWithoutPackage = puzzleParts.dark.filter(name => !packages.includes(name))
34+
35+
if (darkPuzzlePartsWithoutPackage.length)
36+
consola.error(`A package (from dark) is needed:\n${formatTree(darkPuzzlePartsWithoutPackage)}`)
37+
38+
if (packagesWithoutLightPuzzlePart.length || packagesWithoutDarkPuzzlePart.length || lightPuzzlePartsWithoutPackage.length || darkPuzzlePartsWithoutPackage.length)
39+
return exit(1)
40+
41+
return exit(0)
42+
},
43+
})

β€Žbin/commands/generate.tsβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineCommand } from 'citty'
2+
3+
export const generate = defineCommand({
4+
meta: {
5+
name: 'Generate',
6+
},
7+
subCommands: {
8+
'releases-article': () => import('./generate/releases-article').then(m => m.releasesArticle),
9+
},
10+
})
Lines changed: 73 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,91 @@
1-
import { exit } from 'node:process'
2-
import fs from 'node:fs'
3-
import { consola } from 'consola'
4-
import { fetchReleases, fetchRepos } from './utils/github'
5-
import type { GithubRelease } from './types'
6-
7-
/**
8-
* This script is used to fetch all releases from UnJS packages to create a draft for an article to publish on our blog.
9-
*/
10-
async function main() {
11-
const repos = await fetchRepos()
12-
13-
const releases = await fetchReleases(repos)
14-
15-
const today = new Date()
16-
const currentYear = today.getFullYear()
17-
const currentMonth = today.getMonth() + 1
18-
// Filter releases to only keep the ones from the current year and month
19-
const releasesFromCurrentMonth = releases.reduce((acc, releases) => {
20-
const filtered = releases.releases.filter((release) => {
21-
const releaseDate = new Date(release.publishedAt)
22-
const releaseYear = releaseDate.getFullYear()
23-
const releaseMonth = releaseDate.getMonth() + 1
24-
25-
return releaseYear === currentYear && releaseMonth === currentMonth && release.prerelease === false && release.draft === false
26-
})
27-
28-
if (filtered.length > 0) {
29-
acc.push({
30-
...releases,
31-
releases: filtered,
32-
})
33-
}
34-
35-
return acc
36-
}, [] as { name: string, releases: GithubRelease[] }[]).sort((a, b) => a.name.localeCompare(b.name))
37-
38-
const numberOfReleases = releasesFromCurrentMonth.reduce((acc, releases) => acc + releases.releases.length, 0)
39-
40-
// Create the article (draft)
41-
const currentMonthName = today.toLocaleString('default', { month: 'long' })
42-
const currentDay = today.getDate()
43-
const filename = `${currentYear}-${currentMonth}-${currentDay}-${currentMonthName.toLocaleLowerCase()}-monthly-updates.md`
44-
const title = `Monthly updates (${currentMonthName} ${currentYear})`
1+
import { writeFileSync } from 'node:fs'
2+
import { defineCommand } from 'citty'
3+
import { fetchReleases, fetchRepos } from '../../utils/github'
4+
import { getCurrentMonth, getCurrentYear } from '../../utils/date'
5+
import { getBlogPath } from '../../utils/content'
6+
import type { GithubRelease } from '../../types'
7+
8+
export const releasesArticle = defineCommand({
9+
meta: {
10+
name: 'releases-article',
11+
description: 'Generate an article with all releases of the current month',
12+
},
13+
async run() {
14+
const blogPath = getBlogPath()
15+
16+
const currentMonth = getCurrentMonth()
17+
const currentYear = getCurrentYear()
18+
19+
/**
20+
* Retrieve correct data
21+
*/
22+
23+
const repos = await fetchRepos()
24+
const releases = await fetchReleases(repos)
25+
26+
const currentReleases = releases.reduce((acc, _releases) => {
27+
const filtered = getMonthReleases(_releases.releases, currentYear, currentMonth)
28+
29+
if (filtered.length) {
30+
acc.push({
31+
..._releases,
32+
releases: filtered,
33+
})
34+
}
4535

46-
const article = /* md */`---
36+
return acc
37+
}, [] as { name: string, releases: GithubRelease[] }[]).sort((a, b) => a.name.localeCompare(b.name))
38+
39+
const numberOfReleases = currentReleases.reduce((acc, releases) => acc + releases.releases.length, 0)
40+
41+
/**
42+
* Create the article
43+
*/
44+
const today = new Date()
45+
const currentMonthName = today.toLocaleString('default', { month: 'long' })
46+
const currentDay = today.getDate()
47+
const filename = `${currentYear}-${currentMonth.toLocaleString('en-US', {
48+
minimumIntegerDigits: 2,
49+
useGrouping: false,
50+
})}-${currentDay.toLocaleString('en-US', {
51+
minimumIntegerDigits: 2,
52+
useGrouping: false,
53+
})}-${currentMonthName.toLocaleLowerCase()}-monthly-updates.md`
54+
const title = `Monthly updates (${currentMonthName} ${currentYear})`
55+
const article = /* md */`---
4756
title: ${title}
4857
description: ${numberOfReleases} releases this month! What's new in the UnJS ecosystem?
49-
image:
50-
src:
51-
alt:
5258
authors:
5359
- name:
5460
picture:
5561
twitter:
5662
category:
5763
- releases
5864
packages:
59-
- ${releasesFromCurrentMonth.map(releases => releases.name).join('\n - ')}
65+
- ${currentReleases.map(releases => releases.name).join('\n - ')}
6066
publishedAt: ${today.toISOString()}
6167
modifiedAt: ${today.toISOString()}
62-
layout: blog-post
6368
---
6469
65-
${releasesFromCurrentMonth.map(releases => `## ${releases.name}\n\nThis month, we release ${releases.releases.length} new ${releases.releases.length > 1 ? 'releases' : 'release'} (${logReleasesTypes(releases.releases)}):\n\n${releases.releases.map(release => `- [${release.tag}](https://github.com/unjs/${releases.name}/releases/tag/${release.tag})`).join('\n')}\n\n${formatReleasesMarkdown(releases.releases)}`).join('\n\n')}`
70+
${currentReleases.map(releases => `## ${releases.name}\n\nThis month, we release ${releases.releases.length} new ${releases.releases.length > 1 ? 'releases' : 'release'} (${logReleasesTypes(releases.releases)}):\n\n${releases.releases.map(release => `- [${release.tag}](https://github.com/unjs/${releases.name}/releases/tag/${release.tag})`).join('\n')}\n\n${formatReleasesMarkdown(releases.releases)}`).join('\n\n')}`
6671

67-
fs.writeFileSync(`./content/5.blog/${filename}`, article)
68-
}
72+
// An article is a list of repo where each repo have a list releases and then, the changelog of each release.
73+
writeFileSync(`${blogPath}/${filename}`, article)
74+
},
75+
})
6976

70-
main().then(() => exit(0)).catch(consola.error)
77+
/**
78+
* Extract releases from a specific month and year
79+
*/
80+
function getMonthReleases(releases: GithubRelease[], year: number, month: number): GithubRelease[] {
81+
return releases.filter((release) => {
82+
const releaseDate = new Date(release.publishedAt)
83+
const releaseYear = releaseDate.getFullYear()
84+
const releaseMonth = releaseDate.getMonth() + 1
85+
86+
return releaseYear === year && releaseMonth === month && release.prerelease === false && release.draft === false
87+
})
88+
}
7189

7290
function logReleasesTypes(releases: GithubRelease[]): string {
7391
const majorReleases = releases.filter(release => release.tag.endsWith('.0.0'))
@@ -154,7 +172,7 @@ function formatLine(line: string): string {
154172
* Standardize title using h3, removing emoji like πŸš€ and lowercase
155173
*/
156174
function formatTitle(line: string): string {
157-
return `### ${line.replace(/#+\s*/, '').replace(/[πŸš€πŸš¨πŸžπŸ©ΉπŸ“–]/u, '').toLowerCase().trim()}`
175+
return `### ${line.replace(/#+\s*/, '').replace(/[πŸš€πŸš¨πŸžπŸ©ΉπŸ“–πŸ’…πŸŒŠ]/u, '').toLowerCase().trim()}`
158176
}
159177

160178
/**

β€Žbin/commands/github.tsβ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineCommand } from 'citty'
2+
3+
export const github = defineCommand({
4+
meta: {
5+
name: 'github',
6+
description: 'Command that will impact GitHub',
7+
},
8+
subCommands: {
9+
'sync-packages': () => import('./github/sync-packages').then(m => m.syncPackages),
10+
},
11+
})

0 commit comments

Comments
Β (0)