Skip to content

Commit 7c1bcde

Browse files
committed
long description if needed
1 parent 438a877 commit 7c1bcde

3 files changed

Lines changed: 243 additions & 1 deletion

File tree

.github/scripts/evaluate-submission.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,8 @@ Provide a structured evaluation in the following exact markdown format. Do not a
318318
const comment =
319319
`## Hi, I'm ZiggyBot! 🤖 Here's my pre-evaluation of this submission:\n\n` +
320320
evaluation +
321-
`\n\n*ZiggyBot is an AI pre-screener based on Temporal's community mascot Ziggy. Final decisions are made by the community team.*`;
321+
`\n\n---\n*ZiggyBot is an AI pre-screener based on Temporal's community mascot Ziggy. Final decisions are made by the community team.*` +
322+
`\n\n**Reviewer:** Check the box below to generate a Contentful-ready long description for this submission.\n- [ ] Generate long description if needed`;
322323

323324
await postComment(comment);
324325

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* Generates a Contentful-ready markdown description for an approved Code Exchange submission.
3+
* Triggered when a reviewer checks the approval checkbox in ZiggyBot's evaluation comment.
4+
*
5+
* Posts the generated description as a new comment on the issue for the reviewer to copy
6+
* into Contentful.
7+
*/
8+
9+
import Anthropic from "@anthropic-ai/sdk";
10+
11+
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
12+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
13+
const ISSUE_NUMBER = process.env.ISSUE_NUMBER;
14+
const REPO = process.env.GITHUB_REPOSITORY;
15+
16+
function parseIssueBody(body) {
17+
const sections = {};
18+
const lines = (body || "").split("\n");
19+
let currentSection = null;
20+
let currentLines = [];
21+
for (const line of lines) {
22+
const headerMatch = line.match(/^###\s+(.+)$/);
23+
if (headerMatch) {
24+
if (currentSection) sections[currentSection] = currentLines.join("\n").trim();
25+
currentSection = headerMatch[1].trim();
26+
currentLines = [];
27+
} else if (currentSection) {
28+
currentLines.push(line);
29+
}
30+
}
31+
if (currentSection) sections[currentSection] = currentLines.join("\n").trim();
32+
return sections;
33+
}
34+
35+
function extractGitHubUrl(text) {
36+
const match = (text || "").match(/https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)/);
37+
if (!match) return null;
38+
return { url: match[0], owner: match[1], repo: match[2].replace(/\.git$/, "") };
39+
}
40+
41+
async function fetchGitHub(path) {
42+
try {
43+
const res = await fetch(`https://api.github.com${path}`, {
44+
headers: {
45+
Authorization: `Bearer ${GITHUB_TOKEN}`,
46+
Accept: "application/vnd.github+json",
47+
"X-GitHub-Api-Version": "2022-11-28",
48+
},
49+
});
50+
if (!res.ok) return null;
51+
return res.json();
52+
} catch {
53+
return null;
54+
}
55+
}
56+
57+
async function postComment(body) {
58+
const [owner, repo] = REPO.split("/");
59+
const res = await fetch(
60+
`https://api.github.com/repos/${owner}/${repo}/issues/${ISSUE_NUMBER}/comments`,
61+
{
62+
method: "POST",
63+
headers: {
64+
Authorization: `Bearer ${GITHUB_TOKEN}`,
65+
Accept: "application/vnd.github+json",
66+
"X-GitHub-Api-Version": "2022-11-28",
67+
"Content-Type": "application/json",
68+
},
69+
body: JSON.stringify({ body }),
70+
}
71+
);
72+
if (!res.ok) {
73+
const text = await res.text();
74+
throw new Error(`Failed to post comment: ${res.status} ${text}`);
75+
}
76+
}
77+
78+
async function main() {
79+
// Fetch the full issue to get its body
80+
const issue = await fetchGitHub(`/repos/${REPO}/issues/${ISSUE_NUMBER}`);
81+
if (!issue) throw new Error("Could not fetch issue data");
82+
83+
const sections = parseIssueBody(issue.body);
84+
const projectLinkSection =
85+
sections["Project link"] || sections["Project Link"] || Object.values(sections)[0] || "";
86+
const parsed = extractGitHubUrl(projectLinkSection);
87+
if (!parsed) throw new Error("No GitHub URL found in issue");
88+
89+
const { owner, repo } = parsed;
90+
91+
const [repoData, readmeData] = await Promise.all([
92+
fetchGitHub(`/repos/${owner}/${repo}`),
93+
fetchGitHub(`/repos/${owner}/${repo}/readme`),
94+
]);
95+
96+
let readmeContent = "";
97+
if (readmeData?.content) {
98+
readmeContent = Buffer.from(readmeData.content, "base64").toString("utf-8");
99+
if (readmeContent.length > 10000) {
100+
readmeContent = readmeContent.slice(0, 10000) + "\n\n[... README truncated ...]";
101+
}
102+
}
103+
104+
const shortDesc =
105+
sections["Short description (max 256 chars)"] || sections["Short description"] || "";
106+
const longDesc = sections["Long Description"] || sections["Long description"] || "";
107+
const language = sections["Language"] || "";
108+
const authors = sections["Author(s)"] || sections["Authors"] || "";
109+
110+
const prompt = `You are writing the long description for an entry in Temporal's Code Exchange — a curated directory of community-built Temporal projects on temporal.io/code-exchange.
111+
112+
The description will be stored as markdown in Contentful and rendered on the website. Write it to be clear, accurate, and useful to developers evaluating whether this project is relevant to them. Avoid marketing language. Do not use superlatives. Be technical but approachable.
113+
114+
## Project Details
115+
116+
**Title:** ${issue.title.replace(/^\[Submission\]\s*/i, "").trim()}
117+
**GitHub URL:** ${parsed.url}
118+
**Language(s):** ${language}
119+
**License:** ${repoData?.license?.name || "unknown"}
120+
**Stars:** ${repoData?.stargazers_count ?? "unknown"}
121+
**Submitter description (short):** ${shortDesc}
122+
**Submitter description (long):** ${longDesc}
123+
**Author(s):** ${authors}
124+
125+
**README:**
126+
\`\`\`
127+
${readmeContent || "No README available."}
128+
\`\`\`
129+
130+
## Output Format
131+
132+
Write the Contentful markdown description using exactly this structure. Output only the markdown — no preamble, no explanation:
133+
134+
### What it does
135+
136+
[2–3 sentences describing what the project is and what problem it solves. Be specific.]
137+
138+
### How it uses Temporal
139+
140+
[2–3 sentences describing which Temporal concepts or patterns this project demonstrates or relies on — e.g. durable execution, activity retries, signals/queries, schedules, child workflows, sagas, etc. Be concrete.]
141+
142+
### Who it's for
143+
144+
[1–2 sentences describing the intended audience — e.g. developers building X, teams who need Y.]
145+
146+
### Getting started
147+
148+
[Brief summary of how to run or use the project, based on the README. If the README has clear setup steps, summarize them. If not, say so.]`;
149+
150+
const client = new Anthropic({ apiKey: ANTHROPIC_API_KEY });
151+
const message = await client.messages.create({
152+
model: "claude-opus-4-5",
153+
max_tokens: 1024,
154+
messages: [{ role: "user", content: prompt }],
155+
});
156+
157+
const description = message.content[0].text.trim();
158+
159+
const comment =
160+
`## 📋 Contentful Description\n\n` +
161+
`Here's a ready-to-paste markdown description for this entry. Copy the block below into Contentful:\n\n` +
162+
`---\n\n` +
163+
description +
164+
`\n\n---\n\n` +
165+
`*Generated by ZiggyBot based on the project README and submission details.*`;
166+
167+
await postComment(comment);
168+
}
169+
170+
main().catch(async (err) => {
171+
console.error(err);
172+
await postComment(
173+
`## 📋 Contentful Description\n\n` +
174+
`ZiggyBot ran into an error generating the description. Please write it manually.\n\n` +
175+
`\`\`\`\n${err.message}\n\`\`\``
176+
).catch(() => {});
177+
process.exit(1);
178+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Generate Contentful Description
2+
3+
on:
4+
issue_comment:
5+
types: [edited]
6+
7+
jobs:
8+
generate:
9+
# Fire when the approval checkbox in a ZiggyBot comment is checked.
10+
# Conditions:
11+
# 1. The comment was posted by github-actions[bot] (ZiggyBot)
12+
# 2. The new body has the checkbox checked
13+
# 3. The previous body had the checkbox unchecked (i.e. this edit just checked it)
14+
if: |
15+
github.event.comment.user.login == 'github-actions[bot]' &&
16+
contains(github.event.comment.body, '[x] Generate long description if needed') &&
17+
contains(github.event.changes.body.from, '[ ] Generate long description if needed')
18+
runs-on: ubuntu-latest
19+
permissions:
20+
issues: write
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- uses: actions/setup-node@v4
26+
with:
27+
node-version: "20"
28+
29+
- name: Install dependencies
30+
run: npm install
31+
working-directory: .github/scripts
32+
33+
- name: Check editor has write access
34+
id: check_perm
35+
env:
36+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37+
run: |
38+
perm=$(gh api repos/${{ github.repository }}/collaborators/${{ github.event.sender.login }}/permission --jq '.permission' 2>/dev/null || echo "none")
39+
echo "permission=$perm" >> $GITHUB_OUTPUT
40+
41+
- name: Generate Contentful description
42+
if: |
43+
steps.check_perm.outputs.permission == 'admin' ||
44+
steps.check_perm.outputs.permission == 'maintain' ||
45+
steps.check_perm.outputs.permission == 'write'
46+
env:
47+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
48+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49+
GITHUB_REPOSITORY: ${{ github.repository }}
50+
ISSUE_NUMBER: ${{ github.event.issue.number }}
51+
run: node .github/scripts/generate-contentful.mjs
52+
53+
- name: Warn if insufficient permissions
54+
if: |
55+
steps.check_perm.outputs.permission != 'admin' &&
56+
steps.check_perm.outputs.permission != 'maintain' &&
57+
steps.check_perm.outputs.permission != 'write'
58+
env:
59+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60+
run: |
61+
gh issue comment ${{ github.event.issue.number }} \
62+
--repo ${{ github.repository }} \
63+
--body "Only repository collaborators with write access can approve submissions. @${{ github.event.sender.login }} does not have sufficient permissions."

0 commit comments

Comments
 (0)