name: Create a release discussions for each new version label on: schedule: - cron: "0 * * * *" # every hour workflow_dispatch: # allow manual runs permissions: contents: read discussions: write issues: read pull-requests: read jobs: reconcile: runs-on: ubuntu-latest steps: - name: Reconcile release/* labels → discussions uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const owner = context.repo.owner; const repo = context.repo.repo; const categoryName = "Releases"; // 24h cutoff const since = new Date(Date.now() - 24*60*60*1000).toISOString(); core.info(`Scanning issues/PRs updated since ${since}`); // fetch repo + discussion categories const repoData = await github.graphql(` query($owner:String!, $repo:String!){ repository(owner:$owner, name:$repo){ id discussionCategories(first:100){ nodes { id name } } } } `, { owner, repo }); const repoId = repoData.repository.id; const category = repoData.repository.discussionCategories.nodes.find(c => c.name === categoryName); if (!category) { core.setFailed(`Discussion category "${categoryName}" not found`); return; } const categoryId = category.id; // paginate issues/PRs updated in last 24h for await (const { data: items } of github.paginate.iterator( github.rest.issues.listForRepo, { owner, repo, state: "all", since, per_page: 100 } )) { for (const item of items) { const releaseLabels = (item.labels || []) .map(l => (typeof l === "string" ? l : l.name)) // always get the name .filter(n => typeof n === "string" && n.startsWith("release/") && n !== "release/no-notes"); if (releaseLabels.length === 0) continue; core.info(`#${item.number}: ${releaseLabels.join(", ")}`); for (const labelName of releaseLabels) { const version = labelName.substring("release/".length); const titleTarget = `Release: ${version}`; // search discussions let discussionId = null; let cursor = null; while (true) { const page = await github.graphql(` query($owner:String!, $repo:String!, $cursor:String){ repository(owner:$owner, name:$repo){ discussions(first:50, after:$cursor){ nodes{ id title url category{ name } labels(first:50){ nodes{ name } } } pageInfo{ hasNextPage endCursor } } } } `, { owner, repo, cursor }); const nodes = page.repository.discussions.nodes; const byLabel = nodes.find(d => d.category?.name === categoryName && d.labels?.nodes?.some(l => l.name === labelName) ); if (byLabel) { discussionId = byLabel.id; break; } const byTitle = nodes.find(d => d.category?.name === categoryName && d.title === titleTarget ); if (byTitle) { discussionId = byTitle.id; break; } if (!page.repository.discussions.pageInfo.hasNextPage) break; cursor = page.repository.discussions.pageInfo.endCursor; } if (!discussionId) { core.info(`→ Creating discussion for ${labelName}`); const body = `**Release date:** TODO (YYYY-MM-DD)\n\n` + `### Links\n` + `- [Issues and pull requests marked for version ${version}](https://github.com/${owner}/${repo}/issues?q=label%3A${encodeURIComponent(labelName)})\n`; const created = await github.graphql(` mutation($repoId:ID!, $catId:ID!, $title:String!, $body:String!){ createDiscussion(input:{ repositoryId:$repoId, categoryId:$catId, title:$title, body:$body }){ discussion{ id url } } } `, { repoId, catId: categoryId, title: titleTarget, body }); discussionId = created.createDiscussion.discussion.id; // lock the discussion to prevent replies await github.graphql(` mutation($id:ID!){ lockLockable(input:{ lockableId:$id }) { clientMutationId } } `, { id: discussionId }); core.info(`🔒 Locked discussion ${discussionId}`); } else { core.info(`→ Found existing discussion for ${labelName}`); } // ensure label exists let labelId; try { await github.rest.issues.getLabel({ owner, repo, name: labelName }); } catch (e) { if (e.status === 404) { await github.rest.issues.createLabel({ owner, repo, name: labelName, color: "0E8A16" }); } else { throw e; } } const labelNode = await github.graphql(` query($owner:String!, $repo:String!, $name:String!){ repository(owner:$owner, name:$repo){ label(name:$name){ id } } } `, { owner, repo, name: labelName }); labelId = labelNode.repository.label?.id; if (!labelId) continue; // add label to discussion await github.graphql(` mutation($id:ID!, $labels:[ID!]!){ addLabelsToLabelable(input:{ labelableId:$id, labelIds:$labels }) { clientMutationId } } `, { id: discussionId, labels: [labelId] }); core.info(`✓ ${labelName} attached to discussion`); } } }