Skip to content

Commit 8eee78d

Browse files
Merge pull request #16 from GitbookIO/publish-skill-md-to-docs
Publish skills to GitBook Documentation
2 parents 78b5e92 + 33f031a commit 8eee78d

4 files changed

Lines changed: 301 additions & 0 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Publish combined skill to public-docs
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "skills/**"
8+
- "scripts/build-skill.js"
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
publish:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout gitbook-skills
18+
uses: actions/checkout@v4
19+
20+
- name: Build gitbook skill
21+
run: node scripts/build-skill.js
22+
23+
- name: Checkout public-docs
24+
uses: actions/checkout@v4
25+
with:
26+
repository: GitbookIO/public-docs
27+
token: ${{ secrets.PUBLIC_DOCS_PAT }}
28+
path: public-docs
29+
fetch-depth: 0
30+
31+
- name: Sync branch and apply changes
32+
id: sync
33+
working-directory: public-docs
34+
env:
35+
GITHUB_SHA: ${{ github.sha }}
36+
run: |
37+
set -euo pipefail
38+
git config user.name "gitbook-skills-bot"
39+
git config user.email "actions@github.com"
40+
git fetch origin main
41+
git checkout -B sync/gitbook-skill origin/main
42+
43+
cp ../dist/skill.md ./skill.md
44+
rm -rf skill
45+
mkdir -p skill
46+
cp ../dist/skill/*.md skill/
47+
cp ../dist/skill-manifest.json ./skill-manifest.json
48+
node ../scripts/update-summary.js
49+
rm -f skill-manifest.json
50+
51+
git add skill.md skill/ SUMMARY.md
52+
if git diff --cached --quiet; then
53+
echo "No changes to publish"
54+
echo "changed=false" >> "$GITHUB_OUTPUT"
55+
exit 0
56+
fi
57+
58+
git commit -m "Sync gitbook skill.md from gitbook-skills@${GITHUB_SHA:0:7}"
59+
git push --force origin sync/gitbook-skill
60+
echo "changed=true" >> "$GITHUB_OUTPUT"
61+
62+
- name: Open PR if needed
63+
if: steps.sync.outputs.changed == 'true'
64+
working-directory: public-docs
65+
env:
66+
GH_TOKEN: ${{ secrets.PUBLIC_DOCS_PAT }}
67+
GITHUB_SHA: ${{ github.sha }}
68+
run: |
69+
set -euo pipefail
70+
if ! gh pr view sync/gitbook-skill --repo GitbookIO/public-docs >/dev/null 2>&1; then
71+
gh pr create --repo GitbookIO/public-docs \
72+
--head sync/gitbook-skill \
73+
--base main \
74+
--title "Sync gitbook skill.md from gitbook-skills" \
75+
--body "Auto-generated from https://github.com/GitbookIO/gitbook-skills/commit/${GITHUB_SHA}. This PR is kept up to date automatically — new merges to gitbook-skills main will overwrite it until it's merged."
76+
else
77+
echo "PR already open on sync/gitbook-skill, updated in place by the force-push above"
78+
fi

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.DS_Store
22
.env
3+
dist/

scripts/build-skill.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
#!/usr/bin/env node
2+
// Builds the "gitbook" skill for publishing into public-docs:
3+
// - dist/skill.md a short router (<500 lines)
4+
// - dist/skill/<name>.md one page per skill, mirrored verbatim from
5+
// skills/<name>/SKILL.md
6+
// - dist/skill-manifest.json {dir, title}[] used by update-summary.js
7+
//
8+
// Everything the router links to lives on the same origin (gitbook.com)
9+
// as the router itself — no dependency on an agent being willing or able
10+
// to fetch a second domain (github.com) to get full instructions.
11+
//
12+
// Per the Agent Skills spec (https://agentskills.io/specification):
13+
// - frontmatter `description` must be <= 1024 characters
14+
// - SKILL.md should stay under ~500 lines / ~5000 tokens; detailed
15+
// instructions belong in files loaded on demand, not inlined
16+
// Each skills/<name>/SKILL.md is already independently under that limit,
17+
// so mirroring them verbatim as their own pages keeps every page compliant
18+
// on its own.
19+
//
20+
// Deep skills/*/references/** material (~25k lines total) stays linked
21+
// back to GitHub — the one tier where asking for a domain hop is fine,
22+
// since it's optional depth most agents won't need.
23+
24+
const fs = require("fs");
25+
const path = require("path");
26+
27+
const ROOT = path.resolve(__dirname, "..");
28+
const SKILLS_DIR = path.join(ROOT, "skills");
29+
const DIST_DIR = path.join(ROOT, "dist");
30+
const SKILLS_OUT_DIR = path.join(DIST_DIR, "skill");
31+
const REPO_URL = "https://github.com/GitbookIO/gitbook-skills";
32+
const DOCS_BASE_URL = "https://gitbook.com/docs";
33+
const MAX_DESCRIPTION_LENGTH = 1024;
34+
const RECOMMENDED_MAX_LINES = 500;
35+
36+
// Order matches the README's "Available Skills" table.
37+
const SKILLS = [
38+
{ dir: "write-docs", title: "Write & Edit Docs" },
39+
{ dir: "configure-site", title: "Configure a Site" },
40+
{ dir: "write-openapi", title: "Write OpenAPI Reference Docs" },
41+
{ dir: "cr-create", title: "Create & Manage Change Requests" },
42+
{ dir: "cr-review", title: "Review Change Requests" },
43+
{ dir: "build-integration", title: "Build an Integration" },
44+
];
45+
46+
const UMBRELLA_DESCRIPTION =
47+
"Work with GitBook end-to-end: author and format GitBook-flavored Markdown pages and blocks " +
48+
"(hints, tabs, steppers); design, scaffold, and configure documentation sites via the GitBook " +
49+
"REST API and Git Sync; generate and troubleshoot OpenAPI/Swagger API reference docs; create, " +
50+
"push content to, and manage change requests and reviews over the REST API; and build GitBook " +
51+
"integrations (custom blocks, ContentKit UI, events, OAuth). Use whenever a task involves " +
52+
"GitBook: writing or editing docs, SUMMARY.md/.gitbook.yaml, site structure, OpenAPI " +
53+
"references, change-request review flows, or building a GitBook app/integration.";
54+
55+
function parseTopLevelYaml(text) {
56+
const lines = text.split("\n");
57+
const result = {};
58+
for (let i = 0; i < lines.length; i++) {
59+
const line = lines[i];
60+
if (!/^\S/.test(line)) continue; // only top-level (unindented) keys
61+
const m = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
62+
if (!m) continue;
63+
const key = m[1];
64+
let rest = m[2].trim();
65+
if (rest === ">-" || rest === ">" || rest === "|-" || rest === "|") {
66+
const collected = [];
67+
let j = i + 1;
68+
while (j < lines.length && (lines[j] === "" || /^\s+/.test(lines[j]))) {
69+
if (lines[j].trim() !== "") collected.push(lines[j].trim());
70+
j++;
71+
}
72+
result[key] = collected.join(" ").trim();
73+
i = j - 1;
74+
} else {
75+
if (
76+
(rest.startsWith('"') && rest.endsWith('"')) ||
77+
(rest.startsWith("'") && rest.endsWith("'"))
78+
) {
79+
rest = rest.slice(1, -1);
80+
}
81+
result[key] = rest;
82+
}
83+
}
84+
return result;
85+
}
86+
87+
function wrapFolded(text, indent = " ", width = 100) {
88+
const words = text.split(/\s+/);
89+
const lines = [];
90+
let current = "";
91+
for (const word of words) {
92+
if ((current + " " + word).trim().length > width) {
93+
lines.push(current.trim());
94+
current = word;
95+
} else {
96+
current = (current + " " + word).trim();
97+
}
98+
}
99+
if (current) lines.push(current.trim());
100+
return lines.map((l) => indent + l).join("\n");
101+
}
102+
103+
function parseSkillFile(dir) {
104+
const filePath = path.join(SKILLS_DIR, dir, "SKILL.md");
105+
const raw = fs.readFileSync(filePath, "utf8");
106+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
107+
if (!match) throw new Error(`No frontmatter found in ${filePath}`);
108+
const frontmatter = parseTopLevelYaml(match[1]);
109+
return {
110+
name: frontmatter.name || dir,
111+
description: frontmatter.description || "",
112+
raw,
113+
};
114+
}
115+
116+
function main() {
117+
if (UMBRELLA_DESCRIPTION.length > MAX_DESCRIPTION_LENGTH) {
118+
throw new Error(
119+
`Combined description is ${UMBRELLA_DESCRIPTION.length} chars, over the ${MAX_DESCRIPTION_LENGTH}-char spec limit`
120+
);
121+
}
122+
123+
const parsed = SKILLS.map((s) => ({ ...s, ...parseSkillFile(s.dir) }));
124+
125+
fs.mkdirSync(SKILLS_OUT_DIR, { recursive: true });
126+
for (const s of parsed) {
127+
const outPath = path.join(SKILLS_OUT_DIR, `${s.dir}.md`);
128+
fs.writeFileSync(outPath, s.raw);
129+
const lines = s.raw.split("\n").length;
130+
if (lines > RECOMMENDED_MAX_LINES) {
131+
console.warn(
132+
`Warning: skills/${s.dir}/SKILL.md is ${lines} lines, over the spec's recommended ${RECOMMENDED_MAX_LINES}-line ceiling`
133+
);
134+
}
135+
}
136+
137+
const entries = parsed
138+
.map((s) => {
139+
const pageUrl = `${DOCS_BASE_URL}/skill/${s.dir}.md`;
140+
return `### ${s.title}\n\n${s.description}\n\nFull instructions: [${pageUrl}](${pageUrl})`;
141+
})
142+
.join("\n\n");
143+
144+
const routerOut = `---
145+
name: gitbook
146+
description: >-
147+
${wrapFolded(UMBRELLA_DESCRIPTION)}
148+
---
149+
150+
{% hint style="info" %}
151+
This page is generated automatically from [GitbookIO/gitbook-skills](${REPO_URL}). Don't edit it directly — edit the source skills there instead.
152+
{% endhint %}
153+
154+
# GitBook
155+
156+
GitBook's skill for AI coding agents, covering six areas of GitBook work. Each section below is one of the six skills in [gitbook-skills](${REPO_URL}) — read its description to see if it matches your task, then fetch its linked page for full instructions before acting. Each skill's instructions link out to further reference material (full block syntax, API payloads, troubleshooting) in [gitbook-skills](${REPO_URL}) under \`skills/<name>/references/\` when you need more depth than the top-level instructions.
157+
158+
${entries}
159+
`;
160+
161+
const routerLines = routerOut.split("\n").length;
162+
if (routerLines > RECOMMENDED_MAX_LINES) {
163+
console.warn(
164+
`Warning: generated skill.md is ${routerLines} lines, over the spec's recommended ${RECOMMENDED_MAX_LINES}-line ceiling`
165+
);
166+
}
167+
168+
fs.writeFileSync(path.join(DIST_DIR, "skill.md"), routerOut);
169+
fs.writeFileSync(
170+
path.join(DIST_DIR, "skill-manifest.json"),
171+
JSON.stringify(
172+
parsed.map(({ dir, title }) => ({ dir, title })),
173+
null,
174+
2
175+
)
176+
);
177+
178+
console.log(`Wrote dist/skill.md (${routerOut.length} bytes, ${routerLines} lines)`);
179+
console.log(`Wrote ${parsed.length} skill pages to dist/skill/`);
180+
}
181+
182+
main();

scripts/update-summary.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env node
2+
// Idempotently ensures SUMMARY.md has a top-level entry for skill.md plus
3+
// nested entries for each generated skill/<name>.md page, so they publish
4+
// at the site root (gitbook.com/docs/skill -> .../skill.md).
5+
//
6+
// Run from inside a checkout of GitbookIO/public-docs, with
7+
// dist/skill-manifest.json copied alongside SUMMARY.md as
8+
// ./skill-manifest.json.
9+
10+
const fs = require("fs");
11+
const path = require("path");
12+
13+
const summaryPath = path.resolve(process.cwd(), "SUMMARY.md");
14+
const manifestPath = path.resolve(process.cwd(), "skill-manifest.json");
15+
16+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
17+
const content = fs.readFileSync(summaryPath, "utf8");
18+
19+
if (/\(skill\.md\)/.test(content)) {
20+
console.log("SUMMARY.md already references skill.md, leaving as-is");
21+
process.exit(0);
22+
}
23+
24+
const lines = content.split("\n");
25+
const block = [
26+
"* [Skill](skill.md)",
27+
...manifest.map((s) => ` * [${s.title}](skill/${s.dir}.md)`),
28+
];
29+
30+
const overviewIdx = lines.findIndex((l) => /^\*\s*\[.*\]\(README\.md\)\s*$/.test(l));
31+
32+
if (overviewIdx === -1) {
33+
const tocIdx = lines.findIndex((l) => /^#\s*Table of contents/i.test(l));
34+
lines.splice(tocIdx === -1 ? 0 : tocIdx + 1, 0, "", ...block);
35+
} else {
36+
lines.splice(overviewIdx + 1, 0, ...block);
37+
}
38+
39+
fs.writeFileSync(summaryPath, lines.join("\n"));
40+
console.log(`Added skill.md entry with ${manifest.length} nested pages to SUMMARY.md`);

0 commit comments

Comments
 (0)