-
-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
48946f4
commit 7ef0a73
Showing
5 changed files
with
284 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
name: Create GitHub Discussions | ||
|
||
on: | ||
pull_request: | ||
types: [opened] | ||
paths: | ||
- 'src/content/apps/**' | ||
workflow_dispatch: | ||
inputs: | ||
createForExisting: | ||
description: 'Create discussions for existing apps' | ||
required: true | ||
default: 'true' | ||
type: boolean | ||
|
||
jobs: | ||
create-discussion: | ||
runs-on: ubuntu-latest | ||
permissions: | ||
discussions: write | ||
pull-requests: read | ||
contents: read | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Create Discussion for PR | ||
if: github.event_name == 'pull_request' | ||
uses: actions/github-script@v7 | ||
with: | ||
script: | | ||
const { data: categories } = await github.rest.discussions.listRepoCategories({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo | ||
}); | ||
let category = categories.find(c => c.name === "app-votes"); | ||
if (!category) { | ||
const { data: newCategory } = await github.rest.discussions.createRepoCategory({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
name: "app-votes", | ||
description: "Vote for community apps", | ||
format: "discussion" | ||
}); | ||
category = newCategory; | ||
} | ||
const { data: files } = await github.rest.pulls.listFiles({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
pull_number: context.issue.number | ||
}); | ||
const appFiles = files.filter(file => | ||
file.status === 'added' && | ||
file.filename.startsWith('src/content/apps/') && | ||
!file.filename.endsWith('_template.md') | ||
); | ||
for (const file of appFiles) { | ||
const content = Buffer.from((await github.rest.repos.getContent({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
path: file.filename, | ||
ref: context.payload.pull_request.head.sha | ||
})).data.content, 'base64').toString(); | ||
const frontmatterMatch = content.match(/---\n([\s\S]*?)\n---/); | ||
if (!frontmatterMatch) continue; | ||
const frontmatter = frontmatterMatch[1]; | ||
const appData = {}; | ||
frontmatter.split('\n').forEach(line => { | ||
const match = line.match(/^(\w+):\s*"?([^"]*)"?$/); | ||
if (match) { | ||
appData[match[1]] = match[2]; | ||
} | ||
}); | ||
if (!appData.name || !appData.author) { | ||
console.log('Missing required fields in frontmatter'); | ||
continue; | ||
} | ||
const query = `query($searchQuery: String!) { | ||
search(query: $searchQuery, type: DISCUSSION, first: 10) { | ||
nodes { | ||
... on Discussion { | ||
title | ||
} | ||
} | ||
} | ||
}`; | ||
const searchResult = await github.graphql(query, { | ||
searchQuery: `repo:${context.repo.owner}/${context.repo.repo} "${appData.name} by ${appData.author}" in:title` | ||
}); | ||
const existingDiscussion = searchResult.search.nodes.find(node => | ||
node.title === `Vote: ${appData.name} by ${appData.author}` | ||
); | ||
if (!existingDiscussion) { | ||
await github.rest.discussions.create({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
title: `Vote: ${appData.name} by ${appData.author}`, | ||
body: `🗳️ **Vote for this app by giving it a 👍 reaction!**\n\n${appData.description}`, | ||
category_id: category.id | ||
}); | ||
console.log(`Created discussion for ${appData.name}`); | ||
} else { | ||
console.log(`Discussion already exists for ${appData.name}`); | ||
} | ||
} | ||
- name: Create Discussions for Existing Apps | ||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.createForExisting == 'true' | ||
uses: actions/github-script@v7 | ||
with: | ||
script: | | ||
const { data: categories } = await github.rest.discussions.listRepoCategories({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo | ||
}); | ||
let category = categories.find(c => c.name === "app-votes"); | ||
const { data: contents } = await github.rest.repos.getContent({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
path: 'src/content/apps' | ||
}); | ||
for (const file of contents) { | ||
if (file.type !== 'file' || file.name.endsWith('_template.md')) continue; | ||
const content = Buffer.from((await github.rest.repos.getContent({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
path: file.path | ||
})).data.content, 'base64').toString(); | ||
const frontmatterMatch = content.match(/---\n([\s\S]*?)\n---/); | ||
if (!frontmatterMatch) continue; | ||
const frontmatter = frontmatterMatch[1]; | ||
const appData = {}; | ||
frontmatter.split('\n').forEach(line => { | ||
const match = line.match(/^(\w+):\s*"?([^"]*)"?$/); | ||
if (match) { | ||
appData[match[1]] = match[2]; | ||
} | ||
}); | ||
if (!appData.name || !appData.author) { | ||
console.log('Missing required fields in frontmatter'); | ||
continue; | ||
} | ||
const query = `query($searchQuery: String!) { | ||
search(query: $searchQuery, type: DISCUSSION, first: 10) { | ||
nodes { | ||
... on Discussion { | ||
title | ||
} | ||
} | ||
} | ||
}`; | ||
const searchResult = await github.graphql(query, { | ||
searchQuery: `repo:${context.repo.owner}/${context.repo.repo} "${appData.name} by ${appData.author}" in:title` | ||
}); | ||
const existingDiscussion = searchResult.search.nodes.find(node => | ||
node.title === `Vote: ${appData.name} by ${appData.author}` | ||
); | ||
if (!existingDiscussion) { | ||
await github.rest.discussions.create({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
title: `Vote: ${appData.name} by ${appData.author}`, | ||
body: `🗳️ **Vote for this app by giving it a 👍 reaction!**\n\n${appData.description}`, | ||
category_id: category.id | ||
}); | ||
console.log(`Created discussion for ${appData.name}`); | ||
} else { | ||
console.log(`Discussion already exists for ${appData.name}`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
name: Update Vote Counts | ||
|
||
on: | ||
schedule: | ||
- cron: '0 */6 * * *' # Run every 6 hours | ||
workflow_dispatch: # Allow manual triggers | ||
|
||
jobs: | ||
update-votes: | ||
runs-on: ubuntu-latest | ||
permissions: | ||
contents: write | ||
discussions: read | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Update Vote Counts | ||
uses: actions/github-script@v7 | ||
with: | ||
script: | | ||
const fs = require('fs').promises; | ||
const path = require('path'); | ||
const yaml = require('js-yaml'); | ||
// Get all discussions in the Flutter of the Year category | ||
const discussions = await github.paginate(github.rest.discussions.listForRepo, { | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
category_id: process.env.DISCUSSION_CATEGORY_ID | ||
}); | ||
// Create votes.json with the current vote counts | ||
const votes = {}; | ||
for (const discussion of discussions) { | ||
if (discussion.title.startsWith('Vote: ')) { | ||
const appName = discussion.title.replace('Vote: ', '').split(' by ')[0]; | ||
// Count thumbs up reactions | ||
const reactions = await github.rest.reactions.listForDiscussion({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
discussion_number: discussion.number | ||
}); | ||
votes[appName] = reactions.data.filter(r => r.content === '+1').length; | ||
} | ||
} | ||
// Write the votes to a JSON file | ||
await fs.writeFile( | ||
'src/data/votes.json', | ||
JSON.stringify(votes, null, 2) | ||
); | ||
// Commit and push the changes | ||
const date = new Date().toISOString(); | ||
await exec.exec('git', ['config', 'user.name', 'github-actions[bot]']); | ||
await exec.exec('git', ['config', 'user.email', 'github-actions[bot]@users.noreply.github.com']); | ||
await exec.exec('git', ['add', 'src/data/votes.json']); | ||
await exec.exec('git', ['commit', '-m', `Update vote counts - ${date}`]); | ||
await exec.exec('git', ['push']); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
export interface VoteData { | ||
[appName: string]: number; | ||
} | ||
|
||
export function getVotes(): VoteData { | ||
try { | ||
// During build time, this file will be created by GitHub Actions | ||
// We import it as a module to get the data | ||
const votes = import.meta.glob('/src/data/votes.json', { eager: true }); | ||
return Object.values(votes)[0] as VoteData || {}; | ||
} catch (error) { | ||
console.error('Error reading votes:', error); | ||
return {}; | ||
} | ||
} |