Skip to content

Commit d724bfb

Browse files
Improve README gist formatting
1 parent 4ab20ee commit d724bfb

1 file changed

Lines changed: 160 additions & 115 deletions

File tree

.github/workflows/create-readme-gist.yml

Lines changed: 160 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ on:
1818
default: true
1919
type: boolean
2020
gist_description:
21-
description: "Optional gist description"
21+
description: "Optional gist title/description. Defaults to the README H1."
22+
required: false
23+
default: ""
24+
type: string
25+
gist_id:
26+
description: "Optional existing gist ID or URL to update instead of creating a new gist."
2227
required: false
2328
default: ""
2429
type: string
@@ -39,20 +44,25 @@ on:
3944
default: true
4045
type: boolean
4146
gist_description:
42-
description: "Optional gist description"
47+
description: "Optional gist title/description. Defaults to the README H1."
48+
required: false
49+
default: ""
50+
type: string
51+
gist_id:
52+
description: "Optional existing gist ID or URL to update instead of creating a new gist."
4353
required: false
4454
default: ""
4555
type: string
4656
secrets:
4757
SERPGAMEDEVELOPER_GIST_TOKEN:
48-
description: "PAT from the serpgamedeveloper account with gist scope"
58+
description: "PAT from the serpgamedeveloper account with gist write permission"
4959
required: true
5060
outputs:
5161
gist_url:
52-
description: "Created gist URL"
62+
description: "Created or updated gist URL"
5363
value: ${{ jobs.create-readme-gist.outputs.gist_url }}
5464
gist_id:
55-
description: "Created gist ID"
65+
description: "Created or updated gist ID"
5666
value: ${{ jobs.create-readme-gist.outputs.gist_id }}
5767

5868
permissions:
@@ -74,139 +84,174 @@ jobs:
7484
INPUT_REF: ${{ inputs.ref }}
7585
GIST_PUBLIC: ${{ inputs.public }}
7686
GIST_DESCRIPTION: ${{ inputs.gist_description }}
87+
INPUT_GIST_ID: ${{ inputs.gist_id }}
7788
GITHUB_TOKEN: ${{ github.token }}
7889
SERPGAMEDEVELOPER_GIST_TOKEN: ${{ secrets.SERPGAMEDEVELOPER_GIST_TOKEN }}
7990
run: |
8091
node <<'NODE'
8192
const fs = require("fs");
8293
8394
async function main() {
95+
const apiVersion = "2022-11-28";
96+
const readToken = process.env.GITHUB_TOKEN || "";
97+
const gistToken = process.env.SERPGAMEDEVELOPER_GIST_TOKEN || "";
8498
85-
const apiVersion = "2022-11-28";
86-
const readToken = process.env.GITHUB_TOKEN || "";
87-
const gistToken = process.env.SERPGAMEDEVELOPER_GIST_TOKEN || "";
88-
89-
if (!gistToken) {
90-
throw new Error("Missing SERPGAMEDEVELOPER_GIST_TOKEN secret. Add a serpgamedeveloper PAT with gist scope.");
91-
}
99+
if (!gistToken) {
100+
throw new Error("Missing SERPGAMEDEVELOPER_GIST_TOKEN secret. Add a serpgamedeveloper PAT with gist write permission.");
101+
}
92102
93-
const rawRepository = (process.env.INPUT_REPOSITORY || "").trim();
94-
if (!rawRepository) {
95-
throw new Error("The repository input is required.");
96-
}
103+
const rawRepository = (process.env.INPUT_REPOSITORY || "").trim();
104+
if (!rawRepository) {
105+
throw new Error("The repository input is required.");
106+
}
97107
98-
const parts = rawRepository.includes("/")
99-
? rawRepository.split("/")
100-
: ["serpgames", rawRepository];
108+
const parts = rawRepository.includes("/")
109+
? rawRepository.split("/")
110+
: ["serpgames", rawRepository];
101111
102-
if (parts.length !== 2 || parts[0] !== "serpgames") {
103-
throw new Error("repository must be a repo name in serpgames, or serpgames/repo-name.");
104-
}
112+
if (parts.length !== 2 || parts[0] !== "serpgames") {
113+
throw new Error("repository must be a repo name in serpgames, or serpgames/repo-name.");
114+
}
105115
106-
const [owner, repo] = parts;
107-
if (!/^[A-Za-z0-9_.-]+$/.test(repo)) {
108-
throw new Error(`Invalid repository name: ${repo}`);
109-
}
116+
const [owner, repo] = parts;
117+
if (!/^[A-Za-z0-9_.-]+$/.test(repo)) {
118+
throw new Error(`Invalid repository name: ${repo}`);
119+
}
110120
111-
const githubHeaders = {
112-
"Accept": "application/vnd.github+json",
113-
"X-GitHub-Api-Version": apiVersion,
114-
"User-Agent": "serpgames-readme-gist-action",
115-
};
116-
if (readToken) {
117-
githubHeaders.Authorization = `Bearer ${readToken}`;
118-
}
121+
const githubHeaders = {
122+
"Accept": "application/vnd.github+json",
123+
"X-GitHub-Api-Version": apiVersion,
124+
"User-Agent": "serpgames-readme-gist-action",
125+
};
126+
if (readToken) {
127+
githubHeaders.Authorization = `Bearer ${readToken}`;
128+
}
119129
120-
async function requestJson(url, options = {}) {
121-
const response = await fetch(url, {
122-
...options,
123-
headers: {
124-
...githubHeaders,
125-
...(options.headers || {}),
126-
},
127-
});
128-
const text = await response.text();
129-
let body = null;
130-
if (text) {
131-
try {
132-
body = JSON.parse(text);
133-
} catch {
134-
body = text;
130+
async function requestJson(url, options = {}) {
131+
const response = await fetch(url, {
132+
...options,
133+
headers: {
134+
...githubHeaders,
135+
...(options.headers || {}),
136+
},
137+
});
138+
const text = await response.text();
139+
let body = null;
140+
if (text) {
141+
try {
142+
body = JSON.parse(text);
143+
} catch {
144+
body = text;
145+
}
135146
}
147+
if (!response.ok) {
148+
const details = typeof body === "string" ? body : JSON.stringify(body);
149+
throw new Error(`GitHub API request failed: ${response.status} ${response.statusText} ${url} ${details}`);
150+
}
151+
return body;
136152
}
137-
if (!response.ok) {
138-
const details = typeof body === "string" ? body : JSON.stringify(body);
139-
throw new Error(`GitHub API request failed: ${response.status} ${response.statusText} ${url} ${details}`);
153+
154+
function cleanReadmeForGist(markdown) {
155+
return markdown
156+
.replace(/\r\n/g, "\n")
157+
.replace(/\\n/g, "\n")
158+
.replace(/^\[!\[START GAME\]\(\.\/start-game\.(?:gif|svg)\)\]\(([^)]+)\)\s*$/m, "**[START GAME]($1)**")
159+
.trimEnd() + "\n";
140160
}
141-
return body;
142-
}
143161
144-
const repoInfo = await requestJson(`https://api.github.com/repos/${owner}/${repo}`);
145-
const requestedRef = (process.env.INPUT_REF || "").trim();
146-
const ref = requestedRef || repoInfo.default_branch;
147-
const readme = await requestJson(
148-
`https://api.github.com/repos/${owner}/${repo}/readme?ref=${encodeURIComponent(ref)}`
149-
);
162+
function readTitle(markdown, fallback) {
163+
const h1 = markdown.match(/^#\s+(.+?)\s*$/m);
164+
return h1 ? h1[1].trim() : fallback;
165+
}
150166
151-
if (readme.encoding !== "base64") {
152-
throw new Error(`Unsupported README encoding: ${readme.encoding}`);
153-
}
167+
function normalizeGistId(input) {
168+
const value = (input || "").trim();
169+
if (!value) return "";
170+
const match = value.match(/([0-9a-f]{20,})$/i);
171+
return match ? match[1] : value;
172+
}
173+
174+
const repoInfo = await requestJson(`https://api.github.com/repos/${owner}/${repo}`);
175+
const requestedRef = (process.env.INPUT_REF || "").trim();
176+
const ref = requestedRef || repoInfo.default_branch;
177+
const readme = await requestJson(
178+
`https://api.github.com/repos/${owner}/${repo}/readme?ref=${encodeURIComponent(ref)}`
179+
);
180+
181+
if (readme.encoding !== "base64") {
182+
throw new Error(`Unsupported README encoding: ${readme.encoding}`);
183+
}
154184
155-
const readmeMarkdown = Buffer.from(readme.content.replace(/\n/g, ""), "base64").toString("utf8");
156-
const gistPublic = String(process.env.GIST_PUBLIC || "true").toLowerCase() !== "false";
157-
const generatedAt = new Date().toISOString();
158-
const description = (process.env.GIST_DESCRIPTION || "").trim()
159-
|| `${owner}/${repo} README from ${ref}`;
160-
161-
const metadata = {
162-
repository: `${owner}/${repo}`,
163-
ref,
164-
default_branch: repoInfo.default_branch,
165-
readme_path: readme.path,
166-
readme_sha: readme.sha,
167-
readme_size: readme.size,
168-
readme_html_url: readme.html_url,
169-
readme_download_url: readme.download_url,
170-
generated_at: generatedAt,
171-
};
172-
173-
const gist = await requestJson("https://api.github.com/gists", {
174-
method: "POST",
175-
headers: {
176-
Authorization: `Bearer ${gistToken}`,
177-
},
178-
body: JSON.stringify({
179-
description,
180-
public: gistPublic,
181-
files: {
182-
[`${repo}-README.md`]: {
183-
content: readmeMarkdown,
185+
const rawReadmeMarkdown = Buffer.from(readme.content.replace(/\n/g, ""), "base64").toString("utf8");
186+
const readmeMarkdown = cleanReadmeForGist(rawReadmeMarkdown);
187+
const title = (process.env.GIST_DESCRIPTION || "").trim()
188+
|| readTitle(readmeMarkdown, `Play ${repo.replace(/-game$/, "").replace(/-/g, " ")} Game Online - Free Unblocked`);
189+
const gistPublic = String(process.env.GIST_PUBLIC || "true").toLowerCase() !== "false";
190+
const existingGistId = normalizeGistId(process.env.INPUT_GIST_ID);
191+
192+
let gist;
193+
if (existingGistId) {
194+
const existingGist = await requestJson(`https://api.github.com/gists/${existingGistId}`, {
195+
headers: {
196+
Authorization: `Bearer ${gistToken}`,
184197
},
185-
[`${repo}-readme-metadata.json`]: {
186-
content: JSON.stringify(metadata, null, 2),
198+
});
199+
const files = {};
200+
for (const filename of Object.keys(existingGist.files || {})) {
201+
files[filename] = null;
202+
}
203+
const firstMarkdownFile = Object.keys(existingGist.files || {}).find((filename) => filename.toLowerCase().endsWith(".md"));
204+
files[firstMarkdownFile || "README.md"] = {
205+
filename: "README.md",
206+
content: readmeMarkdown,
207+
};
208+
gist = await requestJson(`https://api.github.com/gists/${existingGistId}`, {
209+
method: "PATCH",
210+
headers: {
211+
Authorization: `Bearer ${gistToken}`,
187212
},
188-
},
189-
}),
190-
});
213+
body: JSON.stringify({
214+
description: title,
215+
files,
216+
}),
217+
});
218+
} else {
219+
gist = await requestJson("https://api.github.com/gists", {
220+
method: "POST",
221+
headers: {
222+
Authorization: `Bearer ${gistToken}`,
223+
},
224+
body: JSON.stringify({
225+
description: title,
226+
public: gistPublic,
227+
files: {
228+
"README.md": {
229+
content: readmeMarkdown,
230+
},
231+
},
232+
}),
233+
});
234+
}
191235
192-
fs.appendFileSync(process.env.GITHUB_OUTPUT, `gist_url=${gist.html_url}\n`);
193-
fs.appendFileSync(process.env.GITHUB_OUTPUT, `gist_id=${gist.id}\n`);
194-
195-
if (process.env.GITHUB_STEP_SUMMARY) {
196-
fs.appendFileSync(
197-
process.env.GITHUB_STEP_SUMMARY,
198-
[
199-
"## README gist created",
200-
"",
201-
`- Repository: ${owner}/${repo}`,
202-
`- Ref: ${ref}`,
203-
`- README: ${readme.html_url}`,
204-
`- Gist: ${gist.html_url}`,
205-
`- Public: ${gist.public}`,
206-
"",
207-
].join("\n")
208-
);
209-
}
236+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `gist_url=${gist.html_url}\n`);
237+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `gist_id=${gist.id}\n`);
238+
239+
if (process.env.GITHUB_STEP_SUMMARY) {
240+
fs.appendFileSync(
241+
process.env.GITHUB_STEP_SUMMARY,
242+
[
243+
existingGistId ? "## README gist updated" : "## README gist created",
244+
"",
245+
`- Title: ${title}`,
246+
`- Repository: ${owner}/${repo}`,
247+
`- Ref: ${ref}`,
248+
`- README: ${readme.html_url}`,
249+
`- Gist: ${gist.html_url}`,
250+
`- Public: ${gist.public}`,
251+
"",
252+
].join("\n")
253+
);
254+
}
210255
}
211256
212257
main().catch((error) => {

0 commit comments

Comments
 (0)