Skip to content

Commit 5c0a483

Browse files
committed
ci: refactor generate-docs process
Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
1 parent 33b3f2b commit 5c0a483

30 files changed

Lines changed: 1208 additions & 1171 deletions

.env

Whitespace-only changes.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<!-- header:start -->
2+
3+
# GitHub Action: Generate Documentation
4+
5+
<div align="center">
6+
<img src="https://opengraph.githubassets.com/ed19fc749543a82f50dbc89c5b6e8bba6ee7cd408bad3a0d6748179b2681f1ec/hoverkraft-tech/public-docs" width="60px" align="center" alt="Generate Documentation" />
7+
</div>
8+
9+
---
10+
11+
<!-- header:end -->
12+
<!-- badges:start -->
13+
14+
[![Marketplace](https://img.shields.io/badge/Marketplace-generate--documentation-blue?logo=github-actions)](https://github.com/marketplace/actions/generate-documentation)
15+
[![Release](https://img.shields.io/github/v/release/hoverkraft-tech/public-docs)](https://github.com/hoverkraft-tech/public-docs/releases)
16+
[![License](https://img.shields.io/github/license/hoverkraft-tech/public-docs)](http://choosealicense.com/licenses/mit/)
17+
[![Stars](https://img.shields.io/github/stars/hoverkraft-tech/public-docs?style=social)](https://img.shields.io/github/stars/hoverkraft-tech/public-docs?style=social)
18+
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/hoverkraft-tech/public-docs/blob/main/CONTRIBUTING.md)
19+
20+
<!-- badges:end -->
21+
<!-- overview:start -->
22+
23+
## Overview
24+
25+
Generate project metadata pages
26+
27+
Features:
28+
29+
- Scans all repositories in the organization
30+
- Categorizes projects by type
31+
- Generates the projects overview page
32+
- Updates homepage with featured projects
33+
34+
<!-- overview:end -->
35+
<!-- usage:start -->
36+
37+
## Usage
38+
39+
```yaml
40+
- uses: hoverkraft-tech/public-docs/.github/actions/generate-docs@fca4a9a32575438db28e9b8ad81d97758eca84c4 # main
41+
with:
42+
# GitHub token used to authenticate repository queries.
43+
# This input is required.
44+
github-token: ""
45+
```
46+
47+
<!-- usage:end -->
48+
<!-- inputs:start -->
49+
50+
## Inputs
51+
52+
| **Input** | **Description** | **Required** | **Default** |
53+
| ------------------ | ----------------------------------------------------- | ------------ | ----------- |
54+
| **`github-token`** | GitHub token used to authenticate repository queries. | **true** | - |
55+
56+
<!-- inputs:end -->
57+
<!-- secrets:start -->
58+
<!-- secrets:end -->
59+
<!-- outputs:start -->
60+
<!-- outputs:end -->
61+
<!-- examples:start -->
62+
<!-- examples:end -->
63+
<!-- contributing:start -->
64+
65+
## Contributing
66+
67+
Contributions are welcome! Please see the [contributing guidelines](https://github.com/hoverkraft-tech/public-docs/blob/main/CONTRIBUTING.md) for more details.
68+
69+
<!-- contributing:end -->
70+
<!-- security:start -->
71+
<!-- security:end -->
72+
<!-- license:start -->
73+
74+
## License
75+
76+
This project is licensed under the MIT License.
77+
78+
SPDX-License-Identifier: MIT
79+
80+
Copyright © 2025 hoverkraft-tech
81+
82+
For more details, see the [license](http://choosealicense.com/licenses/mit/).
83+
84+
<!-- license:end -->
85+
<!-- generated:start -->
86+
87+
---
88+
89+
This documentation was automatically generated by [CI Dokumentor](https://github.com/hoverkraft-tech/ci-dokumentor).
90+
91+
<!-- generated:end -->
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Generate Documentation
2+
description: |
3+
Generate project metadata pages
4+
5+
Features:
6+
7+
- Scans all repositories in the organization
8+
- Categorizes projects by type
9+
- Generates the projects overview page
10+
- Updates homepage with featured projects
11+
12+
inputs:
13+
github-token:
14+
description: GitHub token used to authenticate repository queries.
15+
required: true
16+
17+
runs:
18+
using: composite
19+
steps:
20+
- name: Setup Node.js and install dependencies
21+
uses: hoverkraft-tech/ci-github-nodejs/actions/setup-node@6809332ced7647b3d52300a47d65657283f3395e # 0.16.0
22+
with:
23+
working-directory: ${{ github.action_path }}
24+
25+
- name: Generate documentation from repositories
26+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
27+
env:
28+
TEMPLATE_NAME: ${{ inputs.template-name }}
29+
NODE_PATH: ${{ github.action_path }}/node_modules
30+
with:
31+
github-token: ${{ inputs.github-token }}
32+
script: |
33+
const { run } = require('${{ github.action_path }}/generate-docs.js');
34+
await run({ github });
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env node
2+
3+
const { DocumentationGenerator } = require("./lib/documentation-generator");
4+
5+
async function run({ github }) {
6+
try {
7+
const generator = new DocumentationGenerator({ github });
8+
await generator.run();
9+
} catch (error) {
10+
console.error("❌ Error during generation:", error);
11+
throw error;
12+
}
13+
}
14+
15+
module.exports = { run };
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
const ejs = require("ejs");
2+
const fs = require("fs");
3+
const { resolveIcon } = require("./resolve-icon");
4+
const { PROJECTS_MD_TEMPLATE } = require("../constants");
5+
6+
class ProjectsContentBuilder {
7+
constructor({ templatePath } = {}) {
8+
this.templatePath = templatePath || PROJECTS_MD_TEMPLATE;
9+
this.template = fs.readFileSync(this.templatePath, "utf8");
10+
}
11+
12+
build({ categories, repositories, generatedAt }) {
13+
const stats = this.buildStats(repositories, generatedAt);
14+
const categoryModels = this.buildCategoryModels(categories);
15+
16+
return ejs.render(
17+
this.template,
18+
{
19+
stats,
20+
categories: categoryModels,
21+
},
22+
{ filename: this.templatePath }
23+
);
24+
}
25+
26+
buildStats(repositories, generatedAt) {
27+
const totalRepos = repositories.length;
28+
const totalStars = repositories.reduce(
29+
(sum, repository) => sum + (repository.stargazers_count || 0),
30+
0
31+
);
32+
const languages = [
33+
...new Set(
34+
repositories.map((repository) => repository.language).filter(Boolean)
35+
),
36+
];
37+
const primaryLanguages = languages.slice(0, 5);
38+
39+
return {
40+
totalRepos,
41+
totalStars,
42+
languageCount: languages.length,
43+
languagesSummary:
44+
primaryLanguages.length > 0
45+
? primaryLanguages.join(", ")
46+
: "various stacks",
47+
generatedDate: generatedAt.toISOString().split("T")[0],
48+
};
49+
}
50+
51+
buildCategoryModels(categories) {
52+
return Object.entries(categories)
53+
.filter(([, repositories]) => repositories.length > 0)
54+
.map(([name, repositories]) => ({
55+
name,
56+
repositories: repositories.map((repository) =>
57+
this.buildRepositoryModel(repository)
58+
),
59+
}));
60+
}
61+
62+
buildRepositoryModel(repository) {
63+
return {
64+
name: repository.name,
65+
htmlUrl: repository.html_url,
66+
icon: resolveIcon(repository),
67+
language: repository.language || "Unknown",
68+
stars: repository.stargazers_count || 0,
69+
lastUpdated: this.formatDate(repository.updated_at),
70+
description: repository.description || "No description available.",
71+
topics: (repository.topics || []).slice(0, 5),
72+
homepage: repository.homepage,
73+
};
74+
}
75+
76+
formatDate(dateString) {
77+
if (!dateString) {
78+
return "Unknown";
79+
}
80+
81+
const date = new Date(dateString);
82+
83+
if (Number.isNaN(date.getTime())) {
84+
return "Unknown";
85+
}
86+
87+
return date.toLocaleDateString("en-US", {
88+
year: "numeric",
89+
month: "short",
90+
day: "numeric",
91+
});
92+
}
93+
}
94+
95+
module.exports = {
96+
ProjectsContentBuilder,
97+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
function buildProfile(repository) {
2+
return {
3+
name: (repository.name || "").toLowerCase(),
4+
description: (repository.description || "").toLowerCase(),
5+
topics: new Set(repository.topics || []),
6+
};
7+
}
8+
module.exports = {
9+
buildProfile,
10+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const { ICON_RULES } = require("../rules");
2+
const { buildProfile } = require("./repository-profile");
3+
4+
function resolveIcon(repository) {
5+
const profile = buildProfile(repository);
6+
const rule = ICON_RULES.find((candidate) => candidate.predicate(profile));
7+
return rule ? rule.icon : "🔧";
8+
}
9+
10+
module.exports = {
11+
resolveIcon,
12+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const path = require("path");
2+
3+
const ACTION_ROOT = path.join(__dirname, "..");
4+
const REPO_ROOT = path.join(ACTION_ROOT, "..", "..", "..");
5+
const APPLICATION_ROOT = path.join(REPO_ROOT, "application");
6+
const DOCS_DIR = path.join(APPLICATION_ROOT, "docs");
7+
const PROJECTS_MD_PATH = path.join(DOCS_DIR, "projects.md");
8+
const HOMEPAGE_PATH = path.join(APPLICATION_ROOT, "src", "pages", "index.tsx");
9+
const TEMPLATE_DIR = path.join(__dirname, "templates");
10+
const PROJECTS_MD_TEMPLATE = path.join(TEMPLATE_DIR, "projects.md.ejs");
11+
const FEATURED_REPOSITORY_LIMIT = 6;
12+
const HOMEPAGE_CARD_LIMIT = 3;
13+
14+
const OWNER = process.env.GITHUB_REPOSITORY_OWNER;
15+
16+
if (!OWNER) {
17+
throw new Error(
18+
"GITHUB_REPOSITORY_OWNER environment variable must be set for documentation generation."
19+
);
20+
}
21+
22+
const REPOSITORY_SLUG = process.env.GITHUB_REPOSITORY;
23+
24+
if (!REPOSITORY_SLUG) {
25+
throw new Error(
26+
"GITHUB_REPOSITORY environment variable must be set for documentation generation."
27+
);
28+
}
29+
30+
const [, REPOSITORY_NAME] = REPOSITORY_SLUG.split("/");
31+
32+
if (!REPOSITORY_NAME) {
33+
throw new Error(
34+
`Unable to determine repository name from GITHUB_REPOSITORY='${REPOSITORY_SLUG}'.`
35+
);
36+
}
37+
38+
const IGNORED_REPOSITORIES = new Set([REPOSITORY_NAME]);
39+
40+
module.exports = {
41+
OWNER,
42+
DOCS_DIR,
43+
PROJECTS_MD_PATH,
44+
HOMEPAGE_PATH,
45+
PROJECTS_MD_TEMPLATE,
46+
IGNORED_REPOSITORIES,
47+
FEATURED_REPOSITORY_LIMIT,
48+
HOMEPAGE_CARD_LIMIT,
49+
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
const fs = require("fs").promises;
2+
const {
3+
OWNER,
4+
IGNORED_REPOSITORIES,
5+
PROJECTS_MD_PATH,
6+
} = require("./constants");
7+
const { CATEGORY_RULES } = require("./rules");
8+
const {
9+
GitHubRepositoryService,
10+
} = require("./services/github-repository-service");
11+
const { RepositoryFilter } = require("./repository-filter");
12+
const { RepositoryCategorizer } = require("./repository-categorizer");
13+
const {
14+
ProjectsContentBuilder,
15+
} = require("./builders/projects-content-builder");
16+
const {
17+
HomepageProjectsUpdater,
18+
} = require("./homepage/homepage-projects-updater");
19+
20+
class DocumentationGenerator {
21+
constructor({ github }) {
22+
this.repositoryService = new GitHubRepositoryService(github);
23+
this.repositoryFilter = new RepositoryFilter({
24+
ignoredNames: IGNORED_REPOSITORIES,
25+
});
26+
this.repositoryCategorizer = new RepositoryCategorizer(CATEGORY_RULES);
27+
this.projectsContentBuilder = new ProjectsContentBuilder();
28+
this.homepageUpdater = new HomepageProjectsUpdater();
29+
}
30+
31+
async run() {
32+
console.log("🚀 Starting documentation generation...");
33+
34+
const rawRepositories =
35+
await this.repositoryService.fetchOrganizationRepositories(OWNER);
36+
37+
const showcaseRepositories = this.repositoryFilter.apply(rawRepositories);
38+
const categories =
39+
this.repositoryCategorizer.categorize(showcaseRepositories);
40+
41+
await this.writeProjectsAssets({
42+
categories,
43+
repositories: showcaseRepositories,
44+
generatedAt: new Date(),
45+
});
46+
47+
const pinnedRepositoryNames =
48+
await this.repositoryService.fetchOrganizationPinnedRepositories(OWNER);
49+
50+
await this.homepageUpdater.update(rawRepositories, pinnedRepositoryNames);
51+
52+
console.log("✅ Documentation generation completed!");
53+
this.logSummary(categories);
54+
}
55+
56+
async writeProjectsAssets({ categories, repositories, generatedAt }) {
57+
const markdown = this.projectsContentBuilder.build({
58+
categories,
59+
repositories,
60+
generatedAt,
61+
});
62+
63+
await fs.writeFile(PROJECTS_MD_PATH, markdown, "utf8");
64+
65+
console.log("📄 Generated files:");
66+
console.log(" - docs/projects.md");
67+
console.log(" - Updated src/pages/index.tsx");
68+
}
69+
70+
logSummary(categories) {
71+
console.log("\n📊 Repository Summary:");
72+
73+
Object.entries(categories).forEach(([category, repositories]) => {
74+
if (repositories.length > 0) {
75+
console.log(` ${category}: ${repositories.length} projects`);
76+
}
77+
});
78+
}
79+
}
80+
81+
module.exports = {
82+
DocumentationGenerator,
83+
};

0 commit comments

Comments
 (0)